From 84fe15d948c1c20aed4660b79deb08a51a782383 Mon Sep 17 00:00:00 2001 From: Vittorio Minacori Date: Wed, 27 Sep 2023 16:11:25 +0200 Subject: [PATCH 01/82] Add ERC1363 contracts --- contracts/interfaces/IERC1363.sol | 83 ++++++----- contracts/interfaces/IERC1363Errors.sol | 33 +++++ contracts/interfaces/IERC1363Receiver.sol | 37 +++-- contracts/interfaces/IERC1363Spender.sol | 33 ++--- contracts/token/ERC20/extensions/ERC1363.sol | 140 +++++++++++++++++++ 5 files changed, 250 insertions(+), 76 deletions(-) create mode 100644 contracts/interfaces/IERC1363Errors.sol create mode 100644 contracts/token/ERC20/extensions/ERC1363.sol diff --git a/contracts/interfaces/IERC1363.sol b/contracts/interfaces/IERC1363.sol index d1b555a11cb..39a2ba85b47 100644 --- a/contracts/interfaces/IERC1363.sol +++ b/contracts/interfaces/IERC1363.sol @@ -7,13 +7,14 @@ import {IERC20} from "./IERC20.sol"; import {IERC165} from "./IERC165.sol"; /** - * @dev Interface of an ERC1363 compliant contract, as defined in the - * https://eips.ethereum.org/EIPS/eip-1363[EIP]. + * @title IERC1363 + * @dev Interface of the ERC1363 standard as defined in the + * https://eips.ethereum.org/EIPS/eip-1363[EIP-1363]. * * Defines a interface for ERC20 tokens that supports executing recipient * code after `transfer` or `transferFrom`, or spender code after `approve`. */ -interface IERC1363 is IERC165, IERC20 { +interface IERC1363 is IERC20, IERC165 { /* * Note: the ERC-165 identifier for this interface is 0xb0202a11. * 0xb0202a11 === @@ -26,55 +27,61 @@ interface IERC1363 is IERC165, IERC20 { */ /** - * @dev Transfer tokens from `msg.sender` to another address and then call `onTransferReceived` on receiver - * @param to address The address which you want to transfer to - * @param amount uint256 The amount of tokens to be transferred - * @return true unless throwing + * @dev Moves a `value` amount of tokens from the caller's account to `to` + * and then calls `onTransferReceived` on `to`. + * @param to The address which you want to transfer to. + * @param value The amount of tokens to be transferred. + * @return A boolean value indicating whether the operation succeeded unless throwing. */ - function transferAndCall(address to, uint256 amount) external returns (bool); + function transferAndCall(address to, uint256 value) external returns (bool); /** - * @dev Transfer tokens from `msg.sender` to another address and then call `onTransferReceived` on receiver - * @param to address The address which you want to transfer to - * @param amount uint256 The amount of tokens to be transferred - * @param data bytes Additional data with no specified format, sent in call to `to` - * @return true unless throwing + * @dev Moves a `value` amount of tokens from the caller's account to `to` + * and then calls `onTransferReceived` on `to`. + * @param to The address which you want to transfer to. + * @param value The amount of tokens to be transferred. + * @param data Additional data with no specified format, sent in call to `to`. + * @return A boolean value indicating whether the operation succeeded unless throwing. */ - function transferAndCall(address to, uint256 amount, bytes memory data) external returns (bool); + function transferAndCall(address to, uint256 value, bytes calldata data) external returns (bool); /** - * @dev Transfer tokens from one address to another and then call `onTransferReceived` on receiver - * @param from address The address which you want to send tokens from - * @param to address The address which you want to transfer to - * @param amount uint256 The amount of tokens to be transferred - * @return true unless throwing + * @dev Moves a `value` amount of tokens from `from` to `to` using the allowance mechanism + * and then calls `onTransferReceived` on `to`. + * @param from The address which you want to send tokens from. + * @param to The address which you want to transfer to. + * @param value The amount of tokens to be transferred. + * @return A boolean value indicating whether the operation succeeded unless throwing. */ - function transferFromAndCall(address from, address to, uint256 amount) external returns (bool); + function transferFromAndCall(address from, address to, uint256 value) external returns (bool); /** - * @dev Transfer tokens from one address to another and then call `onTransferReceived` on receiver - * @param from address The address which you want to send tokens from - * @param to address The address which you want to transfer to - * @param amount uint256 The amount of tokens to be transferred - * @param data bytes Additional data with no specified format, sent in call to `to` - * @return true unless throwing + * @dev Moves a `value` amount of tokens from `from` to `to` using the allowance mechanism + * and then calls `onTransferReceived` on `to`. + * @param from The address which you want to send tokens from. + * @param to The address which you want to transfer to. + * @param value The amount of tokens to be transferred. + * @param data Additional data with no specified format, sent in call to `to`. + * @return A boolean value indicating whether the operation succeeded unless throwing. */ - function transferFromAndCall(address from, address to, uint256 amount, bytes memory data) external returns (bool); + function transferFromAndCall(address from, address to, uint256 value, bytes calldata data) external returns (bool); /** - * @dev Approve the passed address to spend the specified amount of tokens on behalf of msg.sender - * and then call `onApprovalReceived` on spender. - * @param spender address The address which will spend the funds - * @param amount uint256 The amount of tokens to be spent + * @dev Sets a `value` amount of tokens as the allowance of `spender` over the + * caller's tokens and then calls `onApprovalReceived` on `spender`. + * @param spender The address which will spend the funds. + * @param value The amount of tokens to be spent. + * @return A boolean value indicating whether the operation succeeded unless throwing. */ - function approveAndCall(address spender, uint256 amount) external returns (bool); + function approveAndCall(address spender, uint256 value) external returns (bool); /** - * @dev Approve the passed address to spend the specified amount of tokens on behalf of msg.sender - * and then call `onApprovalReceived` on spender. - * @param spender address The address which will spend the funds - * @param amount uint256 The amount of tokens to be spent - * @param data bytes Additional data with no specified format, sent in call to `spender` + * @dev Sets a `value` amount of tokens as the allowance of `spender` over the + * caller's tokens and then calls `onApprovalReceived` on `spender`. + * @param spender The address which will spend the funds. + * @param value The amount of tokens to be spent. + * @param data Additional data with no specified format, sent in call to `spender`. + * @return A boolean value indicating whether the operation succeeded unless throwing. */ - function approveAndCall(address spender, uint256 amount, bytes memory data) external returns (bool); + function approveAndCall(address spender, uint256 value, bytes calldata data) external returns (bool); } diff --git a/contracts/interfaces/IERC1363Errors.sol b/contracts/interfaces/IERC1363Errors.sol new file mode 100644 index 00000000000..fa324461733 --- /dev/null +++ b/contracts/interfaces/IERC1363Errors.sol @@ -0,0 +1,33 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.20; + +/** + * @title IERC1363Errors + * @dev Interface of the ERC1363 custom errors following the https://eips.ethereum.org/EIPS/eip-6093[ERC-6093] rationale. + */ +interface IERC1363Errors { + /** + * @dev Indicates a failure with the token `receiver` as it can't be an EOA. Used in transfers. + * @param receiver Address to which tokens are being transferred. + */ + error ERC1363EOAReceiver(address receiver); + + /** + * @dev Indicates a failure with the token `spender` as it can't be an EOA. Used in approvals. + * @param spender Address that may be allowed to operate on tokens without being their owner. + */ + error ERC1363EOASpender(address spender); + + /** + * @dev Indicates a failure with the token `receiver`. Used in transfers. + * @param receiver Address to which tokens are being transferred. + */ + error ERC1363InvalidReceiver(address receiver); + + /** + * @dev Indicates a failure with the token `spender`. Used in approvals. + * @param spender Address that may be allowed to operate on tokens without being their owner. + */ + error ERC1363InvalidSpender(address spender); +} diff --git a/contracts/interfaces/IERC1363Receiver.sol b/contracts/interfaces/IERC1363Receiver.sol index 61f32ba3427..664ee54baf1 100644 --- a/contracts/interfaces/IERC1363Receiver.sol +++ b/contracts/interfaces/IERC1363Receiver.sol @@ -4,32 +4,29 @@ pragma solidity ^0.8.20; /** - * @dev Interface for any contract that wants to support {IERC1363-transferAndCall} - * or {IERC1363-transferFromAndCall} from {ERC1363} token contracts. + * @title IERC1363Receiver + * @dev Interface for any contract that wants to support `transferAndCall` or `transferFromAndCall` + * from ERC1363 token contracts. */ interface IERC1363Receiver { - /* - * Note: the ERC-165 identifier for this interface is 0x88a7ca5c. - * 0x88a7ca5c === bytes4(keccak256("onTransferReceived(address,address,uint256,bytes)")) - */ - /** - * @notice Handle the receipt of ERC1363 tokens - * @dev Any ERC1363 smart contract calls this function on the recipient - * after a `transfer` or a `transferFrom`. This function MAY throw to revert and reject the - * transfer. Return of other than the magic value MUST result in the - * transaction being reverted. - * Note: the token contract address is always the message sender. - * @param operator address The address which called `transferAndCall` or `transferFromAndCall` function - * @param from address The address which are token transferred from - * @param amount uint256 The amount of tokens transferred - * @param data bytes Additional data with no specified format - * @return `bytes4(keccak256("onTransferReceived(address,address,uint256,bytes)"))` unless throwing + * @dev Whenever ERC1363 tokens are transferred to this contract via `transferAndCall` or `transferFromAndCall` + * by `operator` from `from`, this function is called. + * + * NOTE: To accept the transfer, this must return + * `bytes4(keccak256("onTransferReceived(address,address,uint256,bytes)"))` + * (i.e. 0x88a7ca5c, or its own function selector). + * + * @param operator The address which called `transferAndCall` or `transferFromAndCall` function. + * @param from The address which are tokens transferred from. + * @param value The amount of tokens transferred. + * @param data Additional data with no specified format. + * @return `bytes4(keccak256("onTransferReceived(address,address,uint256,bytes)"))` if transfer is allowed unless throwing. */ function onTransferReceived( address operator, address from, - uint256 amount, - bytes memory data + uint256 value, + bytes calldata data ) external returns (bytes4); } diff --git a/contracts/interfaces/IERC1363Spender.sol b/contracts/interfaces/IERC1363Spender.sol index ab9e6214086..32a5b7b55e4 100644 --- a/contracts/interfaces/IERC1363Spender.sol +++ b/contracts/interfaces/IERC1363Spender.sol @@ -4,26 +4,23 @@ pragma solidity ^0.8.20; /** - * @dev Interface for any contract that wants to support {IERC1363-approveAndCall} - * from {ERC1363} token contracts. + * @title ERC1363Spender + * @dev Interface for any contract that wants to support `approveAndCall` + * from ERC1363 token contracts. */ interface IERC1363Spender { - /* - * Note: the ERC-165 identifier for this interface is 0x7b04a2d0. - * 0x7b04a2d0 === bytes4(keccak256("onApprovalReceived(address,uint256,bytes)")) - */ - /** - * @notice Handle the approval of ERC1363 tokens - * @dev Any ERC1363 smart contract calls this function on the recipient - * after an `approve`. This function MAY throw to revert and reject the - * approval. Return of other than the magic value MUST result in the - * transaction being reverted. - * Note: the token contract address is always the message sender. - * @param owner address The address which called `approveAndCall` function - * @param amount uint256 The amount of tokens to be spent - * @param data bytes Additional data with no specified format - * @return `bytes4(keccak256("onApprovalReceived(address,uint256,bytes)"))`unless throwing + * @dev Whenever an ERC1363 tokens `owner` approved this contract via `approveAndCall` + * to spent their tokens, this function is called. + * + * NOTE: To accept the approval, this must return + * `bytes4(keccak256("onApprovalReceived(address,uint256,bytes)"))` + * (i.e. 0x7b04a2d0, or its own function selector). + * + * @param owner The address which called `approveAndCall` function and previously owned the tokens. + * @param value The amount of tokens to be spent. + * @param data Additional data with no specified format. + * @return `bytes4(keccak256("onApprovalReceived(address,uint256,bytes)"))` if approval is allowed unless throwing. */ - function onApprovalReceived(address owner, uint256 amount, bytes memory data) external returns (bytes4); + function onApprovalReceived(address owner, uint256 value, bytes calldata data) external returns (bytes4); } diff --git a/contracts/token/ERC20/extensions/ERC1363.sol b/contracts/token/ERC20/extensions/ERC1363.sol new file mode 100644 index 00000000000..43c39acbcb6 --- /dev/null +++ b/contracts/token/ERC20/extensions/ERC1363.sol @@ -0,0 +1,140 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.0; + +import {ERC20} from "../ERC20.sol"; +import {IERC165, ERC165} from "../../../utils/introspection/ERC165.sol"; + +import {IERC1363} from "../../../interfaces/IERC1363.sol"; +import {IERC1363Errors} from "../../../interfaces/IERC1363Errors.sol"; +import {IERC1363Receiver} from "../../../interfaces/IERC1363Receiver.sol"; +import {IERC1363Spender} from "../../../interfaces/IERC1363Spender.sol"; + +/** + * @title ERC1363 + * @dev Implementation of the ERC1363 interface. + */ +abstract contract ERC1363 is ERC20, ERC165, IERC1363, IERC1363Errors { + /** + * @inheritdoc IERC165 + */ + function supportsInterface(bytes4 interfaceId) public view virtual override(ERC165, IERC165) returns (bool) { + return interfaceId == type(IERC1363).interfaceId || super.supportsInterface(interfaceId); + } + + /** + * @inheritdoc IERC1363 + */ + function transferAndCall(address to, uint256 value) public virtual returns (bool) { + return transferAndCall(to, value, ""); + } + + /** + * @inheritdoc IERC1363 + */ + function transferAndCall(address to, uint256 value, bytes memory data) public virtual returns (bool) { + transfer(to, value); + _checkOnTransferReceived(_msgSender(), to, value, data); + return true; + } + + /** + * @inheritdoc IERC1363 + */ + function transferFromAndCall(address from, address to, uint256 value) public virtual returns (bool) { + return transferFromAndCall(from, to, value, ""); + } + + /** + * @inheritdoc IERC1363 + */ + function transferFromAndCall( + address from, + address to, + uint256 value, + bytes memory data + ) public virtual returns (bool) { + transferFrom(from, to, value); + _checkOnTransferReceived(from, to, value, data); + return true; + } + + /** + * @inheritdoc IERC1363 + */ + function approveAndCall(address spender, uint256 value) public virtual returns (bool) { + return approveAndCall(spender, value, ""); + } + + /** + * @inheritdoc IERC1363 + */ + function approveAndCall(address spender, uint256 value, bytes memory data) public virtual returns (bool) { + approve(spender, value); + _checkOnApprovalReceived(spender, value, data); + return true; + } + + /** + * @dev Private function to invoke `onTransferReceived` on a target address. + * This will revert if the target doesn't implement the `IERC1363Receiver` interface or + * if the target doesn't accept the token transfer or + * if the target address is not a contract. + * + * @param from Address representing the previous owner of the given token amount. + * @param to Target address that will receive the tokens. + * @param value The amount of tokens to be transferred. + * @param data Optional data to send along with the call. + */ + function _checkOnTransferReceived(address from, address to, uint256 value, bytes memory data) private { + if (to.code.length == 0) { + revert ERC1363EOAReceiver(to); + } + + try IERC1363Receiver(to).onTransferReceived(_msgSender(), from, value, data) returns (bytes4 retval) { + if (retval != IERC1363Receiver.onTransferReceived.selector) { + revert ERC1363InvalidReceiver(to); + } + } catch (bytes memory reason) { + if (reason.length == 0) { + revert ERC1363InvalidReceiver(to); + } else { + /// @solidity memory-safe-assembly + assembly { + revert(add(32, reason), mload(reason)) + } + } + } + } + + /** + * @dev Private function to invoke `onApprovalReceived` on a target address. + * This will revert if the target doesn't implement the `IERC1363Spender` interface or + * if the target doesn't accept the token approval or + * if the target address is not a contract. + * + * @param spender The address which will spend the funds. + * @param value The amount of tokens to be spent. + * @param data Optional data to send along with the call. + */ + function _checkOnApprovalReceived(address spender, uint256 value, bytes memory data) private { + if (spender.code.length == 0) { + revert ERC1363EOASpender(spender); + } + + try IERC1363Spender(spender).onApprovalReceived(_msgSender(), value, data) returns (bytes4 retval) { + if (retval != IERC1363Spender.onApprovalReceived.selector) { + revert ERC1363InvalidSpender(spender); + } + } catch (bytes memory reason) { + if (reason.length == 0) { + revert ERC1363InvalidSpender(spender); + } else { + /// @solidity memory-safe-assembly + assembly { + revert(add(32, reason), mload(reason)) + } + } + } + } +} From 67d6fcda3d73e81692d70ac4035af183ae68bf9a Mon Sep 17 00:00:00 2001 From: Vittorio Minacori Date: Wed, 27 Sep 2023 16:21:02 +0200 Subject: [PATCH 02/82] Add ERC1363 tests --- contracts/mocks/token/ERC1363ReceiverMock.sol | 47 ++ contracts/mocks/token/ERC1363SpenderMock.sol | 42 ++ .../ERC20/extensions/ERC1363.behaviour.js | 583 ++++++++++++++++++ test/token/ERC20/extensions/ERC1363.test.js | 17 + .../SupportsInterface.behavior.js | 10 + 5 files changed, 699 insertions(+) create mode 100644 contracts/mocks/token/ERC1363ReceiverMock.sol create mode 100644 contracts/mocks/token/ERC1363SpenderMock.sol create mode 100644 test/token/ERC20/extensions/ERC1363.behaviour.js create mode 100644 test/token/ERC20/extensions/ERC1363.test.js diff --git a/contracts/mocks/token/ERC1363ReceiverMock.sol b/contracts/mocks/token/ERC1363ReceiverMock.sol new file mode 100644 index 00000000000..2cacd941b32 --- /dev/null +++ b/contracts/mocks/token/ERC1363ReceiverMock.sol @@ -0,0 +1,47 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.0; + +import {IERC1363Receiver} from "../../interfaces/IERC1363Receiver.sol"; + +contract ERC1363ReceiverMock is IERC1363Receiver { + enum RevertType { + None, + RevertWithoutMessage, + RevertWithMessage, + RevertWithCustomError, + Panic + } + + bytes4 private immutable _retval; + RevertType private immutable _error; + + event Received(address operator, address from, uint256 value, bytes data, uint256 gas); + error CustomError(bytes4); + + constructor(bytes4 retval, RevertType error) { + _retval = retval; + _error = error; + } + + function onTransferReceived( + address operator, + address from, + uint256 value, + bytes calldata data + ) public override returns (bytes4) { + if (_error == RevertType.RevertWithoutMessage) { + revert(); + } else if (_error == RevertType.RevertWithMessage) { + revert("ERC1363ReceiverMock: reverting"); + } else if (_error == RevertType.RevertWithCustomError) { + revert CustomError(_retval); + } else if (_error == RevertType.Panic) { + uint256 a = uint256(0) / uint256(0); + a; + } + + emit Received(operator, from, value, data, gasleft()); + return _retval; + } +} diff --git a/contracts/mocks/token/ERC1363SpenderMock.sol b/contracts/mocks/token/ERC1363SpenderMock.sol new file mode 100644 index 00000000000..a1f5d199a38 --- /dev/null +++ b/contracts/mocks/token/ERC1363SpenderMock.sol @@ -0,0 +1,42 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.0; + +import {IERC1363Spender} from "../../interfaces/IERC1363Spender.sol"; + +contract ERC1363SpenderMock is IERC1363Spender { + enum RevertType { + None, + RevertWithoutMessage, + RevertWithMessage, + RevertWithCustomError, + Panic + } + + bytes4 private immutable _retval; + RevertType private immutable _error; + + event Approved(address owner, uint256 value, bytes data, uint256 gas); + error CustomError(bytes4); + + constructor(bytes4 retval, RevertType error) { + _retval = retval; + _error = error; + } + + function onApprovalReceived(address owner, uint256 value, bytes calldata data) public override returns (bytes4) { + if (_error == RevertType.RevertWithoutMessage) { + revert(); + } else if (_error == RevertType.RevertWithMessage) { + revert("ERC1363SpenderMock: reverting"); + } else if (_error == RevertType.RevertWithCustomError) { + revert CustomError(_retval); + } else if (_error == RevertType.Panic) { + uint256 a = uint256(0) / uint256(0); + a; + } + + emit Approved(owner, value, data, gasleft()); + return _retval; + } +} diff --git a/test/token/ERC20/extensions/ERC1363.behaviour.js b/test/token/ERC20/extensions/ERC1363.behaviour.js new file mode 100644 index 00000000000..963e1c6f048 --- /dev/null +++ b/test/token/ERC20/extensions/ERC1363.behaviour.js @@ -0,0 +1,583 @@ +const { expectRevert, expectEvent } = require('@openzeppelin/test-helpers'); +const { expect } = require('chai'); + +const { shouldSupportInterfaces } = require('../../../utils/introspection/SupportsInterface.behavior'); +const { expectRevertCustomError } = require('../../../helpers/customError'); +const { Enum } = require('../../../helpers/enums'); + +const ERC1363Receiver = artifacts.require('ERC1363ReceiverMock'); +const ERC1363Spender = artifacts.require('ERC1363SpenderMock'); + +const RevertType = Enum('None', 'RevertWithoutMessage', 'RevertWithMessage', 'RevertWithCustomError', 'Panic'); + +function shouldBehaveLikeERC1363(initialSupply, accounts) { + const [owner, spender, recipient] = accounts; + + const RECEIVER_MAGIC_VALUE = '0x88a7ca5c'; + const SPENDER_MAGIC_VALUE = '0x7b04a2d0'; + + beforeEach(async function () { + await this.token.$_mint(owner, initialSupply); + }); + + shouldSupportInterfaces(['ERC165', 'ERC1363']); + + describe('transfers', function () { + const initialBalance = initialSupply; + const data = '0x42'; + + describe('via transferAndCall', function () { + const transferAndCallWithData = function (to, value, opts) { + return this.token.methods['transferAndCall(address,uint256,bytes)'](to, value, data, opts); + }; + + const transferAndCallWithoutData = function (to, value, opts) { + return this.token.methods['transferAndCall(address,uint256)'](to, value, opts); + }; + + const shouldTransferSafely = function (transferFunction, data) { + describe('to a valid receiver contract', function () { + beforeEach(async function () { + this.receiver = await ERC1363Receiver.new(RECEIVER_MAGIC_VALUE, RevertType.None); + this.to = this.receiver.address; + }); + + it('calls onTransferReceived', async function () { + const receipt = await transferFunction.call(this, this.to, initialBalance, { from: owner }); + + await expectEvent.inTransaction(receipt.tx, ERC1363Receiver, 'Received', { + operator: owner, + from: owner, + value: initialBalance, + data, + }); + }); + }); + }; + + const transferWasSuccessful = function (from, balance) { + let to; + + beforeEach(async function () { + const receiverContract = await ERC1363Receiver.new(RECEIVER_MAGIC_VALUE, RevertType.None); + to = receiverContract.address; + }); + + describe('when the sender does not have enough balance', function () { + const value = balance + 1; + + describe('with data', function () { + it('reverts', async function () { + await expectRevertCustomError( + transferAndCallWithData.call(this, to, value, { from }), + 'ERC20InsufficientBalance', + [from, balance, value], + ); + }); + }); + + describe('without data', function () { + it('reverts', async function () { + await expectRevertCustomError( + transferAndCallWithoutData.call(this, to, value, { from }), + 'ERC20InsufficientBalance', + [from, balance, value], + ); + }); + }); + }); + + describe('when the sender has enough balance', function () { + const value = balance; + + describe('with data', function () { + it('transfers the requested amount', async function () { + await transferAndCallWithData.call(this, to, value, { from }); + + expect(await this.token.balanceOf(from)).to.be.bignumber.equal('0'); + + expect(await this.token.balanceOf(to)).to.be.bignumber.equal(value); + }); + + it('emits a transfer event', async function () { + expectEvent(await transferAndCallWithData.call(this, to, value, { from }), 'Transfer', { + from, + to, + value, + }); + }); + }); + + describe('without data', function () { + it('transfers the requested amount', async function () { + await transferAndCallWithoutData.call(this, to, value, { from }); + + expect(await this.token.balanceOf(from)).to.be.bignumber.equal('0'); + + expect(await this.token.balanceOf(to)).to.be.bignumber.equal(value); + }); + + it('emits a transfer event', async function () { + expectEvent(await transferAndCallWithoutData.call(this, to, value, { from }), 'Transfer', { + from, + to, + value, + }); + }); + }); + }); + }; + + describe('with data', function () { + shouldTransferSafely(transferAndCallWithData, data); + }); + + describe('without data', function () { + shouldTransferSafely(transferAndCallWithoutData, null); + }); + + describe('testing ERC20 behaviours', function () { + transferWasSuccessful(owner, initialBalance); + }); + + describe('to a receiver that is not a contract', function () { + it('reverts', async function () { + await expectRevertCustomError( + transferAndCallWithoutData.call(this, recipient, initialBalance, { from: owner }), + 'ERC1363EOAReceiver', + [recipient], + ); + }); + }); + + describe('to a receiver contract returning unexpected value', function () { + it('reverts', async function () { + const invalidReceiver = await ERC1363Receiver.new(data, RevertType.None); + await expectRevertCustomError( + transferAndCallWithoutData.call(this, invalidReceiver.address, initialBalance, { from: owner }), + 'ERC1363InvalidReceiver', + [invalidReceiver.address], + ); + }); + }); + + describe('to a receiver contract that reverts with message', function () { + it('reverts', async function () { + const revertingReceiver = await ERC1363Receiver.new(RECEIVER_MAGIC_VALUE, RevertType.RevertWithMessage); + await expectRevert( + transferAndCallWithoutData.call(this, revertingReceiver.address, initialBalance, { from: owner }), + 'ERC1363ReceiverMock: reverting', + ); + }); + }); + + describe('to a receiver contract that reverts without message', function () { + it('reverts', async function () { + const revertingReceiver = await ERC1363Receiver.new(RECEIVER_MAGIC_VALUE, RevertType.RevertWithoutMessage); + await expectRevertCustomError( + transferAndCallWithoutData.call(this, revertingReceiver.address, initialBalance, { from: owner }), + 'ERC1363InvalidReceiver', + [revertingReceiver.address], + ); + }); + }); + + describe('to a receiver contract that reverts with custom error', function () { + it('reverts', async function () { + const revertingReceiver = await ERC1363Receiver.new(RECEIVER_MAGIC_VALUE, RevertType.RevertWithCustomError); + await expectRevertCustomError( + transferAndCallWithoutData.call(this, revertingReceiver.address, initialBalance, { from: owner }), + 'CustomError', + [RECEIVER_MAGIC_VALUE], + ); + }); + }); + + describe('to a receiver contract that panics', function () { + it('reverts', async function () { + const revertingReceiver = await ERC1363Receiver.new(RECEIVER_MAGIC_VALUE, RevertType.Panic); + await expectRevert.unspecified( + transferAndCallWithoutData.call(this, revertingReceiver.address, initialBalance, { from: owner }), + ); + }); + }); + + describe('to a contract that does not implement the required function', function () { + it('reverts', async function () { + const nonReceiver = this.token; + await expectRevertCustomError( + transferAndCallWithoutData.call(this, nonReceiver.address, initialBalance, { from: owner }), + 'ERC1363InvalidReceiver', + [nonReceiver.address], + ); + }); + }); + }); + + describe('via transferFromAndCall', function () { + beforeEach(async function () { + await this.token.approve(spender, initialBalance, { from: owner }); + }); + + const transferFromAndCallWithData = function (from, to, value, opts) { + return this.token.methods['transferFromAndCall(address,address,uint256,bytes)'](from, to, value, data, opts); + }; + + const transferFromAndCallWithoutData = function (from, to, value, opts) { + return this.token.methods['transferFromAndCall(address,address,uint256)'](from, to, value, opts); + }; + + const shouldTransferFromSafely = function (transferFunction, data) { + describe('to a valid receiver contract', function () { + beforeEach(async function () { + this.receiver = await ERC1363Receiver.new(RECEIVER_MAGIC_VALUE, RevertType.None); + this.to = this.receiver.address; + }); + + it('calls onTransferReceived', async function () { + const receipt = await transferFunction.call(this, owner, this.to, initialBalance, { from: spender }); + + await expectEvent.inTransaction(receipt.tx, ERC1363Receiver, 'Received', { + operator: spender, + from: owner, + value: initialBalance, + data, + }); + }); + }); + }; + + const transferFromWasSuccessful = function (from, spender, balance) { + let to; + + beforeEach(async function () { + const receiverContract = await ERC1363Receiver.new(RECEIVER_MAGIC_VALUE, RevertType.None); + to = receiverContract.address; + }); + + describe('when the sender does not have enough balance', function () { + const value = balance + 1; + + describe('with data', function () { + it('reverts', async function () { + await expectRevertCustomError( + transferFromAndCallWithData.call(this, from, to, value, { from: spender }), + 'ERC20InsufficientAllowance', + [spender, balance, value], + ); + }); + }); + + describe('without data', function () { + it('reverts', async function () { + await expectRevertCustomError( + transferFromAndCallWithoutData.call(this, from, to, value, { from: spender }), + 'ERC20InsufficientAllowance', + [spender, balance, value], + ); + }); + }); + }); + + describe('when the sender has enough balance', function () { + const value = balance; + + describe('with data', function () { + it('transfers the requested amount', async function () { + await transferFromAndCallWithData.call(this, from, to, value, { from: spender }); + + expect(await this.token.balanceOf(from)).to.be.bignumber.equal(''); + + expect(await this.token.balanceOf(to)).to.be.bignumber.equal(value); + }); + + it('emits a transfer event', async function () { + expectEvent( + await transferFromAndCallWithData.call(this, from, to, value, { from: spender }), + 'Transfer', + { from, to, value }, + ); + }); + }); + + describe('without data', function () { + it('transfers the requested amount', async function () { + await transferFromAndCallWithoutData.call(this, from, to, value, { from: spender }); + + expect(await this.token.balanceOf(from)).to.be.bignumber.equal('0'); + + expect(await this.token.balanceOf(to)).to.be.bignumber.equal(value); + }); + + it('emits a transfer event', async function () { + expectEvent( + await transferFromAndCallWithoutData.call(this, from, to, value, { from: spender }), + 'Transfer', + { from, to, value }, + ); + }); + }); + }); + }; + + describe('with data', function () { + shouldTransferFromSafely(transferFromAndCallWithData, data); + }); + + describe('without data', function () { + shouldTransferFromSafely(transferFromAndCallWithoutData, null); + }); + + describe('testing ERC20 behaviours', function () { + transferFromWasSuccessful(owner, spender, initialBalance); + }); + + describe('to a receiver that is not a contract', function () { + it('reverts', async function () { + await expectRevertCustomError( + transferFromAndCallWithoutData.call(this, owner, recipient, initialBalance, { from: spender }), + 'ERC1363EOAReceiver', + [recipient], + ); + }); + }); + + describe('to a receiver contract returning unexpected value', function () { + it('reverts', async function () { + const invalidReceiver = await ERC1363Receiver.new(data, RevertType.None); + await expectRevertCustomError( + transferFromAndCallWithoutData.call(this, owner, invalidReceiver.address, initialBalance, { + from: spender, + }), + 'ERC1363InvalidReceiver', + [invalidReceiver.address], + ); + }); + }); + + describe('to a receiver contract that reverts with message', function () { + it('reverts', async function () { + const revertingReceiver = await ERC1363Receiver.new(RECEIVER_MAGIC_VALUE, RevertType.RevertWithMessage); + await expectRevert( + transferFromAndCallWithoutData.call(this, owner, revertingReceiver.address, initialBalance, { + from: spender, + }), + 'ERC1363ReceiverMock: reverting', + ); + }); + }); + + describe('to a receiver contract that reverts without message', function () { + it('reverts', async function () { + const revertingReceiver = await ERC1363Receiver.new(RECEIVER_MAGIC_VALUE, RevertType.RevertWithoutMessage); + await expectRevertCustomError( + transferFromAndCallWithoutData.call(this, owner, revertingReceiver.address, initialBalance, { + from: spender, + }), + 'ERC1363InvalidReceiver', + [revertingReceiver.address], + ); + }); + }); + + describe('to a receiver contract that reverts with custom error', function () { + it('reverts', async function () { + const revertingReceiver = await ERC1363Receiver.new(RECEIVER_MAGIC_VALUE, RevertType.RevertWithCustomError); + await expectRevertCustomError( + transferFromAndCallWithoutData.call(this, owner, revertingReceiver.address, initialBalance, { + from: spender, + }), + 'CustomError', + [RECEIVER_MAGIC_VALUE], + ); + }); + }); + + describe('to a receiver contract that panics', function () { + it('reverts', async function () { + const revertingReceiver = await ERC1363Receiver.new(RECEIVER_MAGIC_VALUE, RevertType.Panic); + await expectRevert.unspecified( + transferFromAndCallWithoutData.call(this, owner, revertingReceiver.address, initialBalance, { + from: spender, + }), + ); + }); + }); + + describe('to a contract that does not implement the required function', function () { + it('reverts', async function () { + const nonReceiver = this.token; + await expectRevertCustomError( + transferFromAndCallWithoutData.call(this, owner, nonReceiver.address, initialBalance, { from: spender }), + 'ERC1363InvalidReceiver', + [nonReceiver.address], + ); + }); + }); + }); + }); + + describe('approvals', function () { + const value = initialSupply; + const data = '0x42'; + + describe('via approveAndCall', function () { + const approveAndCallWithData = function (spender, value, opts) { + return this.token.methods['approveAndCall(address,uint256,bytes)'](spender, value, data, opts); + }; + + const approveAndCallWithoutData = function (spender, value, opts) { + return this.token.methods['approveAndCall(address,uint256)'](spender, value, opts); + }; + + const shouldApproveSafely = function (approveFunction, data) { + describe('to a valid receiver contract', function () { + beforeEach(async function () { + this.spender = await ERC1363Spender.new(SPENDER_MAGIC_VALUE, RevertType.None); + this.to = this.spender.address; + }); + + it('calls onApprovalReceived', async function () { + const receipt = await approveFunction.call(this, this.to, value, { from: owner }); + + await expectEvent.inTransaction(receipt.tx, ERC1363Spender, 'Approved', { + owner, + value, + data, + }); + }); + }); + }; + + const approveWasSuccessful = function (owner, value) { + let spender; + + beforeEach(async function () { + const spenderContract = await ERC1363Spender.new(SPENDER_MAGIC_VALUE, RevertType.None); + spender = spenderContract.address; + }); + + describe('with data', function () { + it('approves the requested amount', async function () { + await approveAndCallWithData.call(this, spender, value, { from: owner }); + + expect(await this.token.allowance(owner, spender)).to.be.bignumber.equal(value); + }); + + it('emits an approval event', async function () { + expectEvent(await approveAndCallWithData.call(this, spender, value, { from: owner }), 'Approval', { + owner, + spender, + value, + }); + }); + }); + + describe('without data', function () { + it('approves the requested amount', async function () { + await approveAndCallWithoutData.call(this, spender, value, { from: owner }); + + expect(await this.token.allowance(owner, spender)).to.be.bignumber.equal(value); + }); + + it('emits an approval event', async function () { + expectEvent(await approveAndCallWithoutData.call(this, spender, value, { from: owner }), 'Approval', { + owner, + spender, + value, + }); + }); + }); + }; + + describe('with data', function () { + shouldApproveSafely(approveAndCallWithData, data); + }); + + describe('without data', function () { + shouldApproveSafely(approveAndCallWithoutData, null); + }); + + describe('testing ERC20 behaviours', function () { + approveWasSuccessful(owner, value); + }); + + describe('to a spender that is not a contract', function () { + it('reverts', async function () { + await expectRevertCustomError( + approveAndCallWithoutData.call(this, recipient, value, { from: owner }), + 'ERC1363EOASpender', + [recipient], + ); + }); + }); + + describe('to a spender contract returning unexpected value', function () { + it('reverts', async function () { + const invalidSpender = await ERC1363Spender.new(data, RevertType.None); + await expectRevertCustomError( + approveAndCallWithoutData.call(this, invalidSpender.address, value, { from: owner }), + 'ERC1363InvalidSpender', + [invalidSpender.address], + ); + }); + }); + + describe('to a spender contract that reverts with message', function () { + it('reverts', async function () { + const revertingSpender = await ERC1363Spender.new(SPENDER_MAGIC_VALUE, RevertType.RevertWithMessage); + await expectRevert( + approveAndCallWithoutData.call(this, revertingSpender.address, value, { from: owner }), + 'ERC1363SpenderMock: reverting', + ); + }); + }); + + describe('to a spender contract that reverts without message', function () { + it('reverts', async function () { + const revertingSpender = await ERC1363Spender.new(SPENDER_MAGIC_VALUE, RevertType.RevertWithoutMessage); + await expectRevertCustomError( + approveAndCallWithoutData.call(this, revertingSpender.address, value, { from: owner }), + 'ERC1363InvalidSpender', + [revertingSpender.address], + ); + }); + }); + + describe('to a spender contract that reverts with custom error', function () { + it('reverts', async function () { + const revertingSpender = await ERC1363Spender.new(SPENDER_MAGIC_VALUE, RevertType.RevertWithCustomError); + await expectRevertCustomError( + approveAndCallWithoutData.call(this, revertingSpender.address, value, { from: owner }), + 'CustomError', + [SPENDER_MAGIC_VALUE], + ); + }); + }); + + describe('to a spender contract that panics', function () { + it('reverts', async function () { + const revertingSpender = await ERC1363Spender.new(SPENDER_MAGIC_VALUE, RevertType.Panic); + await expectRevert.unspecified( + approveAndCallWithoutData.call(this, revertingSpender.address, value, { from: owner }), + ); + }); + }); + + describe('to a contract that does not implement the required function', function () { + it('reverts', async function () { + const nonSpender = this.token; + await expectRevertCustomError( + approveAndCallWithoutData.call(this, nonSpender.address, value, { from: owner }), + 'ERC1363InvalidSpender', + [nonSpender.address], + ); + }); + }); + }); + }); +} + +module.exports = { + shouldBehaveLikeERC1363, +}; diff --git a/test/token/ERC20/extensions/ERC1363.test.js b/test/token/ERC20/extensions/ERC1363.test.js new file mode 100644 index 00000000000..9fe67015e56 --- /dev/null +++ b/test/token/ERC20/extensions/ERC1363.test.js @@ -0,0 +1,17 @@ +const { BN } = require('@openzeppelin/test-helpers'); + +const { shouldBehaveLikeERC1363 } = require('./ERC1363.behaviour'); + +const ERC1363 = artifacts.require('$ERC1363'); + +contract('ERC1363', function (accounts) { + const name = 'My Token'; + const symbol = 'MTKN'; + const initialSupply = new BN(100); + + beforeEach(async function () { + this.token = await ERC1363.new(name, symbol); + }); + + shouldBehaveLikeERC1363(initialSupply, accounts); +}); diff --git a/test/utils/introspection/SupportsInterface.behavior.js b/test/utils/introspection/SupportsInterface.behavior.js index 7ef2c533f86..b18a30a3465 100644 --- a/test/utils/introspection/SupportsInterface.behavior.js +++ b/test/utils/introspection/SupportsInterface.behavior.js @@ -30,6 +30,16 @@ const INTERFACES = { 'onERC1155Received(address,address,uint256,uint256,bytes)', 'onERC1155BatchReceived(address,address,uint256[],uint256[],bytes)', ], + ERC1363: [ + 'transferAndCall(address,uint256)', + 'transferAndCall(address,uint256,bytes)', + 'transferFromAndCall(address,address,uint256)', + 'transferFromAndCall(address,address,uint256,bytes)', + 'approveAndCall(address,uint256)', + 'approveAndCall(address,uint256,bytes)', + ], + ERC1363Receiver: ['onTransferReceived(address,address,uint256,bytes)'], + ERC1363Spender: ['onApprovalReceived(address,uint256,bytes)'], AccessControl: [ 'hasRole(bytes32,address)', 'getRoleAdmin(bytes32)', From 4cbb703713f51c6c1485b9489b932f1f5c2bf34a Mon Sep 17 00:00:00 2001 From: Vittorio Minacori Date: Wed, 27 Sep 2023 16:29:24 +0200 Subject: [PATCH 03/82] Add changeset --- .changeset/friendly-nails-push.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/friendly-nails-push.md diff --git a/.changeset/friendly-nails-push.md b/.changeset/friendly-nails-push.md new file mode 100644 index 00000000000..e2e63efe8c1 --- /dev/null +++ b/.changeset/friendly-nails-push.md @@ -0,0 +1,5 @@ +--- +'openzeppelin-solidity': minor +--- + +`ERC1363`: add `ERC1363` implementation other than `IERC1363Errors` and tests. From fdd322b93e93c6b17e37f4770deb1a63fba0b154 Mon Sep 17 00:00:00 2001 From: Vittorio Minacori Date: Wed, 27 Sep 2023 20:18:07 +0200 Subject: [PATCH 04/82] Improve documentation --- contracts/interfaces/IERC1363.sol | 4 ++-- contracts/interfaces/README.adoc | 3 +++ contracts/token/ERC20/README.adoc | 3 +++ 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/contracts/interfaces/IERC1363.sol b/contracts/interfaces/IERC1363.sol index 39a2ba85b47..01b22aee4c3 100644 --- a/contracts/interfaces/IERC1363.sol +++ b/contracts/interfaces/IERC1363.sol @@ -11,8 +11,8 @@ import {IERC165} from "./IERC165.sol"; * @dev Interface of the ERC1363 standard as defined in the * https://eips.ethereum.org/EIPS/eip-1363[EIP-1363]. * - * Defines a interface for ERC20 tokens that supports executing recipient - * code after `transfer` or `transferFrom`, or spender code after `approve`. + * Defines an extension interface for ERC20 tokens that supports executing code on a recipient contract + * after `transfer` or `transferFrom`, or code on a spender contract after `approve`, in a single transaction. */ interface IERC1363 is IERC20, IERC165 { /* diff --git a/contracts/interfaces/README.adoc b/contracts/interfaces/README.adoc index 379a24a1e26..a5e1bdee07f 100644 --- a/contracts/interfaces/README.adoc +++ b/contracts/interfaces/README.adoc @@ -27,6 +27,7 @@ are useful to interact with third party contracts that implement them. - {IERC1363} - {IERC1363Receiver} - {IERC1363Spender} +- {IERC1363Errors} - {IERC1820Implementer} - {IERC1820Registry} - {IERC1822Proxiable} @@ -57,6 +58,8 @@ are useful to interact with third party contracts that implement them. {{IERC1363Spender}} +{{IERC1363Errors}} + {{IERC1820Implementer}} {{IERC1820Registry}} diff --git a/contracts/token/ERC20/README.adoc b/contracts/token/ERC20/README.adoc index 2c508802dad..e7fde62db7a 100644 --- a/contracts/token/ERC20/README.adoc +++ b/contracts/token/ERC20/README.adoc @@ -22,6 +22,7 @@ Additionally there are multiple custom extensions, including: * {ERC20FlashMint}: token level support for flash loans through the minting and burning of ephemeral tokens (standardized as ERC3156). * {ERC20Votes}: support for voting and vote delegation. * {ERC20Wrapper}: wrapper to create an ERC20 backed by another ERC20, with deposit and withdraw methods. Useful in conjunction with {ERC20Votes}. +* {ERC1363}: implementation of the ERC1363 interface that supports executing code on a recipient contract after transfers, or code on a spender contract after approvals, in a single transaction. * {ERC4626}: tokenized vault that manages shares (represented as ERC20) that are backed by assets (another ERC20). Finally, there are some utilities to interact with ERC20 contracts in various ways: @@ -60,6 +61,8 @@ NOTE: This core set of contracts is designed to be unopinionated, allowing devel {{ERC20FlashMint}} +{{ERC1363}} + {{ERC4626}} == Utilities From f8bd0036275a8e7e6b2ba7862a35748960bdf67f Mon Sep 17 00:00:00 2001 From: Vittorio Minacori Date: Wed, 27 Sep 2023 20:28:37 +0200 Subject: [PATCH 05/82] Add ERC1363Holder --- contracts/token/ERC20/README.adoc | 3 ++ contracts/token/ERC20/utils/ERC1363Holder.sol | 32 +++++++++++++ test/token/ERC20/utils/ERC1363Holder.test.js | 48 +++++++++++++++++++ 3 files changed, 83 insertions(+) create mode 100644 contracts/token/ERC20/utils/ERC1363Holder.sol create mode 100644 test/token/ERC20/utils/ERC1363Holder.test.js diff --git a/contracts/token/ERC20/README.adoc b/contracts/token/ERC20/README.adoc index e7fde62db7a..10a95498614 100644 --- a/contracts/token/ERC20/README.adoc +++ b/contracts/token/ERC20/README.adoc @@ -28,6 +28,7 @@ Additionally there are multiple custom extensions, including: Finally, there are some utilities to interact with ERC20 contracts in various ways: * {SafeERC20}: a wrapper around the interface that eliminates the need to handle boolean return values. +* {ERC1363Holder}: implementation of `IERC1363Receiver` and `IERC1363Spender` that will allow a contract to receive ERC1363 token transfers or approval. Other utilities that support ERC20 assets can be found in codebase: @@ -68,3 +69,5 @@ NOTE: This core set of contracts is designed to be unopinionated, allowing devel == Utilities {{SafeERC20}} + +{{ERC1363Holder}} diff --git a/contracts/token/ERC20/utils/ERC1363Holder.sol b/contracts/token/ERC20/utils/ERC1363Holder.sol new file mode 100644 index 00000000000..b7dde852c4e --- /dev/null +++ b/contracts/token/ERC20/utils/ERC1363Holder.sol @@ -0,0 +1,32 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.0; + +import {IERC1363Receiver} from "../../../interfaces/IERC1363Receiver.sol"; +import {IERC1363Spender} from "../../../interfaces/IERC1363Spender.sol"; + +/** + * @title ERC1363Holder + * @dev Implementation of `IERC1363Receiver` and `IERC1363Spender` that will allow a contract to receive ERC1363 token + * transfers or approval. + * + * IMPORTANT: When inheriting this contract, you must include a way to use the received tokens or spend the allowance, + * otherwise they will be stuck. + */ +abstract contract ERC1363Holder is IERC1363Receiver, IERC1363Spender { + /* + * NOTE: always returns `IERC1363Receiver.onTransferReceived.selector`. + * @inheritdoc IERC1363Receiver + */ + function onTransferReceived(address, address, uint256, bytes calldata) public virtual override returns (bytes4) { + return this.onTransferReceived.selector; + } + + /* + * NOTE: always returns `IERC1363Spender.onApprovalReceived.selector`. + * @inheritdoc IERC1363Spender + */ + function onApprovalReceived(address, uint256, bytes calldata) public virtual override returns (bytes4) { + return this.onApprovalReceived.selector; + } +} diff --git a/test/token/ERC20/utils/ERC1363Holder.test.js b/test/token/ERC20/utils/ERC1363Holder.test.js new file mode 100644 index 00000000000..7020f2863d6 --- /dev/null +++ b/test/token/ERC20/utils/ERC1363Holder.test.js @@ -0,0 +1,48 @@ +const { BN } = require('@openzeppelin/test-helpers'); +const { expect } = require('chai'); + +const ERC1363Holder = artifacts.require('$ERC1363Holder'); +const ERC1363 = artifacts.require('$ERC1363'); + +contract('ERC1363Holder', function (accounts) { + const [owner, spender] = accounts; + + const name = 'My Token'; + const symbol = 'MTKN'; + const balance = new BN(100); + + beforeEach(async function () { + this.token = await ERC1363.new(name, symbol); + this.receiver = await ERC1363Holder.new(); + + await this.token.$_mint(owner, balance); + }); + + describe('receives ERC1363 token transfers', function () { + it('via transferAndCall', async function () { + await this.token.methods['transferAndCall(address,uint256)'](this.receiver.address, balance, { from: owner }); + + expect(await this.token.balanceOf(owner)).to.be.bignumber.equal('0'); + expect(await this.token.balanceOf(this.receiver.address)).to.be.bignumber.equal(balance); + }); + + it('via transferFromAndCall', async function () { + await this.token.approve(spender, balance, { from: owner }); + + await this.token.methods['transferFromAndCall(address,address,uint256)'](owner, this.receiver.address, balance, { + from: spender, + }); + + expect(await this.token.balanceOf(owner)).to.be.bignumber.equal('0'); + expect(await this.token.balanceOf(this.receiver.address)).to.be.bignumber.equal(balance); + }); + }); + + describe('receives ERC1363 token approvals', function () { + it('via approveAndCall', async function () { + await this.token.methods['approveAndCall(address,uint256)'](this.receiver.address, balance, { from: owner }); + + expect(await this.token.allowance(owner, this.receiver.address)).to.be.bignumber.equal(balance); + }); + }); +}); From 381387ce05c6214a2941272ab4548f1ea4985314 Mon Sep 17 00:00:00 2001 From: Vittorio Minacori Date: Wed, 27 Sep 2023 20:30:02 +0200 Subject: [PATCH 06/82] Update changeset --- .changeset/friendly-nails-push.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/friendly-nails-push.md b/.changeset/friendly-nails-push.md index e2e63efe8c1..581b17883b9 100644 --- a/.changeset/friendly-nails-push.md +++ b/.changeset/friendly-nails-push.md @@ -2,4 +2,4 @@ 'openzeppelin-solidity': minor --- -`ERC1363`: add `ERC1363` implementation other than `IERC1363Errors` and tests. +`ERC1363`: add `ERC1363` implementation other than `IERC1363Errors`, `ERC1363Holder` and tests. From a07c77c4c80df4fbdcc3e6fd844e7260e24bc8a1 Mon Sep 17 00:00:00 2001 From: Vittorio Minacori Date: Sat, 30 Sep 2023 00:57:30 +0200 Subject: [PATCH 07/82] Move mint out of test behavior and rename const to avoid confusion --- .../ERC20/extensions/ERC1363.behaviour.js | 30 +++++++++---------- test/token/ERC20/extensions/ERC1363.test.js | 3 ++ 2 files changed, 17 insertions(+), 16 deletions(-) diff --git a/test/token/ERC20/extensions/ERC1363.behaviour.js b/test/token/ERC20/extensions/ERC1363.behaviour.js index 963e1c6f048..04f8a15bdb0 100644 --- a/test/token/ERC20/extensions/ERC1363.behaviour.js +++ b/test/token/ERC20/extensions/ERC1363.behaviour.js @@ -16,10 +16,6 @@ function shouldBehaveLikeERC1363(initialSupply, accounts) { const RECEIVER_MAGIC_VALUE = '0x88a7ca5c'; const SPENDER_MAGIC_VALUE = '0x7b04a2d0'; - beforeEach(async function () { - await this.token.$_mint(owner, initialSupply); - }); - shouldSupportInterfaces(['ERC165', 'ERC1363']); describe('transfers', function () { @@ -418,7 +414,7 @@ function shouldBehaveLikeERC1363(initialSupply, accounts) { }); describe('approvals', function () { - const value = initialSupply; + const initialBalance = initialSupply; const data = '0x42'; describe('via approveAndCall', function () { @@ -438,18 +434,20 @@ function shouldBehaveLikeERC1363(initialSupply, accounts) { }); it('calls onApprovalReceived', async function () { - const receipt = await approveFunction.call(this, this.to, value, { from: owner }); + const receipt = await approveFunction.call(this, this.to, initialBalance, { from: owner }); await expectEvent.inTransaction(receipt.tx, ERC1363Spender, 'Approved', { owner, - value, + value: initialBalance, data, }); }); }); }; - const approveWasSuccessful = function (owner, value) { + const approveWasSuccessful = function (owner, balance) { + const value = balance; + let spender; beforeEach(async function () { @@ -499,13 +497,13 @@ function shouldBehaveLikeERC1363(initialSupply, accounts) { }); describe('testing ERC20 behaviours', function () { - approveWasSuccessful(owner, value); + approveWasSuccessful(owner, initialBalance); }); describe('to a spender that is not a contract', function () { it('reverts', async function () { await expectRevertCustomError( - approveAndCallWithoutData.call(this, recipient, value, { from: owner }), + approveAndCallWithoutData.call(this, recipient, initialBalance, { from: owner }), 'ERC1363EOASpender', [recipient], ); @@ -516,7 +514,7 @@ function shouldBehaveLikeERC1363(initialSupply, accounts) { it('reverts', async function () { const invalidSpender = await ERC1363Spender.new(data, RevertType.None); await expectRevertCustomError( - approveAndCallWithoutData.call(this, invalidSpender.address, value, { from: owner }), + approveAndCallWithoutData.call(this, invalidSpender.address, initialBalance, { from: owner }), 'ERC1363InvalidSpender', [invalidSpender.address], ); @@ -527,7 +525,7 @@ function shouldBehaveLikeERC1363(initialSupply, accounts) { it('reverts', async function () { const revertingSpender = await ERC1363Spender.new(SPENDER_MAGIC_VALUE, RevertType.RevertWithMessage); await expectRevert( - approveAndCallWithoutData.call(this, revertingSpender.address, value, { from: owner }), + approveAndCallWithoutData.call(this, revertingSpender.address, initialBalance, { from: owner }), 'ERC1363SpenderMock: reverting', ); }); @@ -537,7 +535,7 @@ function shouldBehaveLikeERC1363(initialSupply, accounts) { it('reverts', async function () { const revertingSpender = await ERC1363Spender.new(SPENDER_MAGIC_VALUE, RevertType.RevertWithoutMessage); await expectRevertCustomError( - approveAndCallWithoutData.call(this, revertingSpender.address, value, { from: owner }), + approveAndCallWithoutData.call(this, revertingSpender.address, initialBalance, { from: owner }), 'ERC1363InvalidSpender', [revertingSpender.address], ); @@ -548,7 +546,7 @@ function shouldBehaveLikeERC1363(initialSupply, accounts) { it('reverts', async function () { const revertingSpender = await ERC1363Spender.new(SPENDER_MAGIC_VALUE, RevertType.RevertWithCustomError); await expectRevertCustomError( - approveAndCallWithoutData.call(this, revertingSpender.address, value, { from: owner }), + approveAndCallWithoutData.call(this, revertingSpender.address, initialBalance, { from: owner }), 'CustomError', [SPENDER_MAGIC_VALUE], ); @@ -559,7 +557,7 @@ function shouldBehaveLikeERC1363(initialSupply, accounts) { it('reverts', async function () { const revertingSpender = await ERC1363Spender.new(SPENDER_MAGIC_VALUE, RevertType.Panic); await expectRevert.unspecified( - approveAndCallWithoutData.call(this, revertingSpender.address, value, { from: owner }), + approveAndCallWithoutData.call(this, revertingSpender.address, initialBalance, { from: owner }), ); }); }); @@ -568,7 +566,7 @@ function shouldBehaveLikeERC1363(initialSupply, accounts) { it('reverts', async function () { const nonSpender = this.token; await expectRevertCustomError( - approveAndCallWithoutData.call(this, nonSpender.address, value, { from: owner }), + approveAndCallWithoutData.call(this, nonSpender.address, initialBalance, { from: owner }), 'ERC1363InvalidSpender', [nonSpender.address], ); diff --git a/test/token/ERC20/extensions/ERC1363.test.js b/test/token/ERC20/extensions/ERC1363.test.js index 9fe67015e56..54e6171e57b 100644 --- a/test/token/ERC20/extensions/ERC1363.test.js +++ b/test/token/ERC20/extensions/ERC1363.test.js @@ -5,12 +5,15 @@ const { shouldBehaveLikeERC1363 } = require('./ERC1363.behaviour'); const ERC1363 = artifacts.require('$ERC1363'); contract('ERC1363', function (accounts) { + const [owner] = accounts; + const name = 'My Token'; const symbol = 'MTKN'; const initialSupply = new BN(100); beforeEach(async function () { this.token = await ERC1363.new(name, symbol); + await this.token.$_mint(owner, initialSupply); }); shouldBehaveLikeERC1363(initialSupply, accounts); From cf3c377f8a523da0f1949b0ccc4c5b5d3b9d7f44 Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Thu, 5 Oct 2023 13:20:10 +0200 Subject: [PATCH 08/82] migrate Ownable tests to ethers --- hardhat.config.js | 4 +- package-lock.json | 1086 +++++++++++++++++++++++++++++++++-- package.json | 6 +- test/access/Ownable.test.js | 82 +-- test/helpers/deploy.js | 20 + 5 files changed, 1110 insertions(+), 88 deletions(-) create mode 100644 test/helpers/deploy.js diff --git a/hardhat.config.js b/hardhat.config.js index 4d5ab944ada..9a7667a9f06 100644 --- a/hardhat.config.js +++ b/hardhat.config.js @@ -55,7 +55,9 @@ const argv = require('yargs/yargs')() }, }).argv; -require('@nomiclabs/hardhat-truffle5'); +// require('@nomiclabs/hardhat-truffle5'); +require('@nomicfoundation/hardhat-toolbox'); +require('@nomicfoundation/hardhat-ethers'); require('hardhat-ignore-warnings'); require('hardhat-exposed'); require('solidity-docgen'); diff --git a/package-lock.json b/package-lock.json index 0f4f9f55e4e..11586105968 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,8 +13,11 @@ "@changesets/cli": "^2.26.0", "@changesets/pre": "^1.0.14", "@changesets/read": "^0.5.9", + "@nomicfoundation/hardhat-chai-matchers": "^2.0.2", + "@nomicfoundation/hardhat-ethers": "^3.0.4", "@nomicfoundation/hardhat-foundry": "^1.1.1", "@nomicfoundation/hardhat-network-helpers": "^1.0.3", + "@nomicfoundation/hardhat-toolbox": "^3.0.0", "@nomiclabs/hardhat-truffle5": "^2.0.5", "@nomiclabs/hardhat-web3": "^2.0.0", "@openzeppelin/docs-utils": "^0.1.5", @@ -28,9 +31,10 @@ "eth-sig-util": "^3.0.0", "ethereumjs-util": "^7.0.7", "ethereumjs-wallet": "^1.0.1", + "ethers": "^6.7.1", "glob": "^10.3.5", "graphlib": "^2.1.8", - "hardhat": "^2.9.1", + "hardhat": "^2.17.4", "hardhat-exposed": "^0.3.13", "hardhat-gas-reporter": "^1.0.9", "hardhat-ignore-warnings": "^0.2.0", @@ -63,6 +67,12 @@ "node": ">=0.10.0" } }, + "node_modules/@adraffy/ens-normalize": { + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/@adraffy/ens-normalize/-/ens-normalize-1.9.2.tgz", + "integrity": "sha512-0h+FrQDqe2Wn+IIGFkTCd4aAwTJ+7834Ek1COohCyV26AXhwQ7WQaz+4F/nLOeVl/3BtWHOHLPsq46V8YB46Eg==", + "dev": true + }, "node_modules/@babel/code-frame": { "version": "7.22.13", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.22.13.tgz", @@ -431,6 +441,19 @@ "url": "https://github.com/prettier/prettier?sponsor=1" } }, + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "dev": true, + "peer": true, + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/@ensdomains/address-encoder": { "version": "0.1.9", "resolved": "https://registry.npmjs.org/@ensdomains/address-encoder/-/address-encoder-0.1.9.tgz", @@ -1542,6 +1565,34 @@ "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.1.tgz", + "integrity": "sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA==", + "dev": true, + "peer": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.4.15", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", + "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==", + "dev": true, + "peer": true + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dev": true, + "peer": true, + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, "node_modules/@manypkg/find-root": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@manypkg/find-root/-/find-root-1.1.0.tgz", @@ -2013,6 +2064,38 @@ "node": ">=14" } }, + "node_modules/@nomicfoundation/hardhat-chai-matchers": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@nomicfoundation/hardhat-chai-matchers/-/hardhat-chai-matchers-2.0.2.tgz", + "integrity": "sha512-9Wu9mRtkj0U9ohgXYFbB/RQDa+PcEdyBm2suyEtsJf3PqzZEEjLUZgWnMjlFhATMk/fp3BjmnYVPrwl+gr8oEw==", + "dev": true, + "dependencies": { + "@types/chai-as-promised": "^7.1.3", + "chai-as-promised": "^7.1.1", + "deep-eql": "^4.0.1", + "ordinal": "^1.0.3" + }, + "peerDependencies": { + "@nomicfoundation/hardhat-ethers": "^3.0.0", + "chai": "^4.2.0", + "ethers": "^6.1.0", + "hardhat": "^2.9.4" + } + }, + "node_modules/@nomicfoundation/hardhat-ethers": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@nomicfoundation/hardhat-ethers/-/hardhat-ethers-3.0.4.tgz", + "integrity": "sha512-k9qbLoY7qn6C6Y1LI0gk2kyHXil2Tauj4kGzQ8pgxYXIGw8lWn8tuuL72E11CrlKaXRUvOgF0EXrv/msPI2SbA==", + "dev": true, + "dependencies": { + "debug": "^4.1.1", + "lodash.isequal": "^4.5.0" + }, + "peerDependencies": { + "ethers": "^6.1.0", + "hardhat": "^2.0.0" + } + }, "node_modules/@nomicfoundation/hardhat-foundry": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@nomicfoundation/hardhat-foundry/-/hardhat-foundry-1.1.1.tgz", @@ -2037,6 +2120,75 @@ "hardhat": "^2.9.5" } }, + "node_modules/@nomicfoundation/hardhat-toolbox": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@nomicfoundation/hardhat-toolbox/-/hardhat-toolbox-3.0.0.tgz", + "integrity": "sha512-MsteDXd0UagMksqm9KvcFG6gNKYNa3GGNCy73iQ6bEasEgg2v8Qjl6XA5hjs8o5UD5A3153B6W2BIVJ8SxYUtA==", + "dev": true, + "peerDependencies": { + "@nomicfoundation/hardhat-chai-matchers": "^2.0.0", + "@nomicfoundation/hardhat-ethers": "^3.0.0", + "@nomicfoundation/hardhat-network-helpers": "^1.0.0", + "@nomicfoundation/hardhat-verify": "^1.0.0", + "@typechain/ethers-v6": "^0.4.0", + "@typechain/hardhat": "^8.0.0", + "@types/chai": "^4.2.0", + "@types/mocha": ">=9.1.0", + "@types/node": ">=12.0.0", + "chai": "^4.2.0", + "ethers": "^6.4.0", + "hardhat": "^2.11.0", + "hardhat-gas-reporter": "^1.0.8", + "solidity-coverage": "^0.8.1", + "ts-node": ">=8.0.0", + "typechain": "^8.2.0", + "typescript": ">=4.5.0" + } + }, + "node_modules/@nomicfoundation/hardhat-verify": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@nomicfoundation/hardhat-verify/-/hardhat-verify-1.1.1.tgz", + "integrity": "sha512-9QsTYD7pcZaQFEA3tBb/D/oCStYDiEVDN7Dxeo/4SCyHRSm86APypxxdOMEPlGmXsAvd+p1j/dTODcpxb8aztA==", + "dev": true, + "peer": true, + "dependencies": { + "@ethersproject/abi": "^5.1.2", + "@ethersproject/address": "^5.0.2", + "cbor": "^8.1.0", + "chalk": "^2.4.2", + "debug": "^4.1.1", + "lodash.clonedeep": "^4.5.0", + "semver": "^6.3.0", + "table": "^6.8.0", + "undici": "^5.14.0" + }, + "peerDependencies": { + "hardhat": "^2.0.4" + } + }, + "node_modules/@nomicfoundation/hardhat-verify/node_modules/cbor": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/cbor/-/cbor-8.1.0.tgz", + "integrity": "sha512-DwGjNW9omn6EwP70aXsn7FQJx5kO12tX0bZkaTjzdVFM6/7nhA4t0EENocKGx6D2Bch9PE2KzCUf5SceBdeijg==", + "dev": true, + "peer": true, + "dependencies": { + "nofilter": "^3.1.0" + }, + "engines": { + "node": ">=12.19" + } + }, + "node_modules/@nomicfoundation/hardhat-verify/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "peer": true, + "bin": { + "semver": "bin/semver.js" + } + }, "node_modules/@nomicfoundation/solidity-analyzer": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/@nomicfoundation/solidity-analyzer/-/solidity-analyzer-0.1.1.tgz", @@ -2274,6 +2426,64 @@ "web3-utils": "^1.2.1" } }, + "node_modules/@nomiclabs/truffle-contract/node_modules/aes-js": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/aes-js/-/aes-js-3.0.0.tgz", + "integrity": "sha512-H7wUZRn8WpTq9jocdxQ2c8x2sKo9ZVmzfRE13GiNJXfp7NcKYEdvl3vspKjXox6RIG2VtaRe4JFvxG4rqp2Zuw==", + "dev": true + }, + "node_modules/@nomiclabs/truffle-contract/node_modules/ethers": { + "version": "4.0.49", + "resolved": "https://registry.npmjs.org/ethers/-/ethers-4.0.49.tgz", + "integrity": "sha512-kPltTvWiyu+OktYy1IStSO16i2e7cS9D9OxZ81q2UUaiNPVrm/RTcbxamCXF9VUSKzJIdJV68EAIhTEVBalRWg==", + "dev": true, + "dependencies": { + "aes-js": "3.0.0", + "bn.js": "^4.11.9", + "elliptic": "6.5.4", + "hash.js": "1.1.3", + "js-sha3": "0.5.7", + "scrypt-js": "2.0.4", + "setimmediate": "1.0.4", + "uuid": "2.0.1", + "xmlhttprequest": "1.8.0" + } + }, + "node_modules/@nomiclabs/truffle-contract/node_modules/hash.js": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.3.tgz", + "integrity": "sha512-/UETyP0W22QILqS+6HowevwhEFJ3MBJnwTf75Qob9Wz9t0DPuisL8kW8YZMK62dHAKE1c1p+gY1TtOLY+USEHA==", + "dev": true, + "dependencies": { + "inherits": "^2.0.3", + "minimalistic-assert": "^1.0.0" + } + }, + "node_modules/@nomiclabs/truffle-contract/node_modules/js-sha3": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/js-sha3/-/js-sha3-0.5.7.tgz", + "integrity": "sha512-GII20kjaPX0zJ8wzkTbNDYMY7msuZcTWk8S5UOh6806Jq/wz1J8/bnr8uGU0DAUmYDjj2Mr4X1cW8v/GLYnR+g==", + "dev": true + }, + "node_modules/@nomiclabs/truffle-contract/node_modules/scrypt-js": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/scrypt-js/-/scrypt-js-2.0.4.tgz", + "integrity": "sha512-4KsaGcPnuhtCZQCxFxN3GVYIhKFPTdLd8PLC552XwbMndtD0cjRFAhDuuydXQ0h08ZfPgzqe6EKHozpuH74iDw==", + "dev": true + }, + "node_modules/@nomiclabs/truffle-contract/node_modules/setimmediate": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.4.tgz", + "integrity": "sha512-/TjEmXQVEzdod/FFskf3o7oOAsGhHf2j1dZqRFbDzq4F3mvvxflIIi4Hd3bLQE9y/CpwqfSQam5JakI/mi3Pog==", + "dev": true + }, + "node_modules/@nomiclabs/truffle-contract/node_modules/uuid": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-2.0.1.tgz", + "integrity": "sha512-nWg9+Oa3qD2CQzHIP4qKUqwNfzKn8P0LtFhotaCTFchsV7ZfDhAybeip/HZVeMIpZi9JgY1E3nUlwaCmZT1sEg==", + "deprecated": "Please upgrade to version 7 or higher. Older versions may use Math.random() in certain circumstances, which is known to be problematic. See https://v8.dev/blog/math-random for details.", + "dev": true + }, "node_modules/@openzeppelin/contract-loader": { "version": "0.6.3", "resolved": "https://registry.npmjs.org/@openzeppelin/contract-loader/-/contract-loader-0.6.3.tgz", @@ -2968,6 +3178,12 @@ "node": "^16.20 || ^18.16 || >=20" } }, + "node_modules/@truffle/contract/node_modules/aes-js": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/aes-js/-/aes-js-3.0.0.tgz", + "integrity": "sha512-H7wUZRn8WpTq9jocdxQ2c8x2sKo9ZVmzfRE13GiNJXfp7NcKYEdvl3vspKjXox6RIG2VtaRe4JFvxG4rqp2Zuw==", + "dev": true + }, "node_modules/@truffle/contract/node_modules/cross-fetch": { "version": "3.1.8", "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.1.8.tgz", @@ -2988,6 +3204,58 @@ "xhr-request-promise": "^0.1.2" } }, + "node_modules/@truffle/contract/node_modules/ethers": { + "version": "4.0.49", + "resolved": "https://registry.npmjs.org/ethers/-/ethers-4.0.49.tgz", + "integrity": "sha512-kPltTvWiyu+OktYy1IStSO16i2e7cS9D9OxZ81q2UUaiNPVrm/RTcbxamCXF9VUSKzJIdJV68EAIhTEVBalRWg==", + "dev": true, + "dependencies": { + "aes-js": "3.0.0", + "bn.js": "^4.11.9", + "elliptic": "6.5.4", + "hash.js": "1.1.3", + "js-sha3": "0.5.7", + "scrypt-js": "2.0.4", + "setimmediate": "1.0.4", + "uuid": "2.0.1", + "xmlhttprequest": "1.8.0" + } + }, + "node_modules/@truffle/contract/node_modules/ethers/node_modules/scrypt-js": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/scrypt-js/-/scrypt-js-2.0.4.tgz", + "integrity": "sha512-4KsaGcPnuhtCZQCxFxN3GVYIhKFPTdLd8PLC552XwbMndtD0cjRFAhDuuydXQ0h08ZfPgzqe6EKHozpuH74iDw==", + "dev": true + }, + "node_modules/@truffle/contract/node_modules/ethers/node_modules/uuid": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-2.0.1.tgz", + "integrity": "sha512-nWg9+Oa3qD2CQzHIP4qKUqwNfzKn8P0LtFhotaCTFchsV7ZfDhAybeip/HZVeMIpZi9JgY1E3nUlwaCmZT1sEg==", + "deprecated": "Please upgrade to version 7 or higher. Older versions may use Math.random() in certain circumstances, which is known to be problematic. See https://v8.dev/blog/math-random for details.", + "dev": true + }, + "node_modules/@truffle/contract/node_modules/hash.js": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.3.tgz", + "integrity": "sha512-/UETyP0W22QILqS+6HowevwhEFJ3MBJnwTf75Qob9Wz9t0DPuisL8kW8YZMK62dHAKE1c1p+gY1TtOLY+USEHA==", + "dev": true, + "dependencies": { + "inherits": "^2.0.3", + "minimalistic-assert": "^1.0.0" + } + }, + "node_modules/@truffle/contract/node_modules/js-sha3": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/js-sha3/-/js-sha3-0.5.7.tgz", + "integrity": "sha512-GII20kjaPX0zJ8wzkTbNDYMY7msuZcTWk8S5UOh6806Jq/wz1J8/bnr8uGU0DAUmYDjj2Mr4X1cW8v/GLYnR+g==", + "dev": true + }, + "node_modules/@truffle/contract/node_modules/setimmediate": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.4.tgz", + "integrity": "sha512-/TjEmXQVEzdod/FFskf3o7oOAsGhHf2j1dZqRFbDzq4F3mvvxflIIi4Hd3bLQE9y/CpwqfSQam5JakI/mi3Pog==", + "dev": true + }, "node_modules/@truffle/contract/node_modules/uuid": { "version": "9.0.1", "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", @@ -3358,6 +3626,12 @@ "node": "^16.20 || ^18.16 || >=20" } }, + "node_modules/@truffle/interface-adapter/node_modules/aes-js": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/aes-js/-/aes-js-3.0.0.tgz", + "integrity": "sha512-H7wUZRn8WpTq9jocdxQ2c8x2sKo9ZVmzfRE13GiNJXfp7NcKYEdvl3vspKjXox6RIG2VtaRe4JFvxG4rqp2Zuw==", + "dev": true + }, "node_modules/@truffle/interface-adapter/node_modules/bignumber.js": { "version": "9.1.2", "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.1.2.tgz", @@ -3399,6 +3673,64 @@ "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==", "dev": true }, + "node_modules/@truffle/interface-adapter/node_modules/ethers": { + "version": "4.0.49", + "resolved": "https://registry.npmjs.org/ethers/-/ethers-4.0.49.tgz", + "integrity": "sha512-kPltTvWiyu+OktYy1IStSO16i2e7cS9D9OxZ81q2UUaiNPVrm/RTcbxamCXF9VUSKzJIdJV68EAIhTEVBalRWg==", + "dev": true, + "dependencies": { + "aes-js": "3.0.0", + "bn.js": "^4.11.9", + "elliptic": "6.5.4", + "hash.js": "1.1.3", + "js-sha3": "0.5.7", + "scrypt-js": "2.0.4", + "setimmediate": "1.0.4", + "uuid": "2.0.1", + "xmlhttprequest": "1.8.0" + } + }, + "node_modules/@truffle/interface-adapter/node_modules/ethers/node_modules/bn.js": { + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz", + "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==", + "dev": true + }, + "node_modules/@truffle/interface-adapter/node_modules/ethers/node_modules/scrypt-js": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/scrypt-js/-/scrypt-js-2.0.4.tgz", + "integrity": "sha512-4KsaGcPnuhtCZQCxFxN3GVYIhKFPTdLd8PLC552XwbMndtD0cjRFAhDuuydXQ0h08ZfPgzqe6EKHozpuH74iDw==", + "dev": true + }, + "node_modules/@truffle/interface-adapter/node_modules/ethers/node_modules/uuid": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-2.0.1.tgz", + "integrity": "sha512-nWg9+Oa3qD2CQzHIP4qKUqwNfzKn8P0LtFhotaCTFchsV7ZfDhAybeip/HZVeMIpZi9JgY1E3nUlwaCmZT1sEg==", + "deprecated": "Please upgrade to version 7 or higher. Older versions may use Math.random() in certain circumstances, which is known to be problematic. See https://v8.dev/blog/math-random for details.", + "dev": true + }, + "node_modules/@truffle/interface-adapter/node_modules/hash.js": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.3.tgz", + "integrity": "sha512-/UETyP0W22QILqS+6HowevwhEFJ3MBJnwTf75Qob9Wz9t0DPuisL8kW8YZMK62dHAKE1c1p+gY1TtOLY+USEHA==", + "dev": true, + "dependencies": { + "inherits": "^2.0.3", + "minimalistic-assert": "^1.0.0" + } + }, + "node_modules/@truffle/interface-adapter/node_modules/js-sha3": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/js-sha3/-/js-sha3-0.5.7.tgz", + "integrity": "sha512-GII20kjaPX0zJ8wzkTbNDYMY7msuZcTWk8S5UOh6806Jq/wz1J8/bnr8uGU0DAUmYDjj2Mr4X1cW8v/GLYnR+g==", + "dev": true + }, + "node_modules/@truffle/interface-adapter/node_modules/setimmediate": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.4.tgz", + "integrity": "sha512-/TjEmXQVEzdod/FFskf3o7oOAsGhHf2j1dZqRFbDzq4F3mvvxflIIi4Hd3bLQE9y/CpwqfSQam5JakI/mi3Pog==", + "dev": true + }, "node_modules/@truffle/interface-adapter/node_modules/uuid": { "version": "9.0.1", "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", @@ -3736,6 +4068,105 @@ "node": ">=4" } }, + "node_modules/@tsconfig/node10": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.9.tgz", + "integrity": "sha512-jNsYVVxU8v5g43Erja32laIDHXeoNvFEpX33OK4d6hljo3jDhCBDhx5dhCCTMWUojscpAagGiRkBKxpdl9fxqA==", + "dev": true, + "peer": true + }, + "node_modules/@tsconfig/node12": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", + "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", + "dev": true, + "peer": true + }, + "node_modules/@tsconfig/node14": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", + "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", + "dev": true, + "peer": true + }, + "node_modules/@tsconfig/node16": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", + "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", + "dev": true, + "peer": true + }, + "node_modules/@typechain/ethers-v6": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@typechain/ethers-v6/-/ethers-v6-0.4.3.tgz", + "integrity": "sha512-TrxBsyb4ryhaY9keP6RzhFCviWYApcLCIRMPyWaKp2cZZrfaM3QBoxXTnw/eO4+DAY3l+8O0brNW0WgeQeOiDA==", + "dev": true, + "peer": true, + "dependencies": { + "lodash": "^4.17.15", + "ts-essentials": "^7.0.1" + }, + "peerDependencies": { + "ethers": "6.x", + "typechain": "^8.3.1", + "typescript": ">=4.7.0" + } + }, + "node_modules/@typechain/hardhat": { + "version": "8.0.3", + "resolved": "https://registry.npmjs.org/@typechain/hardhat/-/hardhat-8.0.3.tgz", + "integrity": "sha512-MytSmJJn+gs7Mqrpt/gWkTCOpOQ6ZDfRrRT2gtZL0rfGe4QrU4x9ZdW15fFbVM/XTa+5EsKiOMYXhRABibNeng==", + "dev": true, + "peer": true, + "dependencies": { + "fs-extra": "^9.1.0" + }, + "peerDependencies": { + "@typechain/ethers-v6": "^0.4.3", + "ethers": "^6.1.0", + "hardhat": "^2.9.9", + "typechain": "^8.3.1" + } + }, + "node_modules/@typechain/hardhat/node_modules/fs-extra": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", + "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "dev": true, + "peer": true, + "dependencies": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@typechain/hardhat/node_modules/jsonfile": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", + "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", + "dev": true, + "peer": true, + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/@typechain/hardhat/node_modules/universalify": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz", + "integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==", + "dev": true, + "peer": true, + "engines": { + "node": ">= 10.0.0" + } + }, "node_modules/@types/bignumber.js": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/@types/bignumber.js/-/bignumber.js-5.0.0.tgz", @@ -3773,6 +4204,15 @@ "integrity": "sha512-VOVRLM1mBxIRxydiViqPcKn6MIxZytrbMpd6RJLIWKxUNr3zux8no0Oc7kJx0WAPIitgZ0gkrDS+btlqQpubpw==", "dev": true }, + "node_modules/@types/chai-as-promised": { + "version": "7.1.6", + "resolved": "https://registry.npmjs.org/@types/chai-as-promised/-/chai-as-promised-7.1.6.tgz", + "integrity": "sha512-cQLhk8fFarRVZAXUQV1xEnZgMoPxqKojBvRkqPCKPQCzEhpbbSKl1Uu75kDng7k5Ln6LQLUmNBjLlFthCgm1NA==", + "dev": true, + "dependencies": { + "@types/chai": "*" + } + }, "node_modules/@types/concat-stream": { "version": "1.6.1", "resolved": "https://registry.npmjs.org/@types/concat-stream/-/concat-stream-1.6.1.tgz", @@ -3843,6 +4283,13 @@ "integrity": "sha512-jhuKLIRrhvCPLqwPcx6INqmKeiA5EWrsCOPhrlFSrbrmU4ZMPjj5Ul/oLCMDO98XRUIwVm78xICz4EPCektzeQ==", "dev": true }, + "node_modules/@types/mocha": { + "version": "10.0.2", + "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-10.0.2.tgz", + "integrity": "sha512-NaHL0+0lLNhX6d9rs+NSt97WH/gIlRHmszXbQ/8/MV/eVcFNdeJ/GYhrFuUc8K7WuPhRhTSdMkCp8VMzhUq85w==", + "dev": true, + "peer": true + }, "node_modules/@types/node": { "version": "12.20.55", "resolved": "https://registry.npmjs.org/@types/node/-/node-12.20.55.tgz", @@ -3864,6 +4311,13 @@ "@types/node": "*" } }, + "node_modules/@types/prettier": { + "version": "2.7.3", + "resolved": "https://registry.npmjs.org/@types/prettier/-/prettier-2.7.3.tgz", + "integrity": "sha512-+68kP9yzs4LMp7VNh8gdzMSPZFL44MLGqiHWvttYJe+6qnuVr4Ek9wSBQoveqY/r+LwjCcU29kNVkidwim+kYA==", + "dev": true, + "peer": true + }, "node_modules/@types/qs": { "version": "6.9.8", "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.8.tgz", @@ -3998,6 +4452,16 @@ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, + "node_modules/acorn-walk": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.2.0.tgz", + "integrity": "sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==", + "dev": true, + "peer": true, + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/address": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/address/-/address-1.2.2.tgz", @@ -4158,6 +4622,13 @@ "node": ">= 8" } }, + "node_modules/arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "dev": true, + "peer": true + }, "node_modules/argparse": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", @@ -4167,6 +4638,16 @@ "sprintf-js": "~1.0.2" } }, + "node_modules/array-back": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/array-back/-/array-back-3.1.0.tgz", + "integrity": "sha512-TkuxA4UCOvxuDK6NZYXCalszEzj+TLszyASooky+i742l9TqsOdYCMJJupxRic61hwquNtppB3hgcuq9SVSH1Q==", + "dev": true, + "peer": true, + "engines": { + "node": ">=6" + } + }, "node_modules/array-buffer-byte-length": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.0.tgz", @@ -4352,6 +4833,16 @@ "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", "dev": true }, + "node_modules/at-least-node": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz", + "integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==", + "dev": true, + "peer": true, + "engines": { + "node": ">= 4.0.0" + } + }, "node_modules/available-typed-arrays": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz", @@ -4942,6 +5433,18 @@ "node": ">=4" } }, + "node_modules/chai-as-promised": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/chai-as-promised/-/chai-as-promised-7.1.1.tgz", + "integrity": "sha512-azL6xMoi+uxu6z4rhWQ1jbdUhOMhis2PvscD/xjLqNMkv3BPPp2JyyuTHOrf9BOosGpNQ11v6BKv/g57RXbiaA==", + "dev": true, + "dependencies": { + "check-error": "^1.0.2" + }, + "peerDependencies": { + "chai": ">= 2.1.2 < 5" + } + }, "node_modules/chai-bn": { "version": "0.2.2", "resolved": "https://registry.npmjs.org/chai-bn/-/chai-bn-0.2.2.tgz", @@ -5319,6 +5822,58 @@ "integrity": "sha512-LTQ/SGc+s0Xc0Fu5WaKnR0YiygZkm9eKFvyS+fRsU7/ZWFF8ykFM6Pc9aCVf1+xasOOZpO3BAVgVrKvsqKHV7w==", "dev": true }, + "node_modules/command-line-args": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/command-line-args/-/command-line-args-5.2.1.tgz", + "integrity": "sha512-H4UfQhZyakIjC74I9d34fGYDwk3XpSr17QhEd0Q3I9Xq1CETHo4Hcuo87WyWHpAF1aSLjLRf5lD9ZGX2qStUvg==", + "dev": true, + "peer": true, + "dependencies": { + "array-back": "^3.1.0", + "find-replace": "^3.0.0", + "lodash.camelcase": "^4.3.0", + "typical": "^4.0.0" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/command-line-usage": { + "version": "6.1.3", + "resolved": "https://registry.npmjs.org/command-line-usage/-/command-line-usage-6.1.3.tgz", + "integrity": "sha512-sH5ZSPr+7UStsloltmDh7Ce5fb8XPlHyoPzTpyyMuYCtervL65+ubVZ6Q61cFtFl62UyJlc8/JwERRbAFPUqgw==", + "dev": true, + "peer": true, + "dependencies": { + "array-back": "^4.0.2", + "chalk": "^2.4.2", + "table-layout": "^1.0.2", + "typical": "^5.2.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/command-line-usage/node_modules/array-back": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/array-back/-/array-back-4.0.2.tgz", + "integrity": "sha512-NbdMezxqf94cnNfWLL7V/im0Ub+Anbb0IoZhvzie8+4HJ4nMQuzHuy49FkGYCJK2yAloZ3meiB6AVMClbrI1vg==", + "dev": true, + "peer": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/command-line-usage/node_modules/typical": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/typical/-/typical-5.2.0.tgz", + "integrity": "sha512-dvdQgNDNJo+8B2uBQoqdb11eUCE1JQXhvjC/CZtgvZseVd5TYMXnq0+vuUemXbd/Se29cTaUuPX3YIc2xgbvIg==", + "dev": true, + "peer": true, + "engines": { + "node": ">=8" + } + }, "node_modules/commander": { "version": "10.0.1", "resolved": "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz", @@ -5550,6 +6105,13 @@ "sha.js": "^2.4.8" } }, + "node_modules/create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "dev": true, + "peer": true + }, "node_modules/cross-fetch": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-4.0.0.tgz", @@ -5797,6 +6359,16 @@ "node": ">=6" } }, + "node_modules/deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "dev": true, + "peer": true, + "engines": { + "node": ">=4.0.0" + } + }, "node_modules/deep-is": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", @@ -7097,63 +7669,84 @@ } }, "node_modules/ethers": { - "version": "4.0.49", - "resolved": "https://registry.npmjs.org/ethers/-/ethers-4.0.49.tgz", - "integrity": "sha512-kPltTvWiyu+OktYy1IStSO16i2e7cS9D9OxZ81q2UUaiNPVrm/RTcbxamCXF9VUSKzJIdJV68EAIhTEVBalRWg==", + "version": "6.7.1", + "resolved": "https://registry.npmjs.org/ethers/-/ethers-6.7.1.tgz", + "integrity": "sha512-qX5kxIFMfg1i+epfgb0xF4WM7IqapIIu50pOJ17aebkxxa4BacW5jFrQRmCJpDEg2ZK2oNtR5QjrQ1WDBF29dA==", "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/ethers-io/" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], "dependencies": { - "aes-js": "3.0.0", - "bn.js": "^4.11.9", - "elliptic": "6.5.4", - "hash.js": "1.1.3", - "js-sha3": "0.5.7", - "scrypt-js": "2.0.4", - "setimmediate": "1.0.4", - "uuid": "2.0.1", - "xmlhttprequest": "1.8.0" + "@adraffy/ens-normalize": "1.9.2", + "@noble/hashes": "1.1.2", + "@noble/secp256k1": "1.7.1", + "@types/node": "18.15.13", + "aes-js": "4.0.0-beta.5", + "tslib": "2.4.0", + "ws": "8.5.0" + }, + "engines": { + "node": ">=14.0.0" } }, - "node_modules/ethers/node_modules/aes-js": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/aes-js/-/aes-js-3.0.0.tgz", - "integrity": "sha512-H7wUZRn8WpTq9jocdxQ2c8x2sKo9ZVmzfRE13GiNJXfp7NcKYEdvl3vspKjXox6RIG2VtaRe4JFvxG4rqp2Zuw==", - "dev": true - }, - "node_modules/ethers/node_modules/hash.js": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.3.tgz", - "integrity": "sha512-/UETyP0W22QILqS+6HowevwhEFJ3MBJnwTf75Qob9Wz9t0DPuisL8kW8YZMK62dHAKE1c1p+gY1TtOLY+USEHA==", + "node_modules/ethers/node_modules/@noble/hashes": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.1.2.tgz", + "integrity": "sha512-KYRCASVTv6aeUi1tsF8/vpyR7zpfs3FUzy2Jqm+MU+LmUKhQ0y2FpfwqkCcxSg2ua4GALJd8k2R76WxwZGbQpA==", "dev": true, - "dependencies": { - "inherits": "^2.0.3", - "minimalistic-assert": "^1.0.0" - } - }, - "node_modules/ethers/node_modules/js-sha3": { - "version": "0.5.7", - "resolved": "https://registry.npmjs.org/js-sha3/-/js-sha3-0.5.7.tgz", - "integrity": "sha512-GII20kjaPX0zJ8wzkTbNDYMY7msuZcTWk8S5UOh6806Jq/wz1J8/bnr8uGU0DAUmYDjj2Mr4X1cW8v/GLYnR+g==", - "dev": true + "funding": [ + { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + ] }, - "node_modules/ethers/node_modules/scrypt-js": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/scrypt-js/-/scrypt-js-2.0.4.tgz", - "integrity": "sha512-4KsaGcPnuhtCZQCxFxN3GVYIhKFPTdLd8PLC552XwbMndtD0cjRFAhDuuydXQ0h08ZfPgzqe6EKHozpuH74iDw==", + "node_modules/ethers/node_modules/@types/node": { + "version": "18.15.13", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.15.13.tgz", + "integrity": "sha512-N+0kuo9KgrUQ1Sn/ifDXsvg0TTleP7rIy4zOBGECxAljqvqfqpTfzx0Q1NUedOixRMBfe2Whhb056a42cWs26Q==", "dev": true }, - "node_modules/ethers/node_modules/setimmediate": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.4.tgz", - "integrity": "sha512-/TjEmXQVEzdod/FFskf3o7oOAsGhHf2j1dZqRFbDzq4F3mvvxflIIi4Hd3bLQE9y/CpwqfSQam5JakI/mi3Pog==", + "node_modules/ethers/node_modules/aes-js": { + "version": "4.0.0-beta.5", + "resolved": "https://registry.npmjs.org/aes-js/-/aes-js-4.0.0-beta.5.tgz", + "integrity": "sha512-G965FqalsNyrPqgEGON7nIx1e/OVENSgiEIzyC63haUMuvNnwIgIjMs52hlTCKhkBny7A2ORNlfY9Zu+jmGk1Q==", "dev": true }, - "node_modules/ethers/node_modules/uuid": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-2.0.1.tgz", - "integrity": "sha512-nWg9+Oa3qD2CQzHIP4qKUqwNfzKn8P0LtFhotaCTFchsV7ZfDhAybeip/HZVeMIpZi9JgY1E3nUlwaCmZT1sEg==", - "deprecated": "Please upgrade to version 7 or higher. Older versions may use Math.random() in certain circumstances, which is known to be problematic. See https://v8.dev/blog/math-random for details.", + "node_modules/ethers/node_modules/tslib": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz", + "integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==", "dev": true }, + "node_modules/ethers/node_modules/ws": { + "version": "8.5.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.5.0.tgz", + "integrity": "sha512-BWX0SWVgLPzYwF8lTzEy1egjhS4S4OEAHfsO8o65WOVsrnSRGaSiUaa9e0ggGlkMTtBlmOpEXiie9RUcBO86qg==", + "dev": true, + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/ethjs-abi": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/ethjs-abi/-/ethjs-abi-0.2.1.tgz", @@ -7523,6 +8116,19 @@ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", "dev": true }, + "node_modules/find-replace": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/find-replace/-/find-replace-3.0.0.tgz", + "integrity": "sha512-6Tb2myMioCAgv5kfvP5/PkZZ/ntTpVK39fHY7WkWBgvbeE+VHd/tZuZ4mrC+bxh4cfOZeYKVPaJIZtZXV7GNCQ==", + "dev": true, + "peer": true, + "dependencies": { + "array-back": "^3.0.1" + }, + "engines": { + "node": ">=4.0.0" + } + }, "node_modules/find-up": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", @@ -8194,9 +8800,9 @@ } }, "node_modules/hardhat": { - "version": "2.17.3", - "resolved": "https://registry.npmjs.org/hardhat/-/hardhat-2.17.3.tgz", - "integrity": "sha512-SFZoYVXW1bWJZrIIKXOA+IgcctfuKXDwENywiYNT2dM3YQc4fXNaTbuk/vpPzHIF50upByx4zW5EqczKYQubsA==", + "version": "2.17.4", + "resolved": "https://registry.npmjs.org/hardhat/-/hardhat-2.17.4.tgz", + "integrity": "sha512-YTyHjVc9s14CY/O7Dbtzcr/92fcz6AzhrMaj6lYsZpYPIPLzOrFCZHHPxfGQB6FiE6IPNE0uJaAbr7zGF79goA==", "dev": true, "dependencies": { "@ethersproject/abi": "^5.1.2", @@ -9865,12 +10471,32 @@ "integrity": "sha512-hFuH8TY+Yji7Eja3mGiuAxBqLagejScbG8GbG0j6o9vzn0YL14My+ktnqtZgFTosKymC9/44wP6s7xyuLfnClw==", "dev": true }, + "node_modules/lodash.camelcase": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", + "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==", + "dev": true, + "peer": true + }, + "node_modules/lodash.clonedeep": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz", + "integrity": "sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ==", + "dev": true, + "peer": true + }, "node_modules/lodash.flatten": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/lodash.flatten/-/lodash.flatten-4.4.0.tgz", "integrity": "sha512-C5N2Z3DgnnKr0LOpv/hKCgKdb7ZZwafIrsesve6lmzvZIRZRGaZ/l6Q8+2W7NaT+ZwO3fFlSCzCzrDCFdJfZ4g==", "dev": true }, + "node_modules/lodash.isequal": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", + "integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==", + "dev": true + }, "node_modules/lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", @@ -10032,6 +10658,13 @@ "yallist": "^3.0.2" } }, + "node_modules/make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true, + "peer": true + }, "node_modules/map-obj": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-4.3.0.tgz", @@ -11065,6 +11698,12 @@ "node": ">= 0.8.0" } }, + "node_modules/ordinal": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/ordinal/-/ordinal-1.0.3.tgz", + "integrity": "sha512-cMddMgb2QElm8G7vdaa02jhUNbTSrhsgAGUz1OokD83uJTwSUn+nKoNoKVVaRa08yF6sgfO7Maou1+bgLd9rdQ==", + "dev": true + }, "node_modules/os-locale": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/os-locale/-/os-locale-1.4.0.tgz", @@ -11911,6 +12550,16 @@ "node": ">=8" } }, + "node_modules/reduce-flatten": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/reduce-flatten/-/reduce-flatten-2.0.0.tgz", + "integrity": "sha512-EJ4UNY/U1t2P/2k6oqotuX2Cc3T6nxJwsM0N0asT7dhrtH1ltUxDn4NalSYmPE2rCkVpcf/X6R0wDwcFpzhd4w==", + "dev": true, + "peer": true, + "engines": { + "node": ">=6" + } + }, "node_modules/regenerator-runtime": { "version": "0.14.0", "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.0.tgz", @@ -13993,6 +14642,13 @@ "safe-buffer": "~5.2.0" } }, + "node_modules/string-format": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/string-format/-/string-format-2.0.0.tgz", + "integrity": "sha512-bbEs3scLeYNXLecRRuk6uJxdXUSj6le/8rNPHChIJTn2V79aXVTR1EH2OH5zLKKoz0V02fOUKZZcw01pLUShZA==", + "dev": true, + "peer": true + }, "node_modules/string-width": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", @@ -14353,6 +15009,42 @@ "node": ">=10.0.0" } }, + "node_modules/table-layout": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/table-layout/-/table-layout-1.0.2.tgz", + "integrity": "sha512-qd/R7n5rQTRFi+Zf2sk5XVVd9UQl6ZkduPFC3S7WEGJAmetDTjY3qPN50eSKzwuzEyQKy5TN2TiZdkIjos2L6A==", + "dev": true, + "peer": true, + "dependencies": { + "array-back": "^4.0.1", + "deep-extend": "~0.6.0", + "typical": "^5.2.0", + "wordwrapjs": "^4.0.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/table-layout/node_modules/array-back": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/array-back/-/array-back-4.0.2.tgz", + "integrity": "sha512-NbdMezxqf94cnNfWLL7V/im0Ub+Anbb0IoZhvzie8+4HJ4nMQuzHuy49FkGYCJK2yAloZ3meiB6AVMClbrI1vg==", + "dev": true, + "peer": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/table-layout/node_modules/typical": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/typical/-/typical-5.2.0.tgz", + "integrity": "sha512-dvdQgNDNJo+8B2uBQoqdb11eUCE1JQXhvjC/CZtgvZseVd5TYMXnq0+vuUemXbd/Se29cTaUuPX3YIc2xgbvIg==", + "dev": true, + "peer": true, + "engines": { + "node": ">=8" + } + }, "node_modules/table/node_modules/ajv": { "version": "8.12.0", "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz", @@ -14597,6 +15289,162 @@ "node": ">=8" } }, + "node_modules/ts-command-line-args": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/ts-command-line-args/-/ts-command-line-args-2.5.1.tgz", + "integrity": "sha512-H69ZwTw3rFHb5WYpQya40YAX2/w7Ut75uUECbgBIsLmM+BNuYnxsltfyyLMxy6sEeKxgijLTnQtLd0nKd6+IYw==", + "dev": true, + "peer": true, + "dependencies": { + "chalk": "^4.1.0", + "command-line-args": "^5.1.1", + "command-line-usage": "^6.1.0", + "string-format": "^2.0.0" + }, + "bin": { + "write-markdown": "dist/write-markdown.js" + } + }, + "node_modules/ts-command-line-args/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "peer": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/ts-command-line-args/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "peer": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/ts-command-line-args/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "peer": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/ts-command-line-args/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "peer": true + }, + "node_modules/ts-command-line-args/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "peer": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/ts-command-line-args/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "peer": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/ts-essentials": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/ts-essentials/-/ts-essentials-7.0.3.tgz", + "integrity": "sha512-8+gr5+lqO3G84KdiTSMRLtuyJ+nTBVRKuCrK4lidMPdVeEp0uqC875uE5NMcaA7YYMN7XsNiFQuMvasF8HT/xQ==", + "dev": true, + "peer": true, + "peerDependencies": { + "typescript": ">=3.7.0" + } + }, + "node_modules/ts-node": { + "version": "10.9.1", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.1.tgz", + "integrity": "sha512-NtVysVPkxxrwFGUUxGYhfux8k78pQB3JqYBXlLRZgdGUqTO5wU/UyHop5p70iEbGhB7q5KmiZiU0Y3KlJrScEw==", + "dev": true, + "peer": true, + "dependencies": { + "@cspotcode/source-map-support": "^0.8.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "v8-compile-cache-lib": "^3.0.1", + "yn": "3.1.1" + }, + "bin": { + "ts-node": "dist/bin.js", + "ts-node-cwd": "dist/bin-cwd.js", + "ts-node-esm": "dist/bin-esm.js", + "ts-node-script": "dist/bin-script.js", + "ts-node-transpile-only": "dist/bin-transpile.js", + "ts-script": "dist/bin-script-deprecated.js" + }, + "peerDependencies": { + "@swc/core": ">=1.2.50", + "@swc/wasm": ">=1.2.50", + "@types/node": "*", + "typescript": ">=2.7" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "@swc/wasm": { + "optional": true + } + } + }, + "node_modules/ts-node/node_modules/diff": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "dev": true, + "peer": true, + "engines": { + "node": ">=0.3.1" + } + }, "node_modules/tslib": { "version": "1.14.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", @@ -14797,6 +15645,81 @@ "node": ">= 0.6" } }, + "node_modules/typechain": { + "version": "8.3.1", + "resolved": "https://registry.npmjs.org/typechain/-/typechain-8.3.1.tgz", + "integrity": "sha512-fA7clol2IP/56yq6vkMTR+4URF1nGjV82Wx6Rf09EsqD4tkzMAvEaqYxVFCavJm/1xaRga/oD55K+4FtuXwQOQ==", + "dev": true, + "peer": true, + "dependencies": { + "@types/prettier": "^2.1.1", + "debug": "^4.3.1", + "fs-extra": "^7.0.0", + "glob": "7.1.7", + "js-sha3": "^0.8.0", + "lodash": "^4.17.15", + "mkdirp": "^1.0.4", + "prettier": "^2.3.1", + "ts-command-line-args": "^2.2.0", + "ts-essentials": "^7.0.1" + }, + "bin": { + "typechain": "dist/cli/cli.js" + }, + "peerDependencies": { + "typescript": ">=4.3.0" + } + }, + "node_modules/typechain/node_modules/glob": { + "version": "7.1.7", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.7.tgz", + "integrity": "sha512-OvD9ENzPLbegENnYP5UUfJIirTg4+XwMWGaQfQTY0JenxNvvIKP3U3/tAQSPIu/lHxXYSZmpXlUHeqAIdKzBLQ==", + "dev": true, + "peer": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/typechain/node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "dev": true, + "peer": true, + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/typechain/node_modules/prettier": { + "version": "2.8.8", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.8.tgz", + "integrity": "sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==", + "dev": true, + "peer": true, + "bin": { + "prettier": "bin-prettier.js" + }, + "engines": { + "node": ">=10.13.0" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, "node_modules/typed-array-buffer": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.0.tgz", @@ -14877,6 +15800,30 @@ "is-typedarray": "^1.0.0" } }, + "node_modules/typescript": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.2.2.tgz", + "integrity": "sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w==", + "dev": true, + "peer": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/typical": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/typical/-/typical-4.0.0.tgz", + "integrity": "sha512-VAH4IvQ7BDFYglMd7BPRDfLgxZZX4O4TFcRDA6EN5X7erNJJq+McIEp8np9aVtxrCJ6qx4GTYVfOWNjcqwZgRw==", + "dev": true, + "peer": true, + "engines": { + "node": ">=8" + } + }, "node_modules/uglify-js": { "version": "3.17.4", "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.17.4.tgz", @@ -15033,6 +15980,13 @@ "uuid": "dist/bin/uuid" } }, + "node_modules/v8-compile-cache-lib": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", + "dev": true, + "peer": true + }, "node_modules/validate-npm-package-license": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", @@ -16082,6 +17036,30 @@ "integrity": "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==", "dev": true }, + "node_modules/wordwrapjs": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/wordwrapjs/-/wordwrapjs-4.0.1.tgz", + "integrity": "sha512-kKlNACbvHrkpIw6oPeYDSmdCTu2hdMHoyXLTcUKala++lx5Y+wjJ/e474Jqv5abnVmwxw08DiTuHmw69lJGksA==", + "dev": true, + "peer": true, + "dependencies": { + "reduce-flatten": "^2.0.0", + "typical": "^5.2.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/wordwrapjs/node_modules/typical": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/typical/-/typical-5.2.0.tgz", + "integrity": "sha512-dvdQgNDNJo+8B2uBQoqdb11eUCE1JQXhvjC/CZtgvZseVd5TYMXnq0+vuUemXbd/Se29cTaUuPX3YIc2xgbvIg==", + "dev": true, + "peer": true, + "engines": { + "node": ">=8" + } + }, "node_modules/workerpool": { "version": "6.2.1", "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.2.1.tgz", @@ -16523,6 +17501,16 @@ "node": ">=12" } }, + "node_modules/yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "dev": true, + "peer": true, + "engines": { + "node": ">=6" + } + }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", diff --git a/package.json b/package.json index 55a89746e4e..e6d28bce06c 100644 --- a/package.json +++ b/package.json @@ -53,8 +53,11 @@ "@changesets/cli": "^2.26.0", "@changesets/pre": "^1.0.14", "@changesets/read": "^0.5.9", + "@nomicfoundation/hardhat-chai-matchers": "^2.0.2", + "@nomicfoundation/hardhat-ethers": "^3.0.4", "@nomicfoundation/hardhat-foundry": "^1.1.1", "@nomicfoundation/hardhat-network-helpers": "^1.0.3", + "@nomicfoundation/hardhat-toolbox": "^3.0.0", "@nomiclabs/hardhat-truffle5": "^2.0.5", "@nomiclabs/hardhat-web3": "^2.0.0", "@openzeppelin/docs-utils": "^0.1.5", @@ -68,9 +71,10 @@ "eth-sig-util": "^3.0.0", "ethereumjs-util": "^7.0.7", "ethereumjs-wallet": "^1.0.1", + "ethers": "^6.7.1", "glob": "^10.3.5", "graphlib": "^2.1.8", - "hardhat": "^2.9.1", + "hardhat": "^2.17.4", "hardhat-exposed": "^0.3.13", "hardhat-gas-reporter": "^1.0.9", "hardhat-ignore-warnings": "^0.2.0", diff --git a/test/access/Ownable.test.js b/test/access/Ownable.test.js index f85daec5d28..36e3220c1ff 100644 --- a/test/access/Ownable.test.js +++ b/test/access/Ownable.test.js @@ -1,72 +1,80 @@ -const { constants, expectEvent } = require('@openzeppelin/test-helpers'); -const { expectRevertCustomError } = require('../helpers/customError'); - -const { ZERO_ADDRESS } = constants; - +const { ethers } = require('hardhat'); const { expect } = require('chai'); - -const Ownable = artifacts.require('$Ownable'); - -contract('Ownable', function (accounts) { - const [owner, other] = accounts; - +const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers'); +const { deploy, getFactory } = require('../helpers/deploy'); + +async function fixture() { + const accounts = await ethers.getSigners(); // this is slow :/ + const owner = accounts.shift(); + const other = accounts.shift(); + const ownable = await deploy('$Ownable', [ owner.address ]); + return { accounts, owner, other, ownable }; +}; + +describe('Ownable', function () { beforeEach(async function () { - this.ownable = await Ownable.new(owner); + await loadFixture(fixture).then(results => Object.assign(this, results)); }); it('rejects zero address for initialOwner', async function () { - await expectRevertCustomError(Ownable.new(constants.ZERO_ADDRESS), 'OwnableInvalidOwner', [constants.ZERO_ADDRESS]); + // checking a custom error requires a contract, or at least the interface + // we can get it from the contract factory + const { interface } = await getFactory('$Ownable'); + + await expect(deploy('$Ownable', [ ethers.ZeroAddress ])) + .to.be.revertedWithCustomError({ interface }, 'OwnableInvalidOwner') + .withArgs(ethers.ZeroAddress); }); it('has an owner', async function () { - expect(await this.ownable.owner()).to.equal(owner); + expect(await this.ownable.owner()).to.equal(this.owner.address); }); describe('transfer ownership', function () { it('changes owner after transfer', async function () { - const receipt = await this.ownable.transferOwnership(other, { from: owner }); - expectEvent(receipt, 'OwnershipTransferred'); + await expect(this.ownable.connect(this.owner).transferOwnership(this.other.address)) + .to.emit(this.ownable, 'OwnershipTransferred') + .withArgs(this.owner.address, this.other.address); - expect(await this.ownable.owner()).to.equal(other); + expect(await this.ownable.owner()).to.equal(this.other.address); }); it('prevents non-owners from transferring', async function () { - await expectRevertCustomError( - this.ownable.transferOwnership(other, { from: other }), - 'OwnableUnauthorizedAccount', - [other], - ); + await expect(this.ownable.connect(this.other).transferOwnership(this.other.address)) + .to.be.revertedWithCustomError(this.ownable, 'OwnableUnauthorizedAccount') + .withArgs(this.other.address); }); it('guards ownership against stuck state', async function () { - await expectRevertCustomError( - this.ownable.transferOwnership(ZERO_ADDRESS, { from: owner }), - 'OwnableInvalidOwner', - [ZERO_ADDRESS], - ); + await expect(this.ownable.connect(this.owner).transferOwnership(ethers.ZeroAddress)) + .to.be.revertedWithCustomError(this.ownable, 'OwnableInvalidOwner') + .withArgs(ethers.ZeroAddress); }); }); describe('renounce ownership', function () { it('loses ownership after renouncement', async function () { - const receipt = await this.ownable.renounceOwnership({ from: owner }); - expectEvent(receipt, 'OwnershipTransferred'); + await expect(this.ownable.connect(this.owner).renounceOwnership()) + .to.emit(this.ownable, 'OwnershipTransferred') + .withArgs(this.owner.address, ethers.ZeroAddress); - expect(await this.ownable.owner()).to.equal(ZERO_ADDRESS); + expect(await this.ownable.owner()).to.equal(ethers.ZeroAddress); }); it('prevents non-owners from renouncement', async function () { - await expectRevertCustomError(this.ownable.renounceOwnership({ from: other }), 'OwnableUnauthorizedAccount', [ - other, - ]); + await expect(this.ownable.connect(this.other).renounceOwnership()) + .to.be.revertedWithCustomError(this.ownable, 'OwnableUnauthorizedAccount') + .withArgs(this.other.address); }); it('allows to recover access using the internal _transferOwnership', async function () { - await this.ownable.renounceOwnership({ from: owner }); - const receipt = await this.ownable.$_transferOwnership(other); - expectEvent(receipt, 'OwnershipTransferred'); + await this.ownable.connect(this.owner).renounceOwnership(); + + await expect(this.ownable.$_transferOwnership(this.other.address)) + .to.emit(this.ownable, 'OwnershipTransferred') + .withArgs(ethers.ZeroAddress, this.other.address); - expect(await this.ownable.owner()).to.equal(other); + expect(await this.ownable.owner()).to.equal(this.other.address); }); }); }); diff --git a/test/helpers/deploy.js b/test/helpers/deploy.js new file mode 100644 index 00000000000..763708ad298 --- /dev/null +++ b/test/helpers/deploy.js @@ -0,0 +1,20 @@ +const { ethers } = require('hardhat'); + +async function getFactory(name, opts = {}) { + return ethers.getContractFactory(name).then(contract => contract.connect(opts.signer || contract.runner)); +} + +function attach(name, address, opts = {}) { + return getFactory(name, opts).then(factory => factory.attach(address)); +} + +function deploy(name, args = [], opts = {}) { + if (!Array.isArray(args)) { opts = args; args = []; } + return getFactory(name, opts).then(factory => factory.deploy(...args)).then(contract => contract.waitForDeployment()); +} + +module.exports = { + getFactory, + attach, + deploy, +}; \ No newline at end of file From 673ff7984a2d88c861d07b73c342659f3dac7b27 Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Thu, 5 Oct 2023 13:43:04 +0200 Subject: [PATCH 09/82] fix lint --- test/access/Ownable.test.js | 38 ++++++++++++++++++------------------- test/helpers/deploy.js | 21 ++++++++++++-------- 2 files changed, 32 insertions(+), 27 deletions(-) diff --git a/test/access/Ownable.test.js b/test/access/Ownable.test.js index 36e3220c1ff..f9cfa0a9928 100644 --- a/test/access/Ownable.test.js +++ b/test/access/Ownable.test.js @@ -5,11 +5,11 @@ const { deploy, getFactory } = require('../helpers/deploy'); async function fixture() { const accounts = await ethers.getSigners(); // this is slow :/ - const owner = accounts.shift(); - const other = accounts.shift(); - const ownable = await deploy('$Ownable', [ owner.address ]); + const owner = accounts.shift(); + const other = accounts.shift(); + const ownable = await deploy('$Ownable', [owner.address]); return { accounts, owner, other, ownable }; -}; +} describe('Ownable', function () { beforeEach(async function () { @@ -21,9 +21,9 @@ describe('Ownable', function () { // we can get it from the contract factory const { interface } = await getFactory('$Ownable'); - await expect(deploy('$Ownable', [ ethers.ZeroAddress ])) - .to.be.revertedWithCustomError({ interface }, 'OwnableInvalidOwner') - .withArgs(ethers.ZeroAddress); + await expect(deploy('$Ownable', [ethers.ZeroAddress])) + .to.be.revertedWithCustomError({ interface }, 'OwnableInvalidOwner') + .withArgs(ethers.ZeroAddress); }); it('has an owner', async function () { @@ -33,46 +33,46 @@ describe('Ownable', function () { describe('transfer ownership', function () { it('changes owner after transfer', async function () { await expect(this.ownable.connect(this.owner).transferOwnership(this.other.address)) - .to.emit(this.ownable, 'OwnershipTransferred') - .withArgs(this.owner.address, this.other.address); + .to.emit(this.ownable, 'OwnershipTransferred') + .withArgs(this.owner.address, this.other.address); expect(await this.ownable.owner()).to.equal(this.other.address); }); it('prevents non-owners from transferring', async function () { await expect(this.ownable.connect(this.other).transferOwnership(this.other.address)) - .to.be.revertedWithCustomError(this.ownable, 'OwnableUnauthorizedAccount') - .withArgs(this.other.address); + .to.be.revertedWithCustomError(this.ownable, 'OwnableUnauthorizedAccount') + .withArgs(this.other.address); }); it('guards ownership against stuck state', async function () { await expect(this.ownable.connect(this.owner).transferOwnership(ethers.ZeroAddress)) - .to.be.revertedWithCustomError(this.ownable, 'OwnableInvalidOwner') - .withArgs(ethers.ZeroAddress); + .to.be.revertedWithCustomError(this.ownable, 'OwnableInvalidOwner') + .withArgs(ethers.ZeroAddress); }); }); describe('renounce ownership', function () { it('loses ownership after renouncement', async function () { await expect(this.ownable.connect(this.owner).renounceOwnership()) - .to.emit(this.ownable, 'OwnershipTransferred') - .withArgs(this.owner.address, ethers.ZeroAddress); + .to.emit(this.ownable, 'OwnershipTransferred') + .withArgs(this.owner.address, ethers.ZeroAddress); expect(await this.ownable.owner()).to.equal(ethers.ZeroAddress); }); it('prevents non-owners from renouncement', async function () { await expect(this.ownable.connect(this.other).renounceOwnership()) - .to.be.revertedWithCustomError(this.ownable, 'OwnableUnauthorizedAccount') - .withArgs(this.other.address); + .to.be.revertedWithCustomError(this.ownable, 'OwnableUnauthorizedAccount') + .withArgs(this.other.address); }); it('allows to recover access using the internal _transferOwnership', async function () { await this.ownable.connect(this.owner).renounceOwnership(); await expect(this.ownable.$_transferOwnership(this.other.address)) - .to.emit(this.ownable, 'OwnershipTransferred') - .withArgs(ethers.ZeroAddress, this.other.address); + .to.emit(this.ownable, 'OwnershipTransferred') + .withArgs(ethers.ZeroAddress, this.other.address); expect(await this.ownable.owner()).to.equal(this.other.address); }); diff --git a/test/helpers/deploy.js b/test/helpers/deploy.js index 763708ad298..30817f609fd 100644 --- a/test/helpers/deploy.js +++ b/test/helpers/deploy.js @@ -1,20 +1,25 @@ const { ethers } = require('hardhat'); async function getFactory(name, opts = {}) { - return ethers.getContractFactory(name).then(contract => contract.connect(opts.signer || contract.runner)); + return ethers.getContractFactory(name).then(contract => contract.connect(opts.signer || contract.runner)); } function attach(name, address, opts = {}) { - return getFactory(name, opts).then(factory => factory.attach(address)); + return getFactory(name, opts).then(factory => factory.attach(address)); } function deploy(name, args = [], opts = {}) { - if (!Array.isArray(args)) { opts = args; args = []; } - return getFactory(name, opts).then(factory => factory.deploy(...args)).then(contract => contract.waitForDeployment()); + if (!Array.isArray(args)) { + opts = args; + args = []; + } + return getFactory(name, opts) + .then(factory => factory.deploy(...args)) + .then(contract => contract.waitForDeployment()); } module.exports = { - getFactory, - attach, - deploy, -}; \ No newline at end of file + getFactory, + attach, + deploy, +}; From c9b4e8dd45e64613941c6dad4161e2ec7ffebcee Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Thu, 5 Oct 2023 13:43:31 +0200 Subject: [PATCH 10/82] Update hardhat.config.js --- hardhat.config.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hardhat.config.js b/hardhat.config.js index 9a7667a9f06..b15fb282edb 100644 --- a/hardhat.config.js +++ b/hardhat.config.js @@ -55,7 +55,7 @@ const argv = require('yargs/yargs')() }, }).argv; -// require('@nomiclabs/hardhat-truffle5'); +require('@nomiclabs/hardhat-truffle5'); // deprecated require('@nomicfoundation/hardhat-toolbox'); require('@nomicfoundation/hardhat-ethers'); require('hardhat-ignore-warnings'); From c46a45e3182ddca2bda0717f6001b8e7b8a0cdf6 Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Thu, 5 Oct 2023 15:47:21 +0200 Subject: [PATCH 11/82] add Ownable2Step --- test/access/Ownable2Step.test.js | 90 ++++++++++++++++++-------------- 1 file changed, 52 insertions(+), 38 deletions(-) diff --git a/test/access/Ownable2Step.test.js b/test/access/Ownable2Step.test.js index bdbac48fa12..6e1a2fd9eb6 100644 --- a/test/access/Ownable2Step.test.js +++ b/test/access/Ownable2Step.test.js @@ -1,70 +1,84 @@ -const { constants, expectEvent } = require('@openzeppelin/test-helpers'); -const { ZERO_ADDRESS } = constants; +const { ethers } = require('hardhat'); const { expect } = require('chai'); -const { expectRevertCustomError } = require('../helpers/customError'); +const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers'); +const { deploy } = require('../helpers/deploy'); -const Ownable2Step = artifacts.require('$Ownable2Step'); - -contract('Ownable2Step', function (accounts) { - const [owner, accountA, accountB] = accounts; +async function fixture() { + const accounts = await ethers.getSigners(); + const owner = accounts.shift(); + const accountA = accounts.shift(); + const accountB = accounts.shift(); + const ownable2Step = await deploy('$Ownable2Step', [owner.address]); + return { accounts, owner, accountA, accountB, ownable2Step }; +} +describe('Ownable2Step', function () { beforeEach(async function () { - this.ownable2Step = await Ownable2Step.new(owner); + await loadFixture(fixture).then(results => Object.assign(this, results)); }); describe('transfer ownership', function () { it('starting a transfer does not change owner', async function () { - const receipt = await this.ownable2Step.transferOwnership(accountA, { from: owner }); - expectEvent(receipt, 'OwnershipTransferStarted', { previousOwner: owner, newOwner: accountA }); - expect(await this.ownable2Step.owner()).to.equal(owner); - expect(await this.ownable2Step.pendingOwner()).to.equal(accountA); + await expect(this.ownable2Step.connect(this.owner).transferOwnership(this.accountA.address)) + .to.emit(this.ownable2Step, 'OwnershipTransferStarted') + .withArgs(this.owner.address, this.accountA.address); + + expect(await this.ownable2Step.owner()).to.equal(this.owner.address); + expect(await this.ownable2Step.pendingOwner()).to.equal(this.accountA.address); }); it('changes owner after transfer', async function () { - await this.ownable2Step.transferOwnership(accountA, { from: owner }); - const receipt = await this.ownable2Step.acceptOwnership({ from: accountA }); - expectEvent(receipt, 'OwnershipTransferred', { previousOwner: owner, newOwner: accountA }); - expect(await this.ownable2Step.owner()).to.equal(accountA); - expect(await this.ownable2Step.pendingOwner()).to.not.equal(accountA); + await this.ownable2Step.connect(this.owner).transferOwnership(this.accountA.address); + + await expect(this.ownable2Step.connect(this.accountA).acceptOwnership()) + .to.emit(this.ownable2Step, 'OwnershipTransferred') + .withArgs(this.owner.address, this.accountA.address); + + expect(await this.ownable2Step.owner()).to.equal(this.accountA.address); + expect(await this.ownable2Step.pendingOwner()).to.equal(ethers.ZeroAddress); }); it('guards transfer against invalid user', async function () { - await this.ownable2Step.transferOwnership(accountA, { from: owner }); - await expectRevertCustomError( - this.ownable2Step.acceptOwnership({ from: accountB }), - 'OwnableUnauthorizedAccount', - [accountB], - ); + await this.ownable2Step.connect(this.owner).transferOwnership(this.accountA.address); + + await expect(this.ownable2Step.connect(this.accountB).acceptOwnership()) + .to.be.revertedWithCustomError(this.ownable2Step, 'OwnableUnauthorizedAccount') + .withArgs(this.accountB.address); }); }); describe('renouncing ownership', async function () { it('changes owner after renouncing ownership', async function () { - await this.ownable2Step.renounceOwnership({ from: owner }); + await expect(this.ownable2Step.connect(this.owner).renounceOwnership()) + .to.emit(this.ownable2Step, 'OwnershipTransferred') + .withArgs(this.owner.address, ethers.ZeroAddress); + // If renounceOwnership is removed from parent an alternative is needed ... // without it is difficult to cleanly renounce with the two step process // see: https://github.com/OpenZeppelin/openzeppelin-contracts/pull/3620#discussion_r957930388 - expect(await this.ownable2Step.owner()).to.equal(ZERO_ADDRESS); + expect(await this.ownable2Step.owner()).to.equal(ethers.ZeroAddress); }); it('pending owner resets after renouncing ownership', async function () { - await this.ownable2Step.transferOwnership(accountA, { from: owner }); - expect(await this.ownable2Step.pendingOwner()).to.equal(accountA); - await this.ownable2Step.renounceOwnership({ from: owner }); - expect(await this.ownable2Step.pendingOwner()).to.equal(ZERO_ADDRESS); - await expectRevertCustomError( - this.ownable2Step.acceptOwnership({ from: accountA }), - 'OwnableUnauthorizedAccount', - [accountA], - ); + await this.ownable2Step.connect(this.owner).transferOwnership(this.accountA.address); + expect(await this.ownable2Step.pendingOwner()).to.equal(this.accountA.address); + + await this.ownable2Step.connect(this.owner).renounceOwnership(); + expect(await this.ownable2Step.pendingOwner()).to.equal(ethers.ZeroAddress); + + await expect(this.ownable2Step.connect(this.accountA).acceptOwnership()) + .to.be.revertedWithCustomError(this.ownable2Step, 'OwnableUnauthorizedAccount') + .withArgs(this.accountA.address); }); it('allows to recover access using the internal _transferOwnership', async function () { - await this.ownable2Step.renounceOwnership({ from: owner }); - const receipt = await this.ownable2Step.$_transferOwnership(accountA); - expectEvent(receipt, 'OwnershipTransferred'); + await this.ownable2Step.connect(this.owner).renounceOwnership(); + + await expect(this.ownable2Step.$_transferOwnership(this.accountA.address)) + .to.emit(this.ownable2Step, 'OwnershipTransferred') + .withArgs(ethers.ZeroAddress, this.accountA.address); - expect(await this.ownable2Step.owner()).to.equal(accountA); + expect(await this.ownable2Step.owner()).to.equal(this.accountA.address); }); }); }); From 328a34426d6f4fe8f8612d1e6e556da74c14f5d9 Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Thu, 5 Oct 2023 18:57:03 +0200 Subject: [PATCH 12/82] remove deploy helper --- test/access/Ownable.test.js | 7 +++---- test/access/Ownable2Step.test.js | 3 +-- test/helpers/deploy.js | 25 ------------------------- 3 files changed, 4 insertions(+), 31 deletions(-) delete mode 100644 test/helpers/deploy.js diff --git a/test/access/Ownable.test.js b/test/access/Ownable.test.js index f9cfa0a9928..ec5fa8a4351 100644 --- a/test/access/Ownable.test.js +++ b/test/access/Ownable.test.js @@ -1,13 +1,12 @@ const { ethers } = require('hardhat'); const { expect } = require('chai'); const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers'); -const { deploy, getFactory } = require('../helpers/deploy'); async function fixture() { const accounts = await ethers.getSigners(); // this is slow :/ const owner = accounts.shift(); const other = accounts.shift(); - const ownable = await deploy('$Ownable', [owner.address]); + const ownable = await ethers.deployContract('$Ownable', [owner.address]); return { accounts, owner, other, ownable }; } @@ -19,9 +18,9 @@ describe('Ownable', function () { it('rejects zero address for initialOwner', async function () { // checking a custom error requires a contract, or at least the interface // we can get it from the contract factory - const { interface } = await getFactory('$Ownable'); + const { interface } = await ethers.getContractFactory('$Ownable'); - await expect(deploy('$Ownable', [ethers.ZeroAddress])) + await expect(ethers.deployContract('$Ownable', [ethers.ZeroAddress])) .to.be.revertedWithCustomError({ interface }, 'OwnableInvalidOwner') .withArgs(ethers.ZeroAddress); }); diff --git a/test/access/Ownable2Step.test.js b/test/access/Ownable2Step.test.js index 6e1a2fd9eb6..307a8245f5d 100644 --- a/test/access/Ownable2Step.test.js +++ b/test/access/Ownable2Step.test.js @@ -1,14 +1,13 @@ const { ethers } = require('hardhat'); const { expect } = require('chai'); const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers'); -const { deploy } = require('../helpers/deploy'); async function fixture() { const accounts = await ethers.getSigners(); const owner = accounts.shift(); const accountA = accounts.shift(); const accountB = accounts.shift(); - const ownable2Step = await deploy('$Ownable2Step', [owner.address]); + const ownable2Step = await ethers.deployContract('$Ownable2Step', [owner.address]); return { accounts, owner, accountA, accountB, ownable2Step }; } diff --git a/test/helpers/deploy.js b/test/helpers/deploy.js deleted file mode 100644 index 30817f609fd..00000000000 --- a/test/helpers/deploy.js +++ /dev/null @@ -1,25 +0,0 @@ -const { ethers } = require('hardhat'); - -async function getFactory(name, opts = {}) { - return ethers.getContractFactory(name).then(contract => contract.connect(opts.signer || contract.runner)); -} - -function attach(name, address, opts = {}) { - return getFactory(name, opts).then(factory => factory.attach(address)); -} - -function deploy(name, args = [], opts = {}) { - if (!Array.isArray(args)) { - opts = args; - args = []; - } - return getFactory(name, opts) - .then(factory => factory.deploy(...args)) - .then(contract => contract.waitForDeployment()); -} - -module.exports = { - getFactory, - attach, - deploy, -}; From 7c12232e29f0071b998ca93bd9b9545a734e6906 Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Thu, 5 Oct 2023 19:06:08 +0200 Subject: [PATCH 13/82] add deprecation notices --- test/helpers/chainid.js | 2 ++ test/helpers/create.js | 22 +++------------------- test/helpers/customError.js | 2 ++ 3 files changed, 7 insertions(+), 19 deletions(-) diff --git a/test/helpers/chainid.js b/test/helpers/chainid.js index 58757eb80f9..693a822e559 100644 --- a/test/helpers/chainid.js +++ b/test/helpers/chainid.js @@ -7,4 +7,6 @@ async function getChainId() { module.exports = { getChainId, + // TODO: when tests are ready to support bigint chainId + // getChainId: ethers.provider.getNetwork().then(network => network.chainId), }; diff --git a/test/helpers/create.js b/test/helpers/create.js index 98a0d4c4787..719937e74d0 100644 --- a/test/helpers/create.js +++ b/test/helpers/create.js @@ -1,22 +1,6 @@ -const RLP = require('rlp'); - -function computeCreateAddress(deployer, nonce) { - return web3.utils.toChecksumAddress(web3.utils.sha3(RLP.encode([deployer.address ?? deployer, nonce])).slice(-40)); -} - -function computeCreate2Address(saltHex, bytecode, deployer) { - return web3.utils.toChecksumAddress( - web3.utils - .sha3( - `0x${['ff', deployer.address ?? deployer, saltHex, web3.utils.soliditySha3(bytecode)] - .map(x => x.replace(/0x/, '')) - .join('')}`, - ) - .slice(-40), - ); -} +const { ethers } = require('hardhat'); module.exports = { - computeCreateAddress, - computeCreate2Address, + computeCreateAddress: (from, nonce) => ethers.getCreateAddress({ from, nonce }), + computeCreate2Address: (salt, bytecode, from) => ethers.getCreate2Address(from, salt, ethers.keccak256(ethers.toBeArray(bytecode))), }; diff --git a/test/helpers/customError.js b/test/helpers/customError.js index ea5c36820c5..acc3214eb80 100644 --- a/test/helpers/customError.js +++ b/test/helpers/customError.js @@ -1,3 +1,5 @@ +// DEPRECATED: replace with hardhat-toolbox chai matchers. + const { expect } = require('chai'); /** Revert handler that supports custom errors. */ From 517ad8785f06582c29e301b7041550790c560e4d Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Thu, 5 Oct 2023 19:15:03 +0200 Subject: [PATCH 14/82] up --- test/helpers/create.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/helpers/create.js b/test/helpers/create.js index 719937e74d0..797211ec4c9 100644 --- a/test/helpers/create.js +++ b/test/helpers/create.js @@ -2,5 +2,5 @@ const { ethers } = require('hardhat'); module.exports = { computeCreateAddress: (from, nonce) => ethers.getCreateAddress({ from, nonce }), - computeCreate2Address: (salt, bytecode, from) => ethers.getCreate2Address(from, salt, ethers.keccak256(ethers.toBeArray(bytecode))), + computeCreate2Address: (salt, bytecode, from) => ethers.getCreate2Address(from, salt, ethers.keccak256(ethers.getBytes(bytecode))), }; From 9002e5f7ef8137a8bce62d9eaae54cab0f8cfa5c Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Thu, 5 Oct 2023 19:15:47 +0200 Subject: [PATCH 15/82] up --- test/helpers/create.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/helpers/create.js b/test/helpers/create.js index 797211ec4c9..fa837395ab1 100644 --- a/test/helpers/create.js +++ b/test/helpers/create.js @@ -2,5 +2,5 @@ const { ethers } = require('hardhat'); module.exports = { computeCreateAddress: (from, nonce) => ethers.getCreateAddress({ from, nonce }), - computeCreate2Address: (salt, bytecode, from) => ethers.getCreate2Address(from, salt, ethers.keccak256(ethers.getBytes(bytecode))), + computeCreate2Address: (salt, bytecode, from) => ethers.getCreate2Address(from, salt, ethers.keccak256(bytecode)), }; From 89ab4c9fe828b57e4d3ff76cdd7a218f1851a8aa Mon Sep 17 00:00:00 2001 From: Vittorio Minacori Date: Fri, 6 Oct 2023 12:36:27 +0200 Subject: [PATCH 16/82] Rename wrong file --- .../{ERC1363.behaviour.js => ERC1363.behavior.js} | 6 +++--- test/token/ERC20/extensions/ERC1363.test.js | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) rename test/token/ERC20/extensions/{ERC1363.behaviour.js => ERC1363.behavior.js} (99%) diff --git a/test/token/ERC20/extensions/ERC1363.behaviour.js b/test/token/ERC20/extensions/ERC1363.behavior.js similarity index 99% rename from test/token/ERC20/extensions/ERC1363.behaviour.js rename to test/token/ERC20/extensions/ERC1363.behavior.js index 04f8a15bdb0..0cf101ec4a0 100644 --- a/test/token/ERC20/extensions/ERC1363.behaviour.js +++ b/test/token/ERC20/extensions/ERC1363.behavior.js @@ -132,7 +132,7 @@ function shouldBehaveLikeERC1363(initialSupply, accounts) { shouldTransferSafely(transferAndCallWithoutData, null); }); - describe('testing ERC20 behaviours', function () { + describe('testing ERC20 behavior', function () { transferWasSuccessful(owner, initialBalance); }); @@ -324,7 +324,7 @@ function shouldBehaveLikeERC1363(initialSupply, accounts) { shouldTransferFromSafely(transferFromAndCallWithoutData, null); }); - describe('testing ERC20 behaviours', function () { + describe('testing ERC20 behavior', function () { transferFromWasSuccessful(owner, spender, initialBalance); }); @@ -496,7 +496,7 @@ function shouldBehaveLikeERC1363(initialSupply, accounts) { shouldApproveSafely(approveAndCallWithoutData, null); }); - describe('testing ERC20 behaviours', function () { + describe('testing ERC20 behavior', function () { approveWasSuccessful(owner, initialBalance); }); diff --git a/test/token/ERC20/extensions/ERC1363.test.js b/test/token/ERC20/extensions/ERC1363.test.js index 54e6171e57b..603c89adaae 100644 --- a/test/token/ERC20/extensions/ERC1363.test.js +++ b/test/token/ERC20/extensions/ERC1363.test.js @@ -1,6 +1,6 @@ const { BN } = require('@openzeppelin/test-helpers'); -const { shouldBehaveLikeERC1363 } = require('./ERC1363.behaviour'); +const { shouldBehaveLikeERC1363 } = require('./ERC1363.behavior'); const ERC1363 = artifacts.require('$ERC1363'); From 74558f860a8532cda935b8b50b869fb46a1d2d08 Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Fri, 6 Oct 2023 13:55:04 +0200 Subject: [PATCH 17/82] remove .address when doing ethers call that support addressable --- test/access/Ownable.test.js | 8 ++++---- test/access/Ownable2Step.test.js | 12 ++++++------ 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/test/access/Ownable.test.js b/test/access/Ownable.test.js index ec5fa8a4351..17080acf632 100644 --- a/test/access/Ownable.test.js +++ b/test/access/Ownable.test.js @@ -6,7 +6,7 @@ async function fixture() { const accounts = await ethers.getSigners(); // this is slow :/ const owner = accounts.shift(); const other = accounts.shift(); - const ownable = await ethers.deployContract('$Ownable', [owner.address]); + const ownable = await ethers.deployContract('$Ownable', [owner]); return { accounts, owner, other, ownable }; } @@ -31,7 +31,7 @@ describe('Ownable', function () { describe('transfer ownership', function () { it('changes owner after transfer', async function () { - await expect(this.ownable.connect(this.owner).transferOwnership(this.other.address)) + await expect(this.ownable.connect(this.owner).transferOwnership(this.other)) .to.emit(this.ownable, 'OwnershipTransferred') .withArgs(this.owner.address, this.other.address); @@ -39,7 +39,7 @@ describe('Ownable', function () { }); it('prevents non-owners from transferring', async function () { - await expect(this.ownable.connect(this.other).transferOwnership(this.other.address)) + await expect(this.ownable.connect(this.other).transferOwnership(this.other)) .to.be.revertedWithCustomError(this.ownable, 'OwnableUnauthorizedAccount') .withArgs(this.other.address); }); @@ -69,7 +69,7 @@ describe('Ownable', function () { it('allows to recover access using the internal _transferOwnership', async function () { await this.ownable.connect(this.owner).renounceOwnership(); - await expect(this.ownable.$_transferOwnership(this.other.address)) + await expect(this.ownable.$_transferOwnership(this.other)) .to.emit(this.ownable, 'OwnershipTransferred') .withArgs(ethers.ZeroAddress, this.other.address); diff --git a/test/access/Ownable2Step.test.js b/test/access/Ownable2Step.test.js index 307a8245f5d..868b9739549 100644 --- a/test/access/Ownable2Step.test.js +++ b/test/access/Ownable2Step.test.js @@ -7,7 +7,7 @@ async function fixture() { const owner = accounts.shift(); const accountA = accounts.shift(); const accountB = accounts.shift(); - const ownable2Step = await ethers.deployContract('$Ownable2Step', [owner.address]); + const ownable2Step = await ethers.deployContract('$Ownable2Step', [owner]); return { accounts, owner, accountA, accountB, ownable2Step }; } @@ -18,7 +18,7 @@ describe('Ownable2Step', function () { describe('transfer ownership', function () { it('starting a transfer does not change owner', async function () { - await expect(this.ownable2Step.connect(this.owner).transferOwnership(this.accountA.address)) + await expect(this.ownable2Step.connect(this.owner).transferOwnership(this.accountA)) .to.emit(this.ownable2Step, 'OwnershipTransferStarted') .withArgs(this.owner.address, this.accountA.address); @@ -27,7 +27,7 @@ describe('Ownable2Step', function () { }); it('changes owner after transfer', async function () { - await this.ownable2Step.connect(this.owner).transferOwnership(this.accountA.address); + await this.ownable2Step.connect(this.owner).transferOwnership(this.accountA); await expect(this.ownable2Step.connect(this.accountA).acceptOwnership()) .to.emit(this.ownable2Step, 'OwnershipTransferred') @@ -38,7 +38,7 @@ describe('Ownable2Step', function () { }); it('guards transfer against invalid user', async function () { - await this.ownable2Step.connect(this.owner).transferOwnership(this.accountA.address); + await this.ownable2Step.connect(this.owner).transferOwnership(this.accountA); await expect(this.ownable2Step.connect(this.accountB).acceptOwnership()) .to.be.revertedWithCustomError(this.ownable2Step, 'OwnableUnauthorizedAccount') @@ -59,7 +59,7 @@ describe('Ownable2Step', function () { }); it('pending owner resets after renouncing ownership', async function () { - await this.ownable2Step.connect(this.owner).transferOwnership(this.accountA.address); + await this.ownable2Step.connect(this.owner).transferOwnership(this.accountA); expect(await this.ownable2Step.pendingOwner()).to.equal(this.accountA.address); await this.ownable2Step.connect(this.owner).renounceOwnership(); @@ -73,7 +73,7 @@ describe('Ownable2Step', function () { it('allows to recover access using the internal _transferOwnership', async function () { await this.ownable2Step.connect(this.owner).renounceOwnership(); - await expect(this.ownable2Step.$_transferOwnership(this.accountA.address)) + await expect(this.ownable2Step.$_transferOwnership(this.accountA)) .to.emit(this.ownable2Step, 'OwnershipTransferred') .withArgs(ethers.ZeroAddress, this.accountA.address); From 0e6e84cb95032f70a6f04b5a494511942e5422ac Mon Sep 17 00:00:00 2001 From: Vittorio Minacori Date: Fri, 6 Oct 2023 15:14:29 +0200 Subject: [PATCH 18/82] Update pragma to 0.8.20 --- contracts/mocks/token/ERC1363ReceiverMock.sol | 2 +- contracts/mocks/token/ERC1363SpenderMock.sol | 2 +- contracts/token/ERC20/extensions/ERC1363.sol | 2 +- contracts/token/ERC20/utils/ERC1363Holder.sol | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/contracts/mocks/token/ERC1363ReceiverMock.sol b/contracts/mocks/token/ERC1363ReceiverMock.sol index 2cacd941b32..dc3145415b8 100644 --- a/contracts/mocks/token/ERC1363ReceiverMock.sol +++ b/contracts/mocks/token/ERC1363ReceiverMock.sol @@ -1,6 +1,6 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; +pragma solidity ^0.8.20; import {IERC1363Receiver} from "../../interfaces/IERC1363Receiver.sol"; diff --git a/contracts/mocks/token/ERC1363SpenderMock.sol b/contracts/mocks/token/ERC1363SpenderMock.sol index a1f5d199a38..bc19c39ea0b 100644 --- a/contracts/mocks/token/ERC1363SpenderMock.sol +++ b/contracts/mocks/token/ERC1363SpenderMock.sol @@ -1,6 +1,6 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; +pragma solidity ^0.8.20; import {IERC1363Spender} from "../../interfaces/IERC1363Spender.sol"; diff --git a/contracts/token/ERC20/extensions/ERC1363.sol b/contracts/token/ERC20/extensions/ERC1363.sol index 43c39acbcb6..522693f8274 100644 --- a/contracts/token/ERC20/extensions/ERC1363.sol +++ b/contracts/token/ERC20/extensions/ERC1363.sol @@ -1,6 +1,6 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; +pragma solidity ^0.8.20; import {ERC20} from "../ERC20.sol"; import {IERC165, ERC165} from "../../../utils/introspection/ERC165.sol"; diff --git a/contracts/token/ERC20/utils/ERC1363Holder.sol b/contracts/token/ERC20/utils/ERC1363Holder.sol index b7dde852c4e..c1a784dd90a 100644 --- a/contracts/token/ERC20/utils/ERC1363Holder.sol +++ b/contracts/token/ERC20/utils/ERC1363Holder.sol @@ -1,6 +1,6 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; +pragma solidity ^0.8.20; import {IERC1363Receiver} from "../../../interfaces/IERC1363Receiver.sol"; import {IERC1363Spender} from "../../../interfaces/IERC1363Spender.sol"; From 9ea2db14c6d139c1449d38f39a3a20c4a01cb2f5 Mon Sep 17 00:00:00 2001 From: ernestognw Date: Mon, 9 Oct 2023 22:16:08 -0600 Subject: [PATCH 19/82] Fix flaky test in ERC2981.behavior --- test/access/Ownable.test.js | 6 +----- test/token/common/ERC2981.behavior.js | 2 +- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/test/access/Ownable.test.js b/test/access/Ownable.test.js index 17080acf632..19b6ecbabeb 100644 --- a/test/access/Ownable.test.js +++ b/test/access/Ownable.test.js @@ -16,12 +16,8 @@ describe('Ownable', function () { }); it('rejects zero address for initialOwner', async function () { - // checking a custom error requires a contract, or at least the interface - // we can get it from the contract factory - const { interface } = await ethers.getContractFactory('$Ownable'); - await expect(ethers.deployContract('$Ownable', [ethers.ZeroAddress])) - .to.be.revertedWithCustomError({ interface }, 'OwnableInvalidOwner') + .to.be.revertedWithCustomError(this.ownable, 'OwnableInvalidOwner') .withArgs(ethers.ZeroAddress); }); diff --git a/test/token/common/ERC2981.behavior.js b/test/token/common/ERC2981.behavior.js index 15efa239f70..1c062b0524c 100644 --- a/test/token/common/ERC2981.behavior.js +++ b/test/token/common/ERC2981.behavior.js @@ -108,7 +108,7 @@ function shouldBehaveLikeERC2981() { const token2Info = await this.token.royaltyInfo(this.tokenId2, this.salePrice); // must be different even at the same this.salePrice - expect(token1Info[1]).to.not.be.equal(token2Info.royaltyFraction); + expect(token1Info[1]).to.not.be.bignumber.equal(token2Info[1]); }); it('reverts if invalid parameters', async function () { From fd7100a0ba776da71f29051ddd6148d69e491247 Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Tue, 10 Oct 2023 18:09:50 +0200 Subject: [PATCH 20/82] Update test/access/Ownable.test.js --- test/access/Ownable.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/access/Ownable.test.js b/test/access/Ownable.test.js index 19b6ecbabeb..711ee38dd80 100644 --- a/test/access/Ownable.test.js +++ b/test/access/Ownable.test.js @@ -17,7 +17,7 @@ describe('Ownable', function () { it('rejects zero address for initialOwner', async function () { await expect(ethers.deployContract('$Ownable', [ethers.ZeroAddress])) - .to.be.revertedWithCustomError(this.ownable, 'OwnableInvalidOwner') + .to.be.revertedWithCustomError({interface: this.ownable.interface}, 'OwnableInvalidOwner') .withArgs(ethers.ZeroAddress); }); From 1a9a04624ed7097241ea54f9dbe4598417ce5af4 Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Tue, 10 Oct 2023 18:10:33 +0200 Subject: [PATCH 21/82] Update test/access/Ownable.test.js MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Ernesto García --- test/access/Ownable.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/access/Ownable.test.js b/test/access/Ownable.test.js index 711ee38dd80..9e812cbcf5d 100644 --- a/test/access/Ownable.test.js +++ b/test/access/Ownable.test.js @@ -12,7 +12,7 @@ async function fixture() { describe('Ownable', function () { beforeEach(async function () { - await loadFixture(fixture).then(results => Object.assign(this, results)); + Object.assign(this, await loadFixture(fixture)); }); it('rejects zero address for initialOwner', async function () { From b569ca8604499e2367c0e0c48270def98b435949 Mon Sep 17 00:00:00 2001 From: ernestognw Date: Tue, 10 Oct 2023 11:26:02 -0600 Subject: [PATCH 22/82] Fix lint --- test/access/Ownable.test.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/access/Ownable.test.js b/test/access/Ownable.test.js index 9e812cbcf5d..f1b6d2f2c53 100644 --- a/test/access/Ownable.test.js +++ b/test/access/Ownable.test.js @@ -12,12 +12,12 @@ async function fixture() { describe('Ownable', function () { beforeEach(async function () { - Object.assign(this, await loadFixture(fixture)); + Object.assign(this, await loadFixture(fixture)); }); it('rejects zero address for initialOwner', async function () { await expect(ethers.deployContract('$Ownable', [ethers.ZeroAddress])) - .to.be.revertedWithCustomError({interface: this.ownable.interface}, 'OwnableInvalidOwner') + .to.be.revertedWithCustomError({ interface: this.ownable.interface }, 'OwnableInvalidOwner') .withArgs(ethers.ZeroAddress); }); From b24fec46deebe7086c1ff8719c39817deda567f8 Mon Sep 17 00:00:00 2001 From: ernestognw Date: Tue, 10 Oct 2023 13:37:50 -0600 Subject: [PATCH 23/82] Fix upgradeable tests --- hardhat/env-artifacts.js | 23 +++++++++++++++-------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/hardhat/env-artifacts.js b/hardhat/env-artifacts.js index fbbea2e2d94..cf773f140ca 100644 --- a/hardhat/env-artifacts.js +++ b/hardhat/env-artifacts.js @@ -1,15 +1,11 @@ const { HardhatError } = require('hardhat/internal/core/errors'); -// Modifies `artifacts.require(X)` so that instead of X it loads the XUpgradeable contract. -// This allows us to run the same test suite on both the original and the transpiled and renamed Upgradeable contracts. - -extendEnvironment(env => { - const artifactsRequire = env.artifacts.require; - - env.artifacts.require = name => { +// Extends a require artifact function to try with the Upgradeable variants. +function tryRequireUpgradableVariantsWith(originalRequire) { + return function (name) { for (const suffix of ['UpgradeableWithInit', 'Upgradeable', '']) { try { - return artifactsRequire(name + suffix); + return originalRequire.call(this, name + suffix); } catch (e) { // HH700: Artifact not found - from https://hardhat.org/hardhat-runner/docs/errors#HH700 if (HardhatError.isHardhatError(e) && e.number === 700 && suffix !== '') { @@ -21,4 +17,15 @@ extendEnvironment(env => { } throw new Error('Unreachable'); }; +} + +// Modifies the artifact require functions so that instead of X it loads the XUpgradeable contract. +// This allows us to run the same test suite on both the original and the transpiled and renamed Upgradeable contracts. +extendEnvironment(env => { + for (const require of [ + 'require', // Truffle (Deprecated) + 'readArtifact', // Ethers + ]) { + env.artifacts[require] = tryRequireUpgradableVariantsWith(env.artifacts[require]); + } }); From 5e960212c5d62756ab82ef645dbfbe85acba7585 Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Tue, 10 Oct 2023 23:43:39 +0200 Subject: [PATCH 24/82] redesign signers/fixture --- hardhat/env-contract.js | 6 ++++-- test/access/Ownable.test.js | 13 ++++++------- test/access/Ownable2Step.test.js | 15 +++++++-------- 3 files changed, 17 insertions(+), 17 deletions(-) diff --git a/hardhat/env-contract.js b/hardhat/env-contract.js index c615249a3a3..b697277b34b 100644 --- a/hardhat/env-contract.js +++ b/hardhat/env-contract.js @@ -1,4 +1,6 @@ -extendEnvironment(env => { +extendEnvironment(async env => { + const signers = await env.ethers.getSigners(); + const { contract } = env; env.contract = function (name, body) { @@ -19,7 +21,7 @@ extendEnvironment(env => { // remove the default account from the accounts list used in tests, in order // to protect tests against accidentally passing due to the contract // deployer being used subsequently as function caller - body(accounts.slice(1)); + body(accounts.slice(1), signers.slice(1)); }); }; }); diff --git a/test/access/Ownable.test.js b/test/access/Ownable.test.js index f1b6d2f2c53..6b7ad9793b3 100644 --- a/test/access/Ownable.test.js +++ b/test/access/Ownable.test.js @@ -3,16 +3,15 @@ const { expect } = require('chai'); const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers'); async function fixture() { - const accounts = await ethers.getSigners(); // this is slow :/ - const owner = accounts.shift(); - const other = accounts.shift(); - const ownable = await ethers.deployContract('$Ownable', [owner]); - return { accounts, owner, other, ownable }; + this.owner = this.accounts.shift(); + this.other = this.accounts.shift(); + this.ownable = await ethers.deployContract('$Ownable', [this.owner]); } -describe('Ownable', function () { +contract('Ownable', function (_, signers) { beforeEach(async function () { - Object.assign(this, await loadFixture(fixture)); + this.accounts = signers; + await loadFixture(fixture.bind(this)); }); it('rejects zero address for initialOwner', async function () { diff --git a/test/access/Ownable2Step.test.js b/test/access/Ownable2Step.test.js index 868b9739549..e50a67d019d 100644 --- a/test/access/Ownable2Step.test.js +++ b/test/access/Ownable2Step.test.js @@ -3,17 +3,16 @@ const { expect } = require('chai'); const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers'); async function fixture() { - const accounts = await ethers.getSigners(); - const owner = accounts.shift(); - const accountA = accounts.shift(); - const accountB = accounts.shift(); - const ownable2Step = await ethers.deployContract('$Ownable2Step', [owner]); - return { accounts, owner, accountA, accountB, ownable2Step }; + this.owner = this.accounts.shift(); + this.accountA = this.accounts.shift(); + this.accountB = this.accounts.shift(); + this.ownable2Step = await ethers.deployContract('$Ownable2Step', [this.owner]); } -describe('Ownable2Step', function () { +describe('Ownable2Step', function (_, accounts) { beforeEach(async function () { - await loadFixture(fixture).then(results => Object.assign(this, results)); + this.accounts = accounts; + await loadFixture(fixture.bind(this)); }); describe('transfer ownership', function () { From f382329b2f69ea0867254147a7bbff05d4543c56 Mon Sep 17 00:00:00 2001 From: ernestognw Date: Tue, 10 Oct 2023 15:47:36 -0600 Subject: [PATCH 25/82] Fix vanilla tests by overriding require and readArtifact with sync and async functions respectively --- hardhat/env-artifacts.js | 47 ++++++++++++++++++++++++++-------------- 1 file changed, 31 insertions(+), 16 deletions(-) diff --git a/hardhat/env-artifacts.js b/hardhat/env-artifacts.js index cf773f140ca..c378570bebd 100644 --- a/hardhat/env-artifacts.js +++ b/hardhat/env-artifacts.js @@ -1,14 +1,23 @@ const { HardhatError } = require('hardhat/internal/core/errors'); -// Extends a require artifact function to try with the Upgradeable variants. -function tryRequireUpgradableVariantsWith(originalRequire) { - return function (name) { - for (const suffix of ['UpgradeableWithInit', 'Upgradeable', '']) { +function isExpectedError(e, suffix) { + // HH700: Artifact not found - from https://hardhat.org/hardhat-runner/docs/errors#HH700 + return HardhatError.isHardhatError(e) && e.number === 700 && suffix !== ''; +} + +// Modifies the artifact require functions so that instead of X it loads the XUpgradeable contract. +// This allows us to run the same test suite on both the original and the transpiled and renamed Upgradeable contracts. +extendEnvironment(env => { + const suffixes = ['UpgradeableWithInit', 'Upgradeable', '']; + + // Truffe (deprecated) + const originalRequire = env.artifacts.require; + env.artifacts.require = function (name) { + for (const suffix of suffixes) { try { return originalRequire.call(this, name + suffix); } catch (e) { - // HH700: Artifact not found - from https://hardhat.org/hardhat-runner/docs/errors#HH700 - if (HardhatError.isHardhatError(e) && e.number === 700 && suffix !== '') { + if (isExpectedError(e, suffix)) { continue; } else { throw e; @@ -17,15 +26,21 @@ function tryRequireUpgradableVariantsWith(originalRequire) { } throw new Error('Unreachable'); }; -} -// Modifies the artifact require functions so that instead of X it loads the XUpgradeable contract. -// This allows us to run the same test suite on both the original and the transpiled and renamed Upgradeable contracts. -extendEnvironment(env => { - for (const require of [ - 'require', // Truffle (Deprecated) - 'readArtifact', // Ethers - ]) { - env.artifacts[require] = tryRequireUpgradableVariantsWith(env.artifacts[require]); - } + // Ethers + const originalReadArtifact = env.artifacts.readArtifact; + env.artifacts.readArtifact = async function (name) { + for (const suffix of suffixes) { + try { + return await originalReadArtifact.call(this, name + suffix); + } catch (e) { + if (isExpectedError(e, suffix)) { + continue; + } else { + throw e; + } + } + } + throw new Error('Unreachable'); + }; }); From 95be46d54647d9d4636cda8baeabe8d717e911a9 Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Tue, 10 Oct 2023 23:52:04 +0200 Subject: [PATCH 26/82] Revert "redesign signers/fixture" This reverts commit 5e960212c5d62756ab82ef645dbfbe85acba7585. --- hardhat/env-contract.js | 6 ++---- test/access/Ownable.test.js | 13 +++++++------ test/access/Ownable2Step.test.js | 15 ++++++++------- 3 files changed, 17 insertions(+), 17 deletions(-) diff --git a/hardhat/env-contract.js b/hardhat/env-contract.js index b697277b34b..c615249a3a3 100644 --- a/hardhat/env-contract.js +++ b/hardhat/env-contract.js @@ -1,6 +1,4 @@ -extendEnvironment(async env => { - const signers = await env.ethers.getSigners(); - +extendEnvironment(env => { const { contract } = env; env.contract = function (name, body) { @@ -21,7 +19,7 @@ extendEnvironment(async env => { // remove the default account from the accounts list used in tests, in order // to protect tests against accidentally passing due to the contract // deployer being used subsequently as function caller - body(accounts.slice(1), signers.slice(1)); + body(accounts.slice(1)); }); }; }); diff --git a/test/access/Ownable.test.js b/test/access/Ownable.test.js index 6b7ad9793b3..f1b6d2f2c53 100644 --- a/test/access/Ownable.test.js +++ b/test/access/Ownable.test.js @@ -3,15 +3,16 @@ const { expect } = require('chai'); const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers'); async function fixture() { - this.owner = this.accounts.shift(); - this.other = this.accounts.shift(); - this.ownable = await ethers.deployContract('$Ownable', [this.owner]); + const accounts = await ethers.getSigners(); // this is slow :/ + const owner = accounts.shift(); + const other = accounts.shift(); + const ownable = await ethers.deployContract('$Ownable', [owner]); + return { accounts, owner, other, ownable }; } -contract('Ownable', function (_, signers) { +describe('Ownable', function () { beforeEach(async function () { - this.accounts = signers; - await loadFixture(fixture.bind(this)); + Object.assign(this, await loadFixture(fixture)); }); it('rejects zero address for initialOwner', async function () { diff --git a/test/access/Ownable2Step.test.js b/test/access/Ownable2Step.test.js index e50a67d019d..868b9739549 100644 --- a/test/access/Ownable2Step.test.js +++ b/test/access/Ownable2Step.test.js @@ -3,16 +3,17 @@ const { expect } = require('chai'); const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers'); async function fixture() { - this.owner = this.accounts.shift(); - this.accountA = this.accounts.shift(); - this.accountB = this.accounts.shift(); - this.ownable2Step = await ethers.deployContract('$Ownable2Step', [this.owner]); + const accounts = await ethers.getSigners(); + const owner = accounts.shift(); + const accountA = accounts.shift(); + const accountB = accounts.shift(); + const ownable2Step = await ethers.deployContract('$Ownable2Step', [owner]); + return { accounts, owner, accountA, accountB, ownable2Step }; } -describe('Ownable2Step', function (_, accounts) { +describe('Ownable2Step', function () { beforeEach(async function () { - this.accounts = accounts; - await loadFixture(fixture.bind(this)); + await loadFixture(fixture).then(results => Object.assign(this, results)); }); describe('transfer ownership', function () { From 55b040636d68e12358a5655001035dc238f68ef3 Mon Sep 17 00:00:00 2001 From: ernestognw Date: Tue, 10 Oct 2023 17:24:21 -0600 Subject: [PATCH 27/82] Optimize ethers.getSigners call by passing a promise to use within fixtures --- hardhat/env-contract.js | 11 +++++++---- test/access/Ownable.test.js | 14 ++++++-------- test/access/Ownable2Step.test.js | 24 +++++++++++++----------- 3 files changed, 26 insertions(+), 23 deletions(-) diff --git a/hardhat/env-contract.js b/hardhat/env-contract.js index c615249a3a3..21f7033dbf3 100644 --- a/hardhat/env-contract.js +++ b/hardhat/env-contract.js @@ -1,12 +1,18 @@ extendEnvironment(env => { const { contract } = env; + const signers = env.ethers.getSigners(); + env.contract = function (name, body) { const { takeSnapshot } = require('@nomicfoundation/hardhat-network-helpers'); contract(name, accounts => { // reset the state of the chain in between contract test suites let snapshot; + // remove the default account from the accounts list used in tests, in order + // to protect tests against accidentally passing due to the contract + // deployer being used subsequently as function caller + const filteredAccounts = accounts.slice(1); before(async function () { snapshot = await takeSnapshot(); @@ -16,10 +22,7 @@ extendEnvironment(env => { await snapshot.restore(); }); - // remove the default account from the accounts list used in tests, in order - // to protect tests against accidentally passing due to the contract - // deployer being used subsequently as function caller - body(accounts.slice(1)); + body(filteredAccounts, signers); }); }; }); diff --git a/test/access/Ownable.test.js b/test/access/Ownable.test.js index f1b6d2f2c53..004ef64a5ff 100644 --- a/test/access/Ownable.test.js +++ b/test/access/Ownable.test.js @@ -2,15 +2,13 @@ const { ethers } = require('hardhat'); const { expect } = require('chai'); const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers'); -async function fixture() { - const accounts = await ethers.getSigners(); // this is slow :/ - const owner = accounts.shift(); - const other = accounts.shift(); - const ownable = await ethers.deployContract('$Ownable', [owner]); - return { accounts, owner, other, ownable }; -} +contract('Ownable', function (_, signers) { + async function fixture() { + const [owner, other] = await signers; + const ownable = await ethers.deployContract('$Ownable', [owner]); + return { owner, other, ownable }; + } -describe('Ownable', function () { beforeEach(async function () { Object.assign(this, await loadFixture(fixture)); }); diff --git a/test/access/Ownable2Step.test.js b/test/access/Ownable2Step.test.js index 868b9739549..37e607fdc5d 100644 --- a/test/access/Ownable2Step.test.js +++ b/test/access/Ownable2Step.test.js @@ -2,18 +2,20 @@ const { ethers } = require('hardhat'); const { expect } = require('chai'); const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers'); -async function fixture() { - const accounts = await ethers.getSigners(); - const owner = accounts.shift(); - const accountA = accounts.shift(); - const accountB = accounts.shift(); - const ownable2Step = await ethers.deployContract('$Ownable2Step', [owner]); - return { accounts, owner, accountA, accountB, ownable2Step }; -} - -describe('Ownable2Step', function () { +contract('Ownable2Step', function (_, signers) { + async function fixture() { + const [owner, accountA, accountB] = await signers; + const ownable2Step = await ethers.deployContract('$Ownable2Step', [owner]); + return { + ownable2Step, + owner, + accountA, + accountB, + }; + } + beforeEach(async function () { - await loadFixture(fixture).then(results => Object.assign(this, results)); + Object.assign(this, await loadFixture(fixture)); }); describe('transfer ownership', function () { From e45e482f306e5c0b625d53dcbc27c1a39c77bdd8 Mon Sep 17 00:00:00 2001 From: ernestognw Date: Tue, 10 Oct 2023 21:04:29 -0600 Subject: [PATCH 28/82] Slice ethers accounts --- hardhat/env-contract.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/hardhat/env-contract.js b/hardhat/env-contract.js index 21f7033dbf3..5b062729225 100644 --- a/hardhat/env-contract.js +++ b/hardhat/env-contract.js @@ -13,6 +13,7 @@ extendEnvironment(env => { // to protect tests against accidentally passing due to the contract // deployer being used subsequently as function caller const filteredAccounts = accounts.slice(1); + const filteredSigners = signers.then(signers => signers.slice(1)); before(async function () { snapshot = await takeSnapshot(); @@ -22,7 +23,7 @@ extendEnvironment(env => { await snapshot.restore(); }); - body(filteredAccounts, signers); + body(filteredAccounts, filteredSigners); }); }; }); From b391c2fc8f660ebe7c28a0116723c7b91d37ed08 Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Wed, 11 Oct 2023 15:26:46 +0200 Subject: [PATCH 29/82] rename --- test/access/Ownable.test.js | 4 ++-- test/access/Ownable2Step.test.js | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/test/access/Ownable.test.js b/test/access/Ownable.test.js index 004ef64a5ff..4800443e41f 100644 --- a/test/access/Ownable.test.js +++ b/test/access/Ownable.test.js @@ -2,9 +2,9 @@ const { ethers } = require('hardhat'); const { expect } = require('chai'); const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers'); -contract('Ownable', function (_, signers) { +contract('Ownable', function (_, signersAsPromise) { async function fixture() { - const [owner, other] = await signers; + const [owner, other] = await signersAsPromise; const ownable = await ethers.deployContract('$Ownable', [owner]); return { owner, other, ownable }; } diff --git a/test/access/Ownable2Step.test.js b/test/access/Ownable2Step.test.js index 37e607fdc5d..1f6840e89c6 100644 --- a/test/access/Ownable2Step.test.js +++ b/test/access/Ownable2Step.test.js @@ -2,9 +2,9 @@ const { ethers } = require('hardhat'); const { expect } = require('chai'); const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers'); -contract('Ownable2Step', function (_, signers) { +contract('Ownable2Step', function (_, signersAsPromise) { async function fixture() { - const [owner, accountA, accountB] = await signers; + const [owner, accountA, accountB] = await signersAsPromise; const ownable2Step = await ethers.deployContract('$Ownable2Step', [owner]); return { ownable2Step, From 74bab8140dececbb72b827ca8fa75e96437c9f49 Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Wed, 11 Oct 2023 17:13:13 +0200 Subject: [PATCH 30/82] override hre.ethers.getSigners --- hardhat/env-contract.js | 30 ++++++++++++++++++++---------- 1 file changed, 20 insertions(+), 10 deletions(-) diff --git a/hardhat/env-contract.js b/hardhat/env-contract.js index 5b062729225..e39b2a9d4ad 100644 --- a/hardhat/env-contract.js +++ b/hardhat/env-contract.js @@ -1,19 +1,29 @@ -extendEnvironment(env => { - const { contract } = env; +// Remove the default account from the accounts list used in tests, in order +// to protect tests against accidentally passing due to the contract +// deployer being used subsequently as function caller +// +// This operation affects: +// - the accounts (and signersAsPromise) parameters of `contract` blocks +// - the return of hre.ethers.getSigners() +extendEnvironment(hre => { + // cache old version + const { contract } = hre; + const { getSigners } = hre.ethers; - const signers = env.ethers.getSigners(); + // cache the signer list, so that its resolved only once. + const filteredSignersAsPromise = getSigners().then(signers => signers.slice(1)); - env.contract = function (name, body) { + // override hre.ethers.getSigner() + hre.ethers.getSigners = () => filteredSignersAsPromise; + + // override hre.contract + hre.contract = (name, body) => { const { takeSnapshot } = require('@nomicfoundation/hardhat-network-helpers'); contract(name, accounts => { // reset the state of the chain in between contract test suites + // TODO: this should be removed when migration to ethers is over let snapshot; - // remove the default account from the accounts list used in tests, in order - // to protect tests against accidentally passing due to the contract - // deployer being used subsequently as function caller - const filteredAccounts = accounts.slice(1); - const filteredSigners = signers.then(signers => signers.slice(1)); before(async function () { snapshot = await takeSnapshot(); @@ -23,7 +33,7 @@ extendEnvironment(env => { await snapshot.restore(); }); - body(filteredAccounts, filteredSigners); + body(accounts.slice(1), filteredSignersAsPromise); }); }; }); From 98e5ecde6e5149ea555888b6ad8b5ee62353cdbd Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Wed, 11 Oct 2023 17:52:45 +0200 Subject: [PATCH 31/82] unify coding style/naming between env-contracts.js and env-artifacts.js --- hardhat/env-artifacts.js | 10 +++++----- hardhat/env-contract.js | 23 ++++++++++------------- 2 files changed, 15 insertions(+), 18 deletions(-) diff --git a/hardhat/env-artifacts.js b/hardhat/env-artifacts.js index c378570bebd..4cda9387cc3 100644 --- a/hardhat/env-artifacts.js +++ b/hardhat/env-artifacts.js @@ -7,12 +7,12 @@ function isExpectedError(e, suffix) { // Modifies the artifact require functions so that instead of X it loads the XUpgradeable contract. // This allows us to run the same test suite on both the original and the transpiled and renamed Upgradeable contracts. -extendEnvironment(env => { +extendEnvironment(hre => { const suffixes = ['UpgradeableWithInit', 'Upgradeable', '']; // Truffe (deprecated) - const originalRequire = env.artifacts.require; - env.artifacts.require = function (name) { + const originalRequire = hre.artifacts.require; + hre.artifacts.require = function (name) { for (const suffix of suffixes) { try { return originalRequire.call(this, name + suffix); @@ -28,8 +28,8 @@ extendEnvironment(env => { }; // Ethers - const originalReadArtifact = env.artifacts.readArtifact; - env.artifacts.readArtifact = async function (name) { + const originalReadArtifact = hre.artifacts.readArtifact; + hre.artifacts.readArtifact = async function (name) { for (const suffix of suffixes) { try { return await originalReadArtifact.call(this, name + suffix); diff --git a/hardhat/env-contract.js b/hardhat/env-contract.js index e39b2a9d4ad..2bbafacec85 100644 --- a/hardhat/env-contract.js +++ b/hardhat/env-contract.js @@ -6,30 +6,27 @@ // - the accounts (and signersAsPromise) parameters of `contract` blocks // - the return of hre.ethers.getSigners() extendEnvironment(hre => { - // cache old version - const { contract } = hre; - const { getSigners } = hre.ethers; - - // cache the signer list, so that its resolved only once. - const filteredSignersAsPromise = getSigners().then(signers => signers.slice(1)); - // override hre.ethers.getSigner() + const originalGetSigners = hre.ethers.getSigners; + const filteredSignersAsPromise = originalGetSigners().then(signers => signers.slice(1)); hre.ethers.getSigners = () => filteredSignersAsPromise; // override hre.contract - hre.contract = (name, body) => { - const { takeSnapshot } = require('@nomicfoundation/hardhat-network-helpers'); - - contract(name, accounts => { - // reset the state of the chain in between contract test suites - // TODO: this should be removed when migration to ethers is over + const originalContract = hre.contract; + hre.contract = function (name, body) { + originalContract.call(this, name, accounts => { let snapshot; before(async function () { + // reset the state of the chain in between contract test suites + // TODO: this should be removed when migration to ethers is over + const { takeSnapshot } = require('@nomicfoundation/hardhat-network-helpers'); snapshot = await takeSnapshot(); }); after(async function () { + // reset the state of the chain in between contract test suites + // TODO: this should be removed when migration to ethers is over await snapshot.restore(); }); From 4db18006bf115e19441aded08555ea2f0a985762 Mon Sep 17 00:00:00 2001 From: ernestognw Date: Wed, 11 Oct 2023 23:47:32 -0600 Subject: [PATCH 32/82] Attempt to fix tests by avoid overriding `this` in Truffle require --- hardhat/env-artifacts.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hardhat/env-artifacts.js b/hardhat/env-artifacts.js index 4cda9387cc3..9bd99d33309 100644 --- a/hardhat/env-artifacts.js +++ b/hardhat/env-artifacts.js @@ -15,7 +15,7 @@ extendEnvironment(hre => { hre.artifacts.require = function (name) { for (const suffix of suffixes) { try { - return originalRequire.call(this, name + suffix); + return originalRequire(name + suffix); } catch (e) { if (isExpectedError(e, suffix)) { continue; From 0a8a69d6de065de12171a8ff294ad6bc3d959f29 Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Thu, 12 Oct 2023 11:30:48 +0200 Subject: [PATCH 33/82] Revert "Attempt to fix tests by avoid overriding `this` in Truffle require" This reverts commit 4db18006bf115e19441aded08555ea2f0a985762. --- hardhat/env-artifacts.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hardhat/env-artifacts.js b/hardhat/env-artifacts.js index 9bd99d33309..4cda9387cc3 100644 --- a/hardhat/env-artifacts.js +++ b/hardhat/env-artifacts.js @@ -15,7 +15,7 @@ extendEnvironment(hre => { hre.artifacts.require = function (name) { for (const suffix of suffixes) { try { - return originalRequire(name + suffix); + return originalRequire.call(this, name + suffix); } catch (e) { if (isExpectedError(e, suffix)) { continue; From 5d5a1500340728cfb8133a4f92f1defdcd3c9597 Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Thu, 12 Oct 2023 13:55:17 +0200 Subject: [PATCH 34/82] Force compile on upgradeable and coverage workflows --- .github/workflows/checks.yml | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml index 17dfb669d1b..9951280b74e 100644 --- a/.github/workflows/checks.yml +++ b/.github/workflows/checks.yml @@ -64,6 +64,8 @@ jobs: cp -rnT contracts lib/openzeppelin-contracts/contracts - name: Transpile to upgradeable run: bash scripts/upgradeable/transpile.sh + - name: Compile contracts # TODO: Remove after migrating tests to ethers + run: npm run compile - name: Run tests run: npm run test - name: Check linearisation of the inheritance graph @@ -92,7 +94,10 @@ jobs: - uses: actions/checkout@v4 - name: Set up environment uses: ./.github/actions/setup - - run: npm run coverage + - name: Compile contracts # TODO: Remove after migrating tests to ethers + run: npm run compile + - name: Run coverage + run: npm run coverage - uses: codecov/codecov-action@v3 with: token: ${{ secrets.CODECOV_TOKEN }} From 300fa1ee1e4c760f05bf184840e496e7c2e3ede7 Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Fri, 13 Oct 2023 15:49:34 +0200 Subject: [PATCH 35/82] use cached getSigners() in fixtures --- hardhat.config.js | 4 ++-- hardhat/env-contract.js | 3 ++- test/access/Ownable.test.js | 12 ++++++------ test/access/Ownable2Step.test.js | 24 ++++++++++++------------ 4 files changed, 22 insertions(+), 21 deletions(-) diff --git a/hardhat.config.js b/hardhat.config.js index b15fb282edb..182d6f3782f 100644 --- a/hardhat.config.js +++ b/hardhat.config.js @@ -71,7 +71,7 @@ for (const f of fs.readdirSync(path.join(__dirname, 'hardhat'))) { require(path.join(__dirname, 'hardhat', f)); } -const withOptimizations = argv.gas || argv.compileMode === 'production'; +const withOptimizations = argv.gas || argv.coverage || argv.compileMode === 'production'; /** * @type import('hardhat/config').HardhatUserConfig @@ -101,7 +101,7 @@ module.exports = { }, networks: { hardhat: { - blockGasLimit: 10000000, + blockGasLimit: !argv.coverage ? 30_000_000 : 1_000_000_000_000_000, allowUnlimitedContractSize: !withOptimizations, }, }, diff --git a/hardhat/env-contract.js b/hardhat/env-contract.js index 2bbafacec85..d4e304c2814 100644 --- a/hardhat/env-contract.js +++ b/hardhat/env-contract.js @@ -7,6 +7,7 @@ // - the return of hre.ethers.getSigners() extendEnvironment(hre => { // override hre.ethers.getSigner() + // note that we don't just discard the first signer, we also cache the value to improve speed. const originalGetSigners = hre.ethers.getSigners; const filteredSignersAsPromise = originalGetSigners().then(signers => signers.slice(1)); hre.ethers.getSigners = () => filteredSignersAsPromise; @@ -30,7 +31,7 @@ extendEnvironment(hre => { await snapshot.restore(); }); - body(accounts.slice(1), filteredSignersAsPromise); + body(accounts.slice(1)); }); }; }); diff --git a/test/access/Ownable.test.js b/test/access/Ownable.test.js index 4800443e41f..26b6fa3db8f 100644 --- a/test/access/Ownable.test.js +++ b/test/access/Ownable.test.js @@ -2,13 +2,13 @@ const { ethers } = require('hardhat'); const { expect } = require('chai'); const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers'); -contract('Ownable', function (_, signersAsPromise) { - async function fixture() { - const [owner, other] = await signersAsPromise; - const ownable = await ethers.deployContract('$Ownable', [owner]); - return { owner, other, ownable }; - } +async function fixture() { + const [owner, other] = await ethers.getSigners(); + const ownable = await ethers.deployContract('$Ownable', [owner]); + return { owner, other, ownable }; +} +contract('Ownable', function () { beforeEach(async function () { Object.assign(this, await loadFixture(fixture)); }); diff --git a/test/access/Ownable2Step.test.js b/test/access/Ownable2Step.test.js index 1f6840e89c6..db712d88849 100644 --- a/test/access/Ownable2Step.test.js +++ b/test/access/Ownable2Step.test.js @@ -2,18 +2,18 @@ const { ethers } = require('hardhat'); const { expect } = require('chai'); const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers'); -contract('Ownable2Step', function (_, signersAsPromise) { - async function fixture() { - const [owner, accountA, accountB] = await signersAsPromise; - const ownable2Step = await ethers.deployContract('$Ownable2Step', [owner]); - return { - ownable2Step, - owner, - accountA, - accountB, - }; - } - +async function fixture() { + const [owner, accountA, accountB] = await ethers.getSigners(); + const ownable2Step = await ethers.deployContract('$Ownable2Step', [owner]); + return { + ownable2Step, + owner, + accountA, + accountB, + }; +} + +contract('Ownable2Step', function () { beforeEach(async function () { Object.assign(this, await loadFixture(fixture)); }); From ae4381efe51a02f8febdec57c2826c8a4ef6830d Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Fri, 13 Oct 2023 18:45:08 +0200 Subject: [PATCH 36/82] use describe instead of contract for ethers test that don't need the snapshot --- test/access/Ownable.test.js | 2 +- test/access/Ownable2Step.test.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/test/access/Ownable.test.js b/test/access/Ownable.test.js index 26b6fa3db8f..fabcb7d52d7 100644 --- a/test/access/Ownable.test.js +++ b/test/access/Ownable.test.js @@ -8,7 +8,7 @@ async function fixture() { return { owner, other, ownable }; } -contract('Ownable', function () { +describe('Ownable', function () { beforeEach(async function () { Object.assign(this, await loadFixture(fixture)); }); diff --git a/test/access/Ownable2Step.test.js b/test/access/Ownable2Step.test.js index db712d88849..e77307d9859 100644 --- a/test/access/Ownable2Step.test.js +++ b/test/access/Ownable2Step.test.js @@ -13,7 +13,7 @@ async function fixture() { }; } -contract('Ownable2Step', function () { +describe('Ownable2Step', function () { beforeEach(async function () { Object.assign(this, await loadFixture(fixture)); }); From b02770efa02221dda3e37c463a8b5ac3f451e0f0 Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Mon, 16 Oct 2023 12:32:11 +0200 Subject: [PATCH 37/82] add enviornment sanity check --- test/sanity.test.js | 38 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 test/sanity.test.js diff --git a/test/sanity.test.js b/test/sanity.test.js new file mode 100644 index 00000000000..c15f469c6a0 --- /dev/null +++ b/test/sanity.test.js @@ -0,0 +1,38 @@ +const { ethers } = require('hardhat'); +const { expect } = require('chai'); +const { loadFixture, mine } = require('@nomicfoundation/hardhat-network-helpers'); + +async function fixture() { + const signers = await ethers.getSigners(); + const addresses = await Promise.all(signers.map(s => s.getAddress())); + return { signers, addresses } +} + +contract('Environment sanity', function (accounts) { + beforeEach(async function () { + Object.assign(this, await loadFixture(fixture)); + }); + + describe('signers', function () { + it('match accounts', async function () { + expect(this.addresses).to.deep.equal(accounts); + }); + + it('signer #0 is skipped', async function () { + const signer = await ethers.provider.getSigner(0); + expect(this.addresses).to.not.include(await signer.getAddress()); + }); + }); + + describe('snapshot', function () { + it('cache and mine', async function () { + this.cache = await ethers.provider.getBlockNumber(); + await mine(); + expect(await ethers.provider.getBlockNumber()).to.be.equal(this.cache + 1); + }); + + it('check snapshot', async function () { + expect(await ethers.provider.getBlockNumber()).to.be.equal(this.cache); + }); + }); +}); From 4e6a1cada000ea6ecf13ab095bf3cf20358cec3a Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Mon, 16 Oct 2023 13:48:12 +0200 Subject: [PATCH 38/82] skip signer slice when running coverage --- hardhat/env-contract.js | 14 +++++++++----- test/sanity.test.js | 2 +- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/hardhat/env-contract.js b/hardhat/env-contract.js index d4e304c2814..06d4f187d6f 100644 --- a/hardhat/env-contract.js +++ b/hardhat/env-contract.js @@ -6,11 +6,15 @@ // - the accounts (and signersAsPromise) parameters of `contract` blocks // - the return of hre.ethers.getSigners() extendEnvironment(hre => { - // override hre.ethers.getSigner() - // note that we don't just discard the first signer, we also cache the value to improve speed. - const originalGetSigners = hre.ethers.getSigners; - const filteredSignersAsPromise = originalGetSigners().then(signers => signers.slice(1)); - hre.ethers.getSigners = () => filteredSignersAsPromise; + // TODO: replace with a mocha root hook. + // (see https://github.com/sc-forks/solidity-coverage/issues/819#issuecomment-1762963679) + if (!process.env.COVERAGE) { + // override hre.ethers.getSigner() + // note that we don't just discard the first signer, we also cache the value to improve speed. + const originalGetSigners = hre.ethers.getSigners; + const filteredSignersAsPromise = originalGetSigners().then(signers => signers.slice(1)); + hre.ethers.getSigners = () => filteredSignersAsPromise; + } // override hre.contract const originalContract = hre.contract; diff --git a/test/sanity.test.js b/test/sanity.test.js index c15f469c6a0..323d7511a04 100644 --- a/test/sanity.test.js +++ b/test/sanity.test.js @@ -13,7 +13,7 @@ contract('Environment sanity', function (accounts) { Object.assign(this, await loadFixture(fixture)); }); - describe('signers', function () { + describe('[skip-on-coverage] signers', function () { it('match accounts', async function () { expect(this.addresses).to.deep.equal(accounts); }); From 69b91a0c54b892bd5c4a51ebe2e7ded8ba1873c3 Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Mon, 16 Oct 2023 16:00:14 +0200 Subject: [PATCH 39/82] up --- hardhat.config.js | 1 - test/sanity.test.js | 10 ++++++---- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/hardhat.config.js b/hardhat.config.js index 182d6f3782f..3102cfda599 100644 --- a/hardhat.config.js +++ b/hardhat.config.js @@ -101,7 +101,6 @@ module.exports = { }, networks: { hardhat: { - blockGasLimit: !argv.coverage ? 30_000_000 : 1_000_000_000_000_000, allowUnlimitedContractSize: !withOptimizations, }, }, diff --git a/test/sanity.test.js b/test/sanity.test.js index 323d7511a04..9728b14e41a 100644 --- a/test/sanity.test.js +++ b/test/sanity.test.js @@ -5,7 +5,7 @@ const { loadFixture, mine } = require('@nomicfoundation/hardhat-network-helpers' async function fixture() { const signers = await ethers.getSigners(); const addresses = await Promise.all(signers.map(s => s.getAddress())); - return { signers, addresses } + return { signers, addresses }; } contract('Environment sanity', function (accounts) { @@ -25,14 +25,16 @@ contract('Environment sanity', function (accounts) { }); describe('snapshot', function () { + let blockNumberBefore; + it('cache and mine', async function () { - this.cache = await ethers.provider.getBlockNumber(); + blockNumberBefore = await ethers.provider.getBlockNumber(); await mine(); - expect(await ethers.provider.getBlockNumber()).to.be.equal(this.cache + 1); + expect(await ethers.provider.getBlockNumber()).to.be.equal(blockNumberBefore + 1); }); it('check snapshot', async function () { - expect(await ethers.provider.getBlockNumber()).to.be.equal(this.cache); + expect(await ethers.provider.getBlockNumber()).to.be.equal(blockNumberBefore); }); }); }); From 5162e98334a1989b4e145d75ea353d8e201e9be3 Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Tue, 17 Oct 2023 09:00:16 +0200 Subject: [PATCH 40/82] Move non standardized error definition to the implementation file For consistency with the other files/extensions --- contracts/interfaces/IERC1363Errors.sol | 33 -------------------- contracts/token/ERC20/extensions/ERC1363.sol | 19 ++++++++--- 2 files changed, 15 insertions(+), 37 deletions(-) delete mode 100644 contracts/interfaces/IERC1363Errors.sol diff --git a/contracts/interfaces/IERC1363Errors.sol b/contracts/interfaces/IERC1363Errors.sol deleted file mode 100644 index fa324461733..00000000000 --- a/contracts/interfaces/IERC1363Errors.sol +++ /dev/null @@ -1,33 +0,0 @@ -// SPDX-License-Identifier: MIT - -pragma solidity ^0.8.20; - -/** - * @title IERC1363Errors - * @dev Interface of the ERC1363 custom errors following the https://eips.ethereum.org/EIPS/eip-6093[ERC-6093] rationale. - */ -interface IERC1363Errors { - /** - * @dev Indicates a failure with the token `receiver` as it can't be an EOA. Used in transfers. - * @param receiver Address to which tokens are being transferred. - */ - error ERC1363EOAReceiver(address receiver); - - /** - * @dev Indicates a failure with the token `spender` as it can't be an EOA. Used in approvals. - * @param spender Address that may be allowed to operate on tokens without being their owner. - */ - error ERC1363EOASpender(address spender); - - /** - * @dev Indicates a failure with the token `receiver`. Used in transfers. - * @param receiver Address to which tokens are being transferred. - */ - error ERC1363InvalidReceiver(address receiver); - - /** - * @dev Indicates a failure with the token `spender`. Used in approvals. - * @param spender Address that may be allowed to operate on tokens without being their owner. - */ - error ERC1363InvalidSpender(address spender); -} diff --git a/contracts/token/ERC20/extensions/ERC1363.sol b/contracts/token/ERC20/extensions/ERC1363.sol index 522693f8274..5770543338c 100644 --- a/contracts/token/ERC20/extensions/ERC1363.sol +++ b/contracts/token/ERC20/extensions/ERC1363.sol @@ -6,7 +6,6 @@ import {ERC20} from "../ERC20.sol"; import {IERC165, ERC165} from "../../../utils/introspection/ERC165.sol"; import {IERC1363} from "../../../interfaces/IERC1363.sol"; -import {IERC1363Errors} from "../../../interfaces/IERC1363Errors.sol"; import {IERC1363Receiver} from "../../../interfaces/IERC1363Receiver.sol"; import {IERC1363Spender} from "../../../interfaces/IERC1363Spender.sol"; @@ -14,7 +13,19 @@ import {IERC1363Spender} from "../../../interfaces/IERC1363Spender.sol"; * @title ERC1363 * @dev Implementation of the ERC1363 interface. */ -abstract contract ERC1363 is ERC20, ERC165, IERC1363, IERC1363Errors { +abstract contract ERC1363 is ERC20, ERC165, IERC1363 { + /** + * @dev Indicates a failure with the token `receiver`. Used in transfers. + * @param receiver Address to which tokens are being transferred. + */ + error ERC1363InvalidReceiver(address receiver); + + /** + * @dev Indicates a failure with the token `spender`. Used in approvals. + * @param spender Address that may be allowed to operate on tokens without being their owner. + */ + error ERC1363InvalidSpender(address spender); + /** * @inheritdoc IERC165 */ @@ -88,7 +99,7 @@ abstract contract ERC1363 is ERC20, ERC165, IERC1363, IERC1363Errors { */ function _checkOnTransferReceived(address from, address to, uint256 value, bytes memory data) private { if (to.code.length == 0) { - revert ERC1363EOAReceiver(to); + revert ERC1363InvalidReceiver(to); } try IERC1363Receiver(to).onTransferReceived(_msgSender(), from, value, data) returns (bytes4 retval) { @@ -119,7 +130,7 @@ abstract contract ERC1363 is ERC20, ERC165, IERC1363, IERC1363Errors { */ function _checkOnApprovalReceived(address spender, uint256 value, bytes memory data) private { if (spender.code.length == 0) { - revert ERC1363EOASpender(spender); + revert ERC1363InvalidSpender(spender); } try IERC1363Spender(spender).onApprovalReceived(_msgSender(), value, data) returns (bytes4 retval) { From 266893471d54213baaa66f0aa5e94a24d22512fe Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Tue, 17 Oct 2023 09:01:06 +0200 Subject: [PATCH 41/82] Add "relaxed" helpers for ERC1363 in SafeERC20 --- contracts/token/ERC20/utils/SafeERC20.sol | 41 ++++++++++++++++++++++- 1 file changed, 40 insertions(+), 1 deletion(-) diff --git a/contracts/token/ERC20/utils/SafeERC20.sol b/contracts/token/ERC20/utils/SafeERC20.sol index bb65709b46b..61acac1b2ed 100644 --- a/contracts/token/ERC20/utils/SafeERC20.sol +++ b/contracts/token/ERC20/utils/SafeERC20.sol @@ -4,7 +4,7 @@ pragma solidity ^0.8.20; import {IERC20} from "../IERC20.sol"; -import {IERC20Permit} from "../extensions/IERC20Permit.sol"; +import {IERC1363} from "../../../interfaces/IERC1363.sol"; import {Address} from "../../../utils/Address.sol"; /** @@ -82,6 +82,45 @@ library SafeERC20 { } } + /** + * @dev Perform an {ERC1363} transferAndCall, with a fallback to the simple {ERC20} transfer if the target has no + * code. This can be used to implement an {ERC721}-like safe transfer that rely on {ERC1363} checks when + * targetting contracts. + */ + function transferAndCallRelaxed(IERC1363 token, address to, uint256 value, bytes memory data) internal { + if (to.code.length == 0) { + token.transfer(to, value); + } else { + token.transferAndCall(to, value, data); + } + } + + /** + * @dev Perform an {ERC1363} transferFromAndCall, with a fallback to the simple {ERC20} transferFrom if the target + * has no code. This can be used to implement an {ERC721}-like safe transfer that rely on {ERC1363} checks when + * targetting contracts. + */ + function transferFromAndCallRelaxed(IERC1363 token, address from, address to, uint256 value, bytes memory data) internal { + if (to.code.length == 0) { + token.transferFrom(from, to, value); + } else { + token.transferFromAndCall(from, to, value, data); + } + } + + /** + * @dev Perform an {ERC1363} approveAndCall, with a fallback to the simple {ERC20} approve if the target has no + * code. This can be used to implement an {ERC721}-like safe transfer that rely on {ERC1363} checks when + * targetting contracts. + */ + function approveAndCallRelaxed(IERC1363 token, address to, uint256 value, bytes memory data) internal { + if (to.code.length == 0) { + token.approve(to, value); + } else { + token.approveAndCall(to, value, data); + } + } + /** * @dev Imitates a Solidity high-level call (i.e. a regular function call to a contract), relaxing the requirement * on the return value: the return value is optional (but if data is returned, it must not be false). From f269fa770fa668c4acb491470d974048f026e8f5 Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Tue, 17 Oct 2023 10:04:11 +0200 Subject: [PATCH 42/82] rewrite tests using ethers --- contracts/mocks/token/ERC1363ReceiverMock.sol | 15 +- contracts/mocks/token/ERC1363SpenderMock.sol | 21 +- test/helpers/enums.js | 5 + .../ERC20/extensions/ERC1363.behavior.js | 581 ------------------ test/token/ERC20/extensions/ERC1363.test.js | 216 ++++++- .../SupportsInterface.behavior.js | 2 - 6 files changed, 233 insertions(+), 607 deletions(-) delete mode 100644 test/token/ERC20/extensions/ERC1363.behavior.js diff --git a/contracts/mocks/token/ERC1363ReceiverMock.sol b/contracts/mocks/token/ERC1363ReceiverMock.sol index dc3145415b8..e92f8516504 100644 --- a/contracts/mocks/token/ERC1363ReceiverMock.sol +++ b/contracts/mocks/token/ERC1363ReceiverMock.sol @@ -13,13 +13,18 @@ contract ERC1363ReceiverMock is IERC1363Receiver { Panic } - bytes4 private immutable _retval; - RevertType private immutable _error; + bytes4 private _retval; + RevertType private _error; - event Received(address operator, address from, uint256 value, bytes data, uint256 gas); + event Received(address operator, address from, uint256 value, bytes data); error CustomError(bytes4); - constructor(bytes4 retval, RevertType error) { + constructor() { + _retval = IERC1363Receiver.onTransferReceived.selector; + _error = RevertType.None; + } + + function setUp(bytes4 retval, RevertType error) public { _retval = retval; _error = error; } @@ -41,7 +46,7 @@ contract ERC1363ReceiverMock is IERC1363Receiver { a; } - emit Received(operator, from, value, data, gasleft()); + emit Received(operator, from, value, data); return _retval; } } diff --git a/contracts/mocks/token/ERC1363SpenderMock.sol b/contracts/mocks/token/ERC1363SpenderMock.sol index bc19c39ea0b..c70e54fcc56 100644 --- a/contracts/mocks/token/ERC1363SpenderMock.sol +++ b/contracts/mocks/token/ERC1363SpenderMock.sol @@ -13,18 +13,27 @@ contract ERC1363SpenderMock is IERC1363Spender { Panic } - bytes4 private immutable _retval; - RevertType private immutable _error; + bytes4 private _retval; + RevertType private _error; - event Approved(address owner, uint256 value, bytes data, uint256 gas); + event Approved(address owner, uint256 value, bytes data); error CustomError(bytes4); - constructor(bytes4 retval, RevertType error) { + constructor() { + _retval = IERC1363Spender.onApprovalReceived.selector; + _error = RevertType.None; + } + + function setUp(bytes4 retval, RevertType error) public { _retval = retval; _error = error; } - function onApprovalReceived(address owner, uint256 value, bytes calldata data) public override returns (bytes4) { + function onApprovalReceived( + address owner, + uint256 value, + bytes calldata data + ) public override returns (bytes4) { if (_error == RevertType.RevertWithoutMessage) { revert(); } else if (_error == RevertType.RevertWithMessage) { @@ -36,7 +45,7 @@ contract ERC1363SpenderMock is IERC1363Spender { a; } - emit Approved(owner, value, data, gasleft()); + emit Approved(owner, value, data); return _retval; } } diff --git a/test/helpers/enums.js b/test/helpers/enums.js index 6280e0f319b..86465d4a9c9 100644 --- a/test/helpers/enums.js +++ b/test/helpers/enums.js @@ -2,8 +2,13 @@ function Enum(...options) { return Object.fromEntries(options.map((key, i) => [key, web3.utils.toBN(i)])); } +function Enum2(...options) { + return Object.fromEntries(options.map((key, i) => [key, BigInt(i)])); +} + module.exports = { Enum, + Enum2, ProposalState: Enum('Pending', 'Active', 'Canceled', 'Defeated', 'Succeeded', 'Queued', 'Expired', 'Executed'), VoteType: Enum('Against', 'For', 'Abstain'), Rounding: Enum('Floor', 'Ceil', 'Trunc', 'Expand'), diff --git a/test/token/ERC20/extensions/ERC1363.behavior.js b/test/token/ERC20/extensions/ERC1363.behavior.js deleted file mode 100644 index 0cf101ec4a0..00000000000 --- a/test/token/ERC20/extensions/ERC1363.behavior.js +++ /dev/null @@ -1,581 +0,0 @@ -const { expectRevert, expectEvent } = require('@openzeppelin/test-helpers'); -const { expect } = require('chai'); - -const { shouldSupportInterfaces } = require('../../../utils/introspection/SupportsInterface.behavior'); -const { expectRevertCustomError } = require('../../../helpers/customError'); -const { Enum } = require('../../../helpers/enums'); - -const ERC1363Receiver = artifacts.require('ERC1363ReceiverMock'); -const ERC1363Spender = artifacts.require('ERC1363SpenderMock'); - -const RevertType = Enum('None', 'RevertWithoutMessage', 'RevertWithMessage', 'RevertWithCustomError', 'Panic'); - -function shouldBehaveLikeERC1363(initialSupply, accounts) { - const [owner, spender, recipient] = accounts; - - const RECEIVER_MAGIC_VALUE = '0x88a7ca5c'; - const SPENDER_MAGIC_VALUE = '0x7b04a2d0'; - - shouldSupportInterfaces(['ERC165', 'ERC1363']); - - describe('transfers', function () { - const initialBalance = initialSupply; - const data = '0x42'; - - describe('via transferAndCall', function () { - const transferAndCallWithData = function (to, value, opts) { - return this.token.methods['transferAndCall(address,uint256,bytes)'](to, value, data, opts); - }; - - const transferAndCallWithoutData = function (to, value, opts) { - return this.token.methods['transferAndCall(address,uint256)'](to, value, opts); - }; - - const shouldTransferSafely = function (transferFunction, data) { - describe('to a valid receiver contract', function () { - beforeEach(async function () { - this.receiver = await ERC1363Receiver.new(RECEIVER_MAGIC_VALUE, RevertType.None); - this.to = this.receiver.address; - }); - - it('calls onTransferReceived', async function () { - const receipt = await transferFunction.call(this, this.to, initialBalance, { from: owner }); - - await expectEvent.inTransaction(receipt.tx, ERC1363Receiver, 'Received', { - operator: owner, - from: owner, - value: initialBalance, - data, - }); - }); - }); - }; - - const transferWasSuccessful = function (from, balance) { - let to; - - beforeEach(async function () { - const receiverContract = await ERC1363Receiver.new(RECEIVER_MAGIC_VALUE, RevertType.None); - to = receiverContract.address; - }); - - describe('when the sender does not have enough balance', function () { - const value = balance + 1; - - describe('with data', function () { - it('reverts', async function () { - await expectRevertCustomError( - transferAndCallWithData.call(this, to, value, { from }), - 'ERC20InsufficientBalance', - [from, balance, value], - ); - }); - }); - - describe('without data', function () { - it('reverts', async function () { - await expectRevertCustomError( - transferAndCallWithoutData.call(this, to, value, { from }), - 'ERC20InsufficientBalance', - [from, balance, value], - ); - }); - }); - }); - - describe('when the sender has enough balance', function () { - const value = balance; - - describe('with data', function () { - it('transfers the requested amount', async function () { - await transferAndCallWithData.call(this, to, value, { from }); - - expect(await this.token.balanceOf(from)).to.be.bignumber.equal('0'); - - expect(await this.token.balanceOf(to)).to.be.bignumber.equal(value); - }); - - it('emits a transfer event', async function () { - expectEvent(await transferAndCallWithData.call(this, to, value, { from }), 'Transfer', { - from, - to, - value, - }); - }); - }); - - describe('without data', function () { - it('transfers the requested amount', async function () { - await transferAndCallWithoutData.call(this, to, value, { from }); - - expect(await this.token.balanceOf(from)).to.be.bignumber.equal('0'); - - expect(await this.token.balanceOf(to)).to.be.bignumber.equal(value); - }); - - it('emits a transfer event', async function () { - expectEvent(await transferAndCallWithoutData.call(this, to, value, { from }), 'Transfer', { - from, - to, - value, - }); - }); - }); - }); - }; - - describe('with data', function () { - shouldTransferSafely(transferAndCallWithData, data); - }); - - describe('without data', function () { - shouldTransferSafely(transferAndCallWithoutData, null); - }); - - describe('testing ERC20 behavior', function () { - transferWasSuccessful(owner, initialBalance); - }); - - describe('to a receiver that is not a contract', function () { - it('reverts', async function () { - await expectRevertCustomError( - transferAndCallWithoutData.call(this, recipient, initialBalance, { from: owner }), - 'ERC1363EOAReceiver', - [recipient], - ); - }); - }); - - describe('to a receiver contract returning unexpected value', function () { - it('reverts', async function () { - const invalidReceiver = await ERC1363Receiver.new(data, RevertType.None); - await expectRevertCustomError( - transferAndCallWithoutData.call(this, invalidReceiver.address, initialBalance, { from: owner }), - 'ERC1363InvalidReceiver', - [invalidReceiver.address], - ); - }); - }); - - describe('to a receiver contract that reverts with message', function () { - it('reverts', async function () { - const revertingReceiver = await ERC1363Receiver.new(RECEIVER_MAGIC_VALUE, RevertType.RevertWithMessage); - await expectRevert( - transferAndCallWithoutData.call(this, revertingReceiver.address, initialBalance, { from: owner }), - 'ERC1363ReceiverMock: reverting', - ); - }); - }); - - describe('to a receiver contract that reverts without message', function () { - it('reverts', async function () { - const revertingReceiver = await ERC1363Receiver.new(RECEIVER_MAGIC_VALUE, RevertType.RevertWithoutMessage); - await expectRevertCustomError( - transferAndCallWithoutData.call(this, revertingReceiver.address, initialBalance, { from: owner }), - 'ERC1363InvalidReceiver', - [revertingReceiver.address], - ); - }); - }); - - describe('to a receiver contract that reverts with custom error', function () { - it('reverts', async function () { - const revertingReceiver = await ERC1363Receiver.new(RECEIVER_MAGIC_VALUE, RevertType.RevertWithCustomError); - await expectRevertCustomError( - transferAndCallWithoutData.call(this, revertingReceiver.address, initialBalance, { from: owner }), - 'CustomError', - [RECEIVER_MAGIC_VALUE], - ); - }); - }); - - describe('to a receiver contract that panics', function () { - it('reverts', async function () { - const revertingReceiver = await ERC1363Receiver.new(RECEIVER_MAGIC_VALUE, RevertType.Panic); - await expectRevert.unspecified( - transferAndCallWithoutData.call(this, revertingReceiver.address, initialBalance, { from: owner }), - ); - }); - }); - - describe('to a contract that does not implement the required function', function () { - it('reverts', async function () { - const nonReceiver = this.token; - await expectRevertCustomError( - transferAndCallWithoutData.call(this, nonReceiver.address, initialBalance, { from: owner }), - 'ERC1363InvalidReceiver', - [nonReceiver.address], - ); - }); - }); - }); - - describe('via transferFromAndCall', function () { - beforeEach(async function () { - await this.token.approve(spender, initialBalance, { from: owner }); - }); - - const transferFromAndCallWithData = function (from, to, value, opts) { - return this.token.methods['transferFromAndCall(address,address,uint256,bytes)'](from, to, value, data, opts); - }; - - const transferFromAndCallWithoutData = function (from, to, value, opts) { - return this.token.methods['transferFromAndCall(address,address,uint256)'](from, to, value, opts); - }; - - const shouldTransferFromSafely = function (transferFunction, data) { - describe('to a valid receiver contract', function () { - beforeEach(async function () { - this.receiver = await ERC1363Receiver.new(RECEIVER_MAGIC_VALUE, RevertType.None); - this.to = this.receiver.address; - }); - - it('calls onTransferReceived', async function () { - const receipt = await transferFunction.call(this, owner, this.to, initialBalance, { from: spender }); - - await expectEvent.inTransaction(receipt.tx, ERC1363Receiver, 'Received', { - operator: spender, - from: owner, - value: initialBalance, - data, - }); - }); - }); - }; - - const transferFromWasSuccessful = function (from, spender, balance) { - let to; - - beforeEach(async function () { - const receiverContract = await ERC1363Receiver.new(RECEIVER_MAGIC_VALUE, RevertType.None); - to = receiverContract.address; - }); - - describe('when the sender does not have enough balance', function () { - const value = balance + 1; - - describe('with data', function () { - it('reverts', async function () { - await expectRevertCustomError( - transferFromAndCallWithData.call(this, from, to, value, { from: spender }), - 'ERC20InsufficientAllowance', - [spender, balance, value], - ); - }); - }); - - describe('without data', function () { - it('reverts', async function () { - await expectRevertCustomError( - transferFromAndCallWithoutData.call(this, from, to, value, { from: spender }), - 'ERC20InsufficientAllowance', - [spender, balance, value], - ); - }); - }); - }); - - describe('when the sender has enough balance', function () { - const value = balance; - - describe('with data', function () { - it('transfers the requested amount', async function () { - await transferFromAndCallWithData.call(this, from, to, value, { from: spender }); - - expect(await this.token.balanceOf(from)).to.be.bignumber.equal(''); - - expect(await this.token.balanceOf(to)).to.be.bignumber.equal(value); - }); - - it('emits a transfer event', async function () { - expectEvent( - await transferFromAndCallWithData.call(this, from, to, value, { from: spender }), - 'Transfer', - { from, to, value }, - ); - }); - }); - - describe('without data', function () { - it('transfers the requested amount', async function () { - await transferFromAndCallWithoutData.call(this, from, to, value, { from: spender }); - - expect(await this.token.balanceOf(from)).to.be.bignumber.equal('0'); - - expect(await this.token.balanceOf(to)).to.be.bignumber.equal(value); - }); - - it('emits a transfer event', async function () { - expectEvent( - await transferFromAndCallWithoutData.call(this, from, to, value, { from: spender }), - 'Transfer', - { from, to, value }, - ); - }); - }); - }); - }; - - describe('with data', function () { - shouldTransferFromSafely(transferFromAndCallWithData, data); - }); - - describe('without data', function () { - shouldTransferFromSafely(transferFromAndCallWithoutData, null); - }); - - describe('testing ERC20 behavior', function () { - transferFromWasSuccessful(owner, spender, initialBalance); - }); - - describe('to a receiver that is not a contract', function () { - it('reverts', async function () { - await expectRevertCustomError( - transferFromAndCallWithoutData.call(this, owner, recipient, initialBalance, { from: spender }), - 'ERC1363EOAReceiver', - [recipient], - ); - }); - }); - - describe('to a receiver contract returning unexpected value', function () { - it('reverts', async function () { - const invalidReceiver = await ERC1363Receiver.new(data, RevertType.None); - await expectRevertCustomError( - transferFromAndCallWithoutData.call(this, owner, invalidReceiver.address, initialBalance, { - from: spender, - }), - 'ERC1363InvalidReceiver', - [invalidReceiver.address], - ); - }); - }); - - describe('to a receiver contract that reverts with message', function () { - it('reverts', async function () { - const revertingReceiver = await ERC1363Receiver.new(RECEIVER_MAGIC_VALUE, RevertType.RevertWithMessage); - await expectRevert( - transferFromAndCallWithoutData.call(this, owner, revertingReceiver.address, initialBalance, { - from: spender, - }), - 'ERC1363ReceiverMock: reverting', - ); - }); - }); - - describe('to a receiver contract that reverts without message', function () { - it('reverts', async function () { - const revertingReceiver = await ERC1363Receiver.new(RECEIVER_MAGIC_VALUE, RevertType.RevertWithoutMessage); - await expectRevertCustomError( - transferFromAndCallWithoutData.call(this, owner, revertingReceiver.address, initialBalance, { - from: spender, - }), - 'ERC1363InvalidReceiver', - [revertingReceiver.address], - ); - }); - }); - - describe('to a receiver contract that reverts with custom error', function () { - it('reverts', async function () { - const revertingReceiver = await ERC1363Receiver.new(RECEIVER_MAGIC_VALUE, RevertType.RevertWithCustomError); - await expectRevertCustomError( - transferFromAndCallWithoutData.call(this, owner, revertingReceiver.address, initialBalance, { - from: spender, - }), - 'CustomError', - [RECEIVER_MAGIC_VALUE], - ); - }); - }); - - describe('to a receiver contract that panics', function () { - it('reverts', async function () { - const revertingReceiver = await ERC1363Receiver.new(RECEIVER_MAGIC_VALUE, RevertType.Panic); - await expectRevert.unspecified( - transferFromAndCallWithoutData.call(this, owner, revertingReceiver.address, initialBalance, { - from: spender, - }), - ); - }); - }); - - describe('to a contract that does not implement the required function', function () { - it('reverts', async function () { - const nonReceiver = this.token; - await expectRevertCustomError( - transferFromAndCallWithoutData.call(this, owner, nonReceiver.address, initialBalance, { from: spender }), - 'ERC1363InvalidReceiver', - [nonReceiver.address], - ); - }); - }); - }); - }); - - describe('approvals', function () { - const initialBalance = initialSupply; - const data = '0x42'; - - describe('via approveAndCall', function () { - const approveAndCallWithData = function (spender, value, opts) { - return this.token.methods['approveAndCall(address,uint256,bytes)'](spender, value, data, opts); - }; - - const approveAndCallWithoutData = function (spender, value, opts) { - return this.token.methods['approveAndCall(address,uint256)'](spender, value, opts); - }; - - const shouldApproveSafely = function (approveFunction, data) { - describe('to a valid receiver contract', function () { - beforeEach(async function () { - this.spender = await ERC1363Spender.new(SPENDER_MAGIC_VALUE, RevertType.None); - this.to = this.spender.address; - }); - - it('calls onApprovalReceived', async function () { - const receipt = await approveFunction.call(this, this.to, initialBalance, { from: owner }); - - await expectEvent.inTransaction(receipt.tx, ERC1363Spender, 'Approved', { - owner, - value: initialBalance, - data, - }); - }); - }); - }; - - const approveWasSuccessful = function (owner, balance) { - const value = balance; - - let spender; - - beforeEach(async function () { - const spenderContract = await ERC1363Spender.new(SPENDER_MAGIC_VALUE, RevertType.None); - spender = spenderContract.address; - }); - - describe('with data', function () { - it('approves the requested amount', async function () { - await approveAndCallWithData.call(this, spender, value, { from: owner }); - - expect(await this.token.allowance(owner, spender)).to.be.bignumber.equal(value); - }); - - it('emits an approval event', async function () { - expectEvent(await approveAndCallWithData.call(this, spender, value, { from: owner }), 'Approval', { - owner, - spender, - value, - }); - }); - }); - - describe('without data', function () { - it('approves the requested amount', async function () { - await approveAndCallWithoutData.call(this, spender, value, { from: owner }); - - expect(await this.token.allowance(owner, spender)).to.be.bignumber.equal(value); - }); - - it('emits an approval event', async function () { - expectEvent(await approveAndCallWithoutData.call(this, spender, value, { from: owner }), 'Approval', { - owner, - spender, - value, - }); - }); - }); - }; - - describe('with data', function () { - shouldApproveSafely(approveAndCallWithData, data); - }); - - describe('without data', function () { - shouldApproveSafely(approveAndCallWithoutData, null); - }); - - describe('testing ERC20 behavior', function () { - approveWasSuccessful(owner, initialBalance); - }); - - describe('to a spender that is not a contract', function () { - it('reverts', async function () { - await expectRevertCustomError( - approveAndCallWithoutData.call(this, recipient, initialBalance, { from: owner }), - 'ERC1363EOASpender', - [recipient], - ); - }); - }); - - describe('to a spender contract returning unexpected value', function () { - it('reverts', async function () { - const invalidSpender = await ERC1363Spender.new(data, RevertType.None); - await expectRevertCustomError( - approveAndCallWithoutData.call(this, invalidSpender.address, initialBalance, { from: owner }), - 'ERC1363InvalidSpender', - [invalidSpender.address], - ); - }); - }); - - describe('to a spender contract that reverts with message', function () { - it('reverts', async function () { - const revertingSpender = await ERC1363Spender.new(SPENDER_MAGIC_VALUE, RevertType.RevertWithMessage); - await expectRevert( - approveAndCallWithoutData.call(this, revertingSpender.address, initialBalance, { from: owner }), - 'ERC1363SpenderMock: reverting', - ); - }); - }); - - describe('to a spender contract that reverts without message', function () { - it('reverts', async function () { - const revertingSpender = await ERC1363Spender.new(SPENDER_MAGIC_VALUE, RevertType.RevertWithoutMessage); - await expectRevertCustomError( - approveAndCallWithoutData.call(this, revertingSpender.address, initialBalance, { from: owner }), - 'ERC1363InvalidSpender', - [revertingSpender.address], - ); - }); - }); - - describe('to a spender contract that reverts with custom error', function () { - it('reverts', async function () { - const revertingSpender = await ERC1363Spender.new(SPENDER_MAGIC_VALUE, RevertType.RevertWithCustomError); - await expectRevertCustomError( - approveAndCallWithoutData.call(this, revertingSpender.address, initialBalance, { from: owner }), - 'CustomError', - [SPENDER_MAGIC_VALUE], - ); - }); - }); - - describe('to a spender contract that panics', function () { - it('reverts', async function () { - const revertingSpender = await ERC1363Spender.new(SPENDER_MAGIC_VALUE, RevertType.Panic); - await expectRevert.unspecified( - approveAndCallWithoutData.call(this, revertingSpender.address, initialBalance, { from: owner }), - ); - }); - }); - - describe('to a contract that does not implement the required function', function () { - it('reverts', async function () { - const nonSpender = this.token; - await expectRevertCustomError( - approveAndCallWithoutData.call(this, nonSpender.address, initialBalance, { from: owner }), - 'ERC1363InvalidSpender', - [nonSpender.address], - ); - }); - }); - }); - }); -} - -module.exports = { - shouldBehaveLikeERC1363, -}; diff --git a/test/token/ERC20/extensions/ERC1363.test.js b/test/token/ERC20/extensions/ERC1363.test.js index 603c89adaae..4a4a0307c96 100644 --- a/test/token/ERC20/extensions/ERC1363.test.js +++ b/test/token/ERC20/extensions/ERC1363.test.js @@ -1,20 +1,210 @@ -const { BN } = require('@openzeppelin/test-helpers'); +const { ethers } = require('hardhat'); +const { expect } = require('chai'); +const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers'); -const { shouldBehaveLikeERC1363 } = require('./ERC1363.behavior'); +const { shouldSupportInterfaces } = require('../../../utils/introspection/SupportsInterface.behavior'); +const { Enum2 } = require('../../../helpers/enums'); +const RevertType = Enum2('None', 'RevertWithoutMessage', 'RevertWithMessage', 'RevertWithCustomError', 'Panic'); -const ERC1363 = artifacts.require('$ERC1363'); +const name = 'My Token'; +const symbol = 'MTKN'; +const value = 1000n; +const data = '0x123456'; -contract('ERC1363', function (accounts) { - const [owner] = accounts; - - const name = 'My Token'; - const symbol = 'MTKN'; - const initialSupply = new BN(100); +async function fixture() { + const [holder, other] = await ethers.getSigners(); + const receiver = await ethers.deployContract('ERC1363ReceiverMock'); + const spender = await ethers.deployContract('ERC1363SpenderMock'); + const token = await ethers.deployContract('$ERC1363', [ name, symbol ]); + await token.$_mint(holder, value); + return { + token, + holder, + other, + receiver, + spender, + selectors: { + onTransferReceived: receiver.interface.getFunction('onTransferReceived(address,address,uint256,bytes)').selector, + onApprovalReceived: spender.interface.getFunction('onApprovalReceived(address,uint256,bytes)').selector, + }, + }; +} +describe('ERC1363', function () { beforeEach(async function () { - this.token = await ERC1363.new(name, symbol); - await this.token.$_mint(owner, initialSupply); + Object.assign(this, await loadFixture(fixture)); }); - shouldBehaveLikeERC1363(initialSupply, accounts); -}); + // TODO: check ERC20 behavior when behavior is migrated to ethers + + shouldSupportInterfaces(['ERC165', 'ERC1363']); + + describe('transferAndCall', function () { + it('to an EOA', async function () { + await expect(this.token.connect(this.holder).getFunction('transferAndCall(address,uint256)')(this.other, value)) + .to.be.revertedWithCustomError(this.token, 'ERC1363InvalidReceiver').withArgs(this.other.address); + }); + + it('without data', async function () { + await expect(this.token.connect(this.holder).getFunction('transferAndCall(address,uint256)')(this.receiver, value)) + .to.emit(this.token, 'Transfer').withArgs(this.holder.address, this.receiver.target, value) + .to.emit(this.receiver, 'Received').withArgs(this.holder.address, this.holder.address, value, '0x'); + }); + + it('with data', async function () { + await expect(this.token.connect(this.holder).getFunction('transferAndCall(address,uint256,bytes)')(this.receiver, value, data)) + .to.emit(this.token, 'Transfer').withArgs(this.holder.address, this.receiver.target, value) + .to.emit(this.receiver, 'Received').withArgs(this.holder.address, this.holder.address, value, data); + }); + + it('with reverting hook (without reason)', async function () { + await this.receiver.setUp(this.selectors.onTransferReceived, RevertType.RevertWithoutMessage); + + await expect(this.token.connect(this.holder).getFunction('transferAndCall(address,uint256,bytes)')(this.receiver, value, data)) + .to.be.revertedWithCustomError(this.token, 'ERC1363InvalidReceiver').withArgs(this.receiver.target); + }); + + it('with reverting hook (with reason)', async function () { + await this.receiver.setUp(this.selectors.onTransferReceived, RevertType.RevertWithMessage); + + await expect(this.token.connect(this.holder).getFunction('transferAndCall(address,uint256,bytes)')(this.receiver, value, data)) + .to.be.revertedWith('ERC1363ReceiverMock: reverting'); + }); + + it('with reverting hook (with custom error)', async function () { + const reason = '0x12345678'; + await this.receiver.setUp(reason, RevertType.RevertWithCustomError); + + await expect(this.token.connect(this.holder).getFunction('transferAndCall(address,uint256,bytes)')(this.receiver, value, data)) + .to.be.revertedWithCustomError(this.receiver, 'CustomError').withArgs(reason); + }); + + it('with reverting hook (with panic)', async function () { + await this.receiver.setUp(this.selectors.onTransferReceived, RevertType.Panic); + + await expect(this.token.connect(this.holder).getFunction('transferAndCall(address,uint256,bytes)')(this.receiver, value, data)) + .to.be.revertedWithPanic(); + }); + + it('with bad return value', async function () { + await this.receiver.setUp('0x12345678', RevertType.None); + + await expect(this.token.connect(this.holder).getFunction('transferAndCall(address,uint256,bytes)')(this.receiver, value, data)) + .to.be.revertedWithCustomError(this.token, 'ERC1363InvalidReceiver').withArgs(this.receiver.target); + }); + }); + + describe('transferFromAndCall', function () { + beforeEach(async function () { + await this.token.connect(this.holder).approve(this.other, ethers.MaxUint256); + }); + + it('to an EOA', async function () { + await expect(this.token.connect(this.other).getFunction('transferFromAndCall(address,address,uint256)')(this.holder, this.other, value)) + .to.be.revertedWithCustomError(this.token, 'ERC1363InvalidReceiver').withArgs(this.other.address); + }); + + it('without data', async function () { + await expect(this.token.connect(this.other).getFunction('transferFromAndCall(address,address,uint256)')(this.holder, this.receiver, value)) + .to.emit(this.token, 'Transfer').withArgs(this.holder.address, this.receiver.target, value) + .to.emit(this.receiver, 'Received').withArgs(this.other.address, this.holder.address, value, '0x'); + }); + + it('with data', async function () { + await expect(this.token.connect(this.other).getFunction('transferFromAndCall(address,address,uint256,bytes)')(this.holder, this.receiver, value, data)) + .to.emit(this.token, 'Transfer').withArgs(this.holder.address, this.receiver.target, value) + .to.emit(this.receiver, 'Received').withArgs(this.other.address, this.holder.address, value, data); + }); + + it('with reverting hook (without reason)', async function () { + await this.receiver.setUp(this.selectors.onTransferReceived, RevertType.RevertWithoutMessage); + + await expect(this.token.connect(this.other).getFunction('transferFromAndCall(address,address,uint256,bytes)')(this.holder, this.receiver, value, data)) + .to.be.revertedWithCustomError(this.token, 'ERC1363InvalidReceiver').withArgs(this.receiver.target); + }); + + it('with reverting hook (with reason)', async function () { + await this.receiver.setUp(this.selectors.onTransferReceived, RevertType.RevertWithMessage); + + await expect(this.token.connect(this.other).getFunction('transferFromAndCall(address,address,uint256,bytes)')(this.holder, this.receiver, value, data)) + .to.be.revertedWith('ERC1363ReceiverMock: reverting'); + }); + + it('with reverting hook (with custom error)', async function () { + const reason = '0x12345678'; + await this.receiver.setUp(reason, RevertType.RevertWithCustomError); + + await expect(this.token.connect(this.other).getFunction('transferFromAndCall(address,address,uint256,bytes)')(this.holder, this.receiver, value, data)) + .to.be.revertedWithCustomError(this.receiver, 'CustomError').withArgs(reason); + }); + + it('with reverting hook (with panic)', async function () { + await this.receiver.setUp(this.selectors.onTransferReceived, RevertType.Panic); + + await expect(this.token.connect(this.other).getFunction('transferFromAndCall(address,address,uint256,bytes)')(this.holder, this.receiver, value, data)) + .to.be.revertedWithPanic(); + }); + + it('with bad return value', async function () { + await this.receiver.setUp('0x12345678', RevertType.None); + + await expect(this.token.connect(this.other).getFunction('transferFromAndCall(address,address,uint256,bytes)')(this.holder, this.receiver, value, data)) + .to.be.revertedWithCustomError(this.token, 'ERC1363InvalidReceiver').withArgs(this.receiver.target); + }); + }); + + describe('approveAndCall', function () { + it('an EOA', async function () { + await expect(this.token.connect(this.holder).getFunction('approveAndCall(address,uint256)')(this.other, value)) + .to.be.revertedWithCustomError(this.token, 'ERC1363InvalidSpender').withArgs(this.other.address); + }); + + it('without data', async function () { + await expect(this.token.connect(this.holder).getFunction('approveAndCall(address,uint256)')(this.spender, value)) + .to.emit(this.token, 'Approval').withArgs(this.holder.address, this.spender.target, value) + .to.emit(this.spender, 'Approved').withArgs(this.holder.address, value, '0x'); + }); + + it('with data', async function () { + await expect(this.token.connect(this.holder).getFunction('approveAndCall(address,uint256,bytes)')(this.spender, value, data)) + .to.emit(this.token, 'Approval').withArgs(this.holder.address, this.spender.target, value) + .to.emit(this.spender, 'Approved').withArgs(this.holder.address, value, data); + }); + + it('with reverting hook (without reason)', async function () { + await this.spender.setUp(this.selectors.onApprovalReceived, RevertType.RevertWithoutMessage); + + await expect(this.token.connect(this.holder).getFunction('approveAndCall(address,uint256,bytes)')(this.spender, value, data)) + .to.be.revertedWithCustomError(this.token, 'ERC1363InvalidSpender').withArgs(this.spender.target); + }); + + it('with reverting hook (with reason)', async function () { + await this.spender.setUp(this.selectors.onApprovalReceived, RevertType.RevertWithMessage); + + await expect(this.token.connect(this.holder).getFunction('approveAndCall(address,uint256,bytes)')(this.spender, value, data)) + .to.be.revertedWith('ERC1363SpenderMock: reverting'); + }); + + it('with reverting hook (with custom error)', async function () { + const reason = '0x12345678'; + await this.spender.setUp(reason, RevertType.RevertWithCustomError); + + await expect(this.token.connect(this.holder).getFunction('approveAndCall(address,uint256,bytes)')(this.spender, value, data)) + .to.be.revertedWithCustomError(this.spender, 'CustomError').withArgs(reason); + }); + + it('with reverting hook (with panic)', async function () { + await this.spender.setUp(this.selectors.onApprovalReceived, RevertType.Panic); + + await expect(this.token.connect(this.holder).getFunction('approveAndCall(address,uint256,bytes)')(this.spender, value, data)) + .to.be.revertedWithPanic(); + }); + + it('with bad return value', async function () { + await this.spender.setUp('0x12345678', RevertType.None); + + await expect(this.token.connect(this.holder).getFunction('approveAndCall(address,uint256,bytes)')(this.spender, value, data)) + .to.be.revertedWithCustomError(this.token, 'ERC1363InvalidSpender').withArgs(this.spender.target); + }); + }); +}); \ No newline at end of file diff --git a/test/utils/introspection/SupportsInterface.behavior.js b/test/utils/introspection/SupportsInterface.behavior.js index b18a30a3465..77f481df1be 100644 --- a/test/utils/introspection/SupportsInterface.behavior.js +++ b/test/utils/introspection/SupportsInterface.behavior.js @@ -38,8 +38,6 @@ const INTERFACES = { 'approveAndCall(address,uint256)', 'approveAndCall(address,uint256,bytes)', ], - ERC1363Receiver: ['onTransferReceived(address,address,uint256,bytes)'], - ERC1363Spender: ['onApprovalReceived(address,uint256,bytes)'], AccessControl: [ 'hasRole(bytes32,address)', 'getRoleAdmin(bytes32)', From 46858a77fd3511bd5043519720dcd188d76f5979 Mon Sep 17 00:00:00 2001 From: Vittorio Minacori Date: Tue, 17 Oct 2023 10:24:32 +0200 Subject: [PATCH 43/82] Remove ERC1363 Spender and Receiver interface from introspection test --- test/utils/introspection/SupportsInterface.behavior.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/test/utils/introspection/SupportsInterface.behavior.js b/test/utils/introspection/SupportsInterface.behavior.js index b18a30a3465..77f481df1be 100644 --- a/test/utils/introspection/SupportsInterface.behavior.js +++ b/test/utils/introspection/SupportsInterface.behavior.js @@ -38,8 +38,6 @@ const INTERFACES = { 'approveAndCall(address,uint256)', 'approveAndCall(address,uint256,bytes)', ], - ERC1363Receiver: ['onTransferReceived(address,address,uint256,bytes)'], - ERC1363Spender: ['onApprovalReceived(address,uint256,bytes)'], AccessControl: [ 'hasRole(bytes32,address)', 'getRoleAdmin(bytes32)', From c07c4140ef498e7bf066bbbc5bd9e90e08f3ad4d Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Tue, 17 Oct 2023 10:28:54 +0200 Subject: [PATCH 44/82] addapt erc165 verification to ethers --- .../utils/introspection/SupportsInterface.behavior.js | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/test/utils/introspection/SupportsInterface.behavior.js b/test/utils/introspection/SupportsInterface.behavior.js index 77f481df1be..7affbf97efb 100644 --- a/test/utils/introspection/SupportsInterface.behavior.js +++ b/test/utils/introspection/SupportsInterface.behavior.js @@ -136,11 +136,12 @@ function shouldSupportInterfaces(interfaces = []) { // skip interfaces for which we don't have a function list if (INTERFACES[k] === undefined) continue; for (const fnName of INTERFACES[k]) { - const fnSig = FN_SIGNATURES[fnName]; - expect(this.contractUnderTest.abi.filter(fn => fn.signature === fnSig).length).to.equal( - 1, - `did not find ${fnName}`, - ); + if (this.contractUnderTest.abi) { + const fnSig = FN_SIGNATURES[fnName]; + expect(this.contractUnderTest.abi.filter(fn => fn.signature === fnSig).length).to.equal(1, `did not find ${fnName}`); + } else { + expect(this.contractUnderTest.interface.getFunction(fnName)).to.be.an('object', `did not find ${fnName}`); + } } } }); From 24e8e89f0356e36b54a2c74c6216d20d0063f50a Mon Sep 17 00:00:00 2001 From: Vittorio Minacori Date: Tue, 17 Oct 2023 10:32:45 +0200 Subject: [PATCH 45/82] Add a test to check that events are emitted --- test/token/ERC20/utils/ERC1363Holder.test.js | 27 +++++++++++++++----- 1 file changed, 21 insertions(+), 6 deletions(-) diff --git a/test/token/ERC20/utils/ERC1363Holder.test.js b/test/token/ERC20/utils/ERC1363Holder.test.js index 7020f2863d6..7aedac5577a 100644 --- a/test/token/ERC20/utils/ERC1363Holder.test.js +++ b/test/token/ERC20/utils/ERC1363Holder.test.js @@ -1,4 +1,4 @@ -const { BN } = require('@openzeppelin/test-helpers'); +const { BN, expectEvent } = require('@openzeppelin/test-helpers'); const { expect } = require('chai'); const ERC1363Holder = artifacts.require('$ERC1363Holder'); @@ -20,7 +20,11 @@ contract('ERC1363Holder', function (accounts) { describe('receives ERC1363 token transfers', function () { it('via transferAndCall', async function () { - await this.token.methods['transferAndCall(address,uint256)'](this.receiver.address, balance, { from: owner }); + const receipt = await this.token.methods['transferAndCall(address,uint256)'](this.receiver.address, balance, { + from: owner, + }); + + expectEvent(receipt, 'Transfer', { from: owner, to: this.receiver.address, value: balance }); expect(await this.token.balanceOf(owner)).to.be.bignumber.equal('0'); expect(await this.token.balanceOf(this.receiver.address)).to.be.bignumber.equal(balance); @@ -29,9 +33,16 @@ contract('ERC1363Holder', function (accounts) { it('via transferFromAndCall', async function () { await this.token.approve(spender, balance, { from: owner }); - await this.token.methods['transferFromAndCall(address,address,uint256)'](owner, this.receiver.address, balance, { - from: spender, - }); + const receipt = await this.token.methods['transferFromAndCall(address,address,uint256)']( + owner, + this.receiver.address, + balance, + { + from: spender, + }, + ); + + expectEvent(receipt, 'Transfer', { from: owner, to: this.receiver.address, value: balance }); expect(await this.token.balanceOf(owner)).to.be.bignumber.equal('0'); expect(await this.token.balanceOf(this.receiver.address)).to.be.bignumber.equal(balance); @@ -40,7 +51,11 @@ contract('ERC1363Holder', function (accounts) { describe('receives ERC1363 token approvals', function () { it('via approveAndCall', async function () { - await this.token.methods['approveAndCall(address,uint256)'](this.receiver.address, balance, { from: owner }); + const receipt = await this.token.methods['approveAndCall(address,uint256)'](this.receiver.address, balance, { + from: owner, + }); + + expectEvent(receipt, 'Approval', { owner, spender: this.receiver.address, value: balance }); expect(await this.token.allowance(owner, this.receiver.address)).to.be.bignumber.equal(balance); }); From f781db4f0ad69ede415bed4f4d1432ecd0ab6f5c Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Tue, 17 Oct 2023 11:21:23 +0200 Subject: [PATCH 46/82] minor fixes --- contracts/interfaces/README.adoc | 3 --- contracts/token/ERC20/utils/SafeERC20.sol | 6 +++--- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/contracts/interfaces/README.adoc b/contracts/interfaces/README.adoc index a5e1bdee07f..379a24a1e26 100644 --- a/contracts/interfaces/README.adoc +++ b/contracts/interfaces/README.adoc @@ -27,7 +27,6 @@ are useful to interact with third party contracts that implement them. - {IERC1363} - {IERC1363Receiver} - {IERC1363Spender} -- {IERC1363Errors} - {IERC1820Implementer} - {IERC1820Registry} - {IERC1822Proxiable} @@ -58,8 +57,6 @@ are useful to interact with third party contracts that implement them. {{IERC1363Spender}} -{{IERC1363Errors}} - {{IERC1820Implementer}} {{IERC1820Registry}} diff --git a/contracts/token/ERC20/utils/SafeERC20.sol b/contracts/token/ERC20/utils/SafeERC20.sol index 61acac1b2ed..4b911bda175 100644 --- a/contracts/token/ERC20/utils/SafeERC20.sol +++ b/contracts/token/ERC20/utils/SafeERC20.sol @@ -89,7 +89,7 @@ library SafeERC20 { */ function transferAndCallRelaxed(IERC1363 token, address to, uint256 value, bytes memory data) internal { if (to.code.length == 0) { - token.transfer(to, value); + safeTransfer(token, to, value); } else { token.transferAndCall(to, value, data); } @@ -102,7 +102,7 @@ library SafeERC20 { */ function transferFromAndCallRelaxed(IERC1363 token, address from, address to, uint256 value, bytes memory data) internal { if (to.code.length == 0) { - token.transferFrom(from, to, value); + safeTransferFrom(token, from, to, value); } else { token.transferFromAndCall(from, to, value, data); } @@ -115,7 +115,7 @@ library SafeERC20 { */ function approveAndCallRelaxed(IERC1363 token, address to, uint256 value, bytes memory data) internal { if (to.code.length == 0) { - token.approve(to, value); + forceApprove(token, to, value); } else { token.approveAndCall(to, value, data); } From 80930e844b5e91fdc2fd596d4a736b2a77572294 Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Tue, 17 Oct 2023 11:32:22 +0200 Subject: [PATCH 47/82] migrate ERC1363Holder test to ethers + lint:fix --- contracts/mocks/token/ERC1363SpenderMock.sol | 6 +- contracts/token/ERC20/utils/SafeERC20.sol | 8 +- test/token/ERC20/extensions/ERC1363.test.js | 228 ++++++++++++++---- test/token/ERC20/utils/ERC1363Holder.test.js | 91 ++++--- .../SupportsInterface.behavior.js | 5 +- 5 files changed, 239 insertions(+), 99 deletions(-) diff --git a/contracts/mocks/token/ERC1363SpenderMock.sol b/contracts/mocks/token/ERC1363SpenderMock.sol index c70e54fcc56..52f84472ef7 100644 --- a/contracts/mocks/token/ERC1363SpenderMock.sol +++ b/contracts/mocks/token/ERC1363SpenderMock.sol @@ -29,11 +29,7 @@ contract ERC1363SpenderMock is IERC1363Spender { _error = error; } - function onApprovalReceived( - address owner, - uint256 value, - bytes calldata data - ) public override returns (bytes4) { + function onApprovalReceived(address owner, uint256 value, bytes calldata data) public override returns (bytes4) { if (_error == RevertType.RevertWithoutMessage) { revert(); } else if (_error == RevertType.RevertWithMessage) { diff --git a/contracts/token/ERC20/utils/SafeERC20.sol b/contracts/token/ERC20/utils/SafeERC20.sol index 4b911bda175..91df39fbb1d 100644 --- a/contracts/token/ERC20/utils/SafeERC20.sol +++ b/contracts/token/ERC20/utils/SafeERC20.sol @@ -100,7 +100,13 @@ library SafeERC20 { * has no code. This can be used to implement an {ERC721}-like safe transfer that rely on {ERC1363} checks when * targetting contracts. */ - function transferFromAndCallRelaxed(IERC1363 token, address from, address to, uint256 value, bytes memory data) internal { + function transferFromAndCallRelaxed( + IERC1363 token, + address from, + address to, + uint256 value, + bytes memory data + ) internal { if (to.code.length == 0) { safeTransferFrom(token, from, to, value); } else { diff --git a/test/token/ERC20/extensions/ERC1363.test.js b/test/token/ERC20/extensions/ERC1363.test.js index 4a4a0307c96..2fbd75bccef 100644 --- a/test/token/ERC20/extensions/ERC1363.test.js +++ b/test/token/ERC20/extensions/ERC1363.test.js @@ -15,7 +15,7 @@ async function fixture() { const [holder, other] = await ethers.getSigners(); const receiver = await ethers.deployContract('ERC1363ReceiverMock'); const spender = await ethers.deployContract('ERC1363SpenderMock'); - const token = await ethers.deployContract('$ERC1363', [ name, symbol ]); + const token = await ethers.deployContract('$ERC1363', [name, symbol]); await token.$_mint(holder, value); return { token, @@ -42,55 +42,99 @@ describe('ERC1363', function () { describe('transferAndCall', function () { it('to an EOA', async function () { await expect(this.token.connect(this.holder).getFunction('transferAndCall(address,uint256)')(this.other, value)) - .to.be.revertedWithCustomError(this.token, 'ERC1363InvalidReceiver').withArgs(this.other.address); + .to.be.revertedWithCustomError(this.token, 'ERC1363InvalidReceiver') + .withArgs(this.other.address); }); it('without data', async function () { - await expect(this.token.connect(this.holder).getFunction('transferAndCall(address,uint256)')(this.receiver, value)) - .to.emit(this.token, 'Transfer').withArgs(this.holder.address, this.receiver.target, value) - .to.emit(this.receiver, 'Received').withArgs(this.holder.address, this.holder.address, value, '0x'); + await expect( + this.token.connect(this.holder).getFunction('transferAndCall(address,uint256)')(this.receiver, value), + ) + .to.emit(this.token, 'Transfer') + .withArgs(this.holder.address, this.receiver.target, value) + .to.emit(this.receiver, 'Received') + .withArgs(this.holder.address, this.holder.address, value, '0x'); }); it('with data', async function () { - await expect(this.token.connect(this.holder).getFunction('transferAndCall(address,uint256,bytes)')(this.receiver, value, data)) - .to.emit(this.token, 'Transfer').withArgs(this.holder.address, this.receiver.target, value) - .to.emit(this.receiver, 'Received').withArgs(this.holder.address, this.holder.address, value, data); + await expect( + this.token.connect(this.holder).getFunction('transferAndCall(address,uint256,bytes)')( + this.receiver, + value, + data, + ), + ) + .to.emit(this.token, 'Transfer') + .withArgs(this.holder.address, this.receiver.target, value) + .to.emit(this.receiver, 'Received') + .withArgs(this.holder.address, this.holder.address, value, data); }); it('with reverting hook (without reason)', async function () { await this.receiver.setUp(this.selectors.onTransferReceived, RevertType.RevertWithoutMessage); - await expect(this.token.connect(this.holder).getFunction('transferAndCall(address,uint256,bytes)')(this.receiver, value, data)) - .to.be.revertedWithCustomError(this.token, 'ERC1363InvalidReceiver').withArgs(this.receiver.target); + await expect( + this.token.connect(this.holder).getFunction('transferAndCall(address,uint256,bytes)')( + this.receiver, + value, + data, + ), + ) + .to.be.revertedWithCustomError(this.token, 'ERC1363InvalidReceiver') + .withArgs(this.receiver.target); }); it('with reverting hook (with reason)', async function () { await this.receiver.setUp(this.selectors.onTransferReceived, RevertType.RevertWithMessage); - await expect(this.token.connect(this.holder).getFunction('transferAndCall(address,uint256,bytes)')(this.receiver, value, data)) - .to.be.revertedWith('ERC1363ReceiverMock: reverting'); + await expect( + this.token.connect(this.holder).getFunction('transferAndCall(address,uint256,bytes)')( + this.receiver, + value, + data, + ), + ).to.be.revertedWith('ERC1363ReceiverMock: reverting'); }); it('with reverting hook (with custom error)', async function () { const reason = '0x12345678'; await this.receiver.setUp(reason, RevertType.RevertWithCustomError); - await expect(this.token.connect(this.holder).getFunction('transferAndCall(address,uint256,bytes)')(this.receiver, value, data)) - .to.be.revertedWithCustomError(this.receiver, 'CustomError').withArgs(reason); + await expect( + this.token.connect(this.holder).getFunction('transferAndCall(address,uint256,bytes)')( + this.receiver, + value, + data, + ), + ) + .to.be.revertedWithCustomError(this.receiver, 'CustomError') + .withArgs(reason); }); it('with reverting hook (with panic)', async function () { await this.receiver.setUp(this.selectors.onTransferReceived, RevertType.Panic); - await expect(this.token.connect(this.holder).getFunction('transferAndCall(address,uint256,bytes)')(this.receiver, value, data)) - .to.be.revertedWithPanic(); + await expect( + this.token.connect(this.holder).getFunction('transferAndCall(address,uint256,bytes)')( + this.receiver, + value, + data, + ), + ).to.be.revertedWithPanic(); }); it('with bad return value', async function () { await this.receiver.setUp('0x12345678', RevertType.None); - await expect(this.token.connect(this.holder).getFunction('transferAndCall(address,uint256,bytes)')(this.receiver, value, data)) - .to.be.revertedWithCustomError(this.token, 'ERC1363InvalidReceiver').withArgs(this.receiver.target); + await expect( + this.token.connect(this.holder).getFunction('transferAndCall(address,uint256,bytes)')( + this.receiver, + value, + data, + ), + ) + .to.be.revertedWithCustomError(this.token, 'ERC1363InvalidReceiver') + .withArgs(this.receiver.target); }); }); @@ -100,111 +144,189 @@ describe('ERC1363', function () { }); it('to an EOA', async function () { - await expect(this.token.connect(this.other).getFunction('transferFromAndCall(address,address,uint256)')(this.holder, this.other, value)) - .to.be.revertedWithCustomError(this.token, 'ERC1363InvalidReceiver').withArgs(this.other.address); + await expect( + this.token.connect(this.other).getFunction('transferFromAndCall(address,address,uint256)')( + this.holder, + this.other, + value, + ), + ) + .to.be.revertedWithCustomError(this.token, 'ERC1363InvalidReceiver') + .withArgs(this.other.address); }); it('without data', async function () { - await expect(this.token.connect(this.other).getFunction('transferFromAndCall(address,address,uint256)')(this.holder, this.receiver, value)) - .to.emit(this.token, 'Transfer').withArgs(this.holder.address, this.receiver.target, value) - .to.emit(this.receiver, 'Received').withArgs(this.other.address, this.holder.address, value, '0x'); + await expect( + this.token.connect(this.other).getFunction('transferFromAndCall(address,address,uint256)')( + this.holder, + this.receiver, + value, + ), + ) + .to.emit(this.token, 'Transfer') + .withArgs(this.holder.address, this.receiver.target, value) + .to.emit(this.receiver, 'Received') + .withArgs(this.other.address, this.holder.address, value, '0x'); }); it('with data', async function () { - await expect(this.token.connect(this.other).getFunction('transferFromAndCall(address,address,uint256,bytes)')(this.holder, this.receiver, value, data)) - .to.emit(this.token, 'Transfer').withArgs(this.holder.address, this.receiver.target, value) - .to.emit(this.receiver, 'Received').withArgs(this.other.address, this.holder.address, value, data); + await expect( + this.token.connect(this.other).getFunction('transferFromAndCall(address,address,uint256,bytes)')( + this.holder, + this.receiver, + value, + data, + ), + ) + .to.emit(this.token, 'Transfer') + .withArgs(this.holder.address, this.receiver.target, value) + .to.emit(this.receiver, 'Received') + .withArgs(this.other.address, this.holder.address, value, data); }); it('with reverting hook (without reason)', async function () { await this.receiver.setUp(this.selectors.onTransferReceived, RevertType.RevertWithoutMessage); - await expect(this.token.connect(this.other).getFunction('transferFromAndCall(address,address,uint256,bytes)')(this.holder, this.receiver, value, data)) - .to.be.revertedWithCustomError(this.token, 'ERC1363InvalidReceiver').withArgs(this.receiver.target); + await expect( + this.token.connect(this.other).getFunction('transferFromAndCall(address,address,uint256,bytes)')( + this.holder, + this.receiver, + value, + data, + ), + ) + .to.be.revertedWithCustomError(this.token, 'ERC1363InvalidReceiver') + .withArgs(this.receiver.target); }); it('with reverting hook (with reason)', async function () { await this.receiver.setUp(this.selectors.onTransferReceived, RevertType.RevertWithMessage); - await expect(this.token.connect(this.other).getFunction('transferFromAndCall(address,address,uint256,bytes)')(this.holder, this.receiver, value, data)) - .to.be.revertedWith('ERC1363ReceiverMock: reverting'); + await expect( + this.token.connect(this.other).getFunction('transferFromAndCall(address,address,uint256,bytes)')( + this.holder, + this.receiver, + value, + data, + ), + ).to.be.revertedWith('ERC1363ReceiverMock: reverting'); }); it('with reverting hook (with custom error)', async function () { const reason = '0x12345678'; await this.receiver.setUp(reason, RevertType.RevertWithCustomError); - await expect(this.token.connect(this.other).getFunction('transferFromAndCall(address,address,uint256,bytes)')(this.holder, this.receiver, value, data)) - .to.be.revertedWithCustomError(this.receiver, 'CustomError').withArgs(reason); + await expect( + this.token.connect(this.other).getFunction('transferFromAndCall(address,address,uint256,bytes)')( + this.holder, + this.receiver, + value, + data, + ), + ) + .to.be.revertedWithCustomError(this.receiver, 'CustomError') + .withArgs(reason); }); it('with reverting hook (with panic)', async function () { await this.receiver.setUp(this.selectors.onTransferReceived, RevertType.Panic); - await expect(this.token.connect(this.other).getFunction('transferFromAndCall(address,address,uint256,bytes)')(this.holder, this.receiver, value, data)) - .to.be.revertedWithPanic(); + await expect( + this.token.connect(this.other).getFunction('transferFromAndCall(address,address,uint256,bytes)')( + this.holder, + this.receiver, + value, + data, + ), + ).to.be.revertedWithPanic(); }); it('with bad return value', async function () { await this.receiver.setUp('0x12345678', RevertType.None); - await expect(this.token.connect(this.other).getFunction('transferFromAndCall(address,address,uint256,bytes)')(this.holder, this.receiver, value, data)) - .to.be.revertedWithCustomError(this.token, 'ERC1363InvalidReceiver').withArgs(this.receiver.target); + await expect( + this.token.connect(this.other).getFunction('transferFromAndCall(address,address,uint256,bytes)')( + this.holder, + this.receiver, + value, + data, + ), + ) + .to.be.revertedWithCustomError(this.token, 'ERC1363InvalidReceiver') + .withArgs(this.receiver.target); }); }); describe('approveAndCall', function () { it('an EOA', async function () { await expect(this.token.connect(this.holder).getFunction('approveAndCall(address,uint256)')(this.other, value)) - .to.be.revertedWithCustomError(this.token, 'ERC1363InvalidSpender').withArgs(this.other.address); + .to.be.revertedWithCustomError(this.token, 'ERC1363InvalidSpender') + .withArgs(this.other.address); }); it('without data', async function () { await expect(this.token.connect(this.holder).getFunction('approveAndCall(address,uint256)')(this.spender, value)) - .to.emit(this.token, 'Approval').withArgs(this.holder.address, this.spender.target, value) - .to.emit(this.spender, 'Approved').withArgs(this.holder.address, value, '0x'); + .to.emit(this.token, 'Approval') + .withArgs(this.holder.address, this.spender.target, value) + .to.emit(this.spender, 'Approved') + .withArgs(this.holder.address, value, '0x'); }); it('with data', async function () { - await expect(this.token.connect(this.holder).getFunction('approveAndCall(address,uint256,bytes)')(this.spender, value, data)) - .to.emit(this.token, 'Approval').withArgs(this.holder.address, this.spender.target, value) - .to.emit(this.spender, 'Approved').withArgs(this.holder.address, value, data); + await expect( + this.token.connect(this.holder).getFunction('approveAndCall(address,uint256,bytes)')(this.spender, value, data), + ) + .to.emit(this.token, 'Approval') + .withArgs(this.holder.address, this.spender.target, value) + .to.emit(this.spender, 'Approved') + .withArgs(this.holder.address, value, data); }); it('with reverting hook (without reason)', async function () { await this.spender.setUp(this.selectors.onApprovalReceived, RevertType.RevertWithoutMessage); - await expect(this.token.connect(this.holder).getFunction('approveAndCall(address,uint256,bytes)')(this.spender, value, data)) - .to.be.revertedWithCustomError(this.token, 'ERC1363InvalidSpender').withArgs(this.spender.target); + await expect( + this.token.connect(this.holder).getFunction('approveAndCall(address,uint256,bytes)')(this.spender, value, data), + ) + .to.be.revertedWithCustomError(this.token, 'ERC1363InvalidSpender') + .withArgs(this.spender.target); }); it('with reverting hook (with reason)', async function () { await this.spender.setUp(this.selectors.onApprovalReceived, RevertType.RevertWithMessage); - await expect(this.token.connect(this.holder).getFunction('approveAndCall(address,uint256,bytes)')(this.spender, value, data)) - .to.be.revertedWith('ERC1363SpenderMock: reverting'); + await expect( + this.token.connect(this.holder).getFunction('approveAndCall(address,uint256,bytes)')(this.spender, value, data), + ).to.be.revertedWith('ERC1363SpenderMock: reverting'); }); it('with reverting hook (with custom error)', async function () { const reason = '0x12345678'; await this.spender.setUp(reason, RevertType.RevertWithCustomError); - await expect(this.token.connect(this.holder).getFunction('approveAndCall(address,uint256,bytes)')(this.spender, value, data)) - .to.be.revertedWithCustomError(this.spender, 'CustomError').withArgs(reason); + await expect( + this.token.connect(this.holder).getFunction('approveAndCall(address,uint256,bytes)')(this.spender, value, data), + ) + .to.be.revertedWithCustomError(this.spender, 'CustomError') + .withArgs(reason); }); it('with reverting hook (with panic)', async function () { await this.spender.setUp(this.selectors.onApprovalReceived, RevertType.Panic); - await expect(this.token.connect(this.holder).getFunction('approveAndCall(address,uint256,bytes)')(this.spender, value, data)) - .to.be.revertedWithPanic(); + await expect( + this.token.connect(this.holder).getFunction('approveAndCall(address,uint256,bytes)')(this.spender, value, data), + ).to.be.revertedWithPanic(); }); it('with bad return value', async function () { await this.spender.setUp('0x12345678', RevertType.None); - await expect(this.token.connect(this.holder).getFunction('approveAndCall(address,uint256,bytes)')(this.spender, value, data)) - .to.be.revertedWithCustomError(this.token, 'ERC1363InvalidSpender').withArgs(this.spender.target); + await expect( + this.token.connect(this.holder).getFunction('approveAndCall(address,uint256,bytes)')(this.spender, value, data), + ) + .to.be.revertedWithCustomError(this.token, 'ERC1363InvalidSpender') + .withArgs(this.spender.target); }); }); -}); \ No newline at end of file +}); diff --git a/test/token/ERC20/utils/ERC1363Holder.test.js b/test/token/ERC20/utils/ERC1363Holder.test.js index 7aedac5577a..e3e384d477a 100644 --- a/test/token/ERC20/utils/ERC1363Holder.test.js +++ b/test/token/ERC20/utils/ERC1363Holder.test.js @@ -1,63 +1,76 @@ -const { BN, expectEvent } = require('@openzeppelin/test-helpers'); +const { ethers } = require('hardhat'); const { expect } = require('chai'); +const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers'); -const ERC1363Holder = artifacts.require('$ERC1363Holder'); -const ERC1363 = artifacts.require('$ERC1363'); +const name = 'My Token'; +const symbol = 'MTKN'; +const value = 1000n; +const data = '0x123456'; -contract('ERC1363Holder', function (accounts) { - const [owner, spender] = accounts; - - const name = 'My Token'; - const symbol = 'MTKN'; - const balance = new BN(100); +async function fixture() { + const [holder, spender] = await ethers.getSigners(); + const mock = await ethers.deployContract('$ERC1363Holder'); + const token = await ethers.deployContract('$ERC1363', [name, symbol]); + await token.$_mint(holder, value); + return { + token, + mock, + holder, + spender, + }; +} +describe('ERC1363Holder', function () { beforeEach(async function () { - this.token = await ERC1363.new(name, symbol); - this.receiver = await ERC1363Holder.new(); - - await this.token.$_mint(owner, balance); + Object.assign(this, await loadFixture(fixture)); }); describe('receives ERC1363 token transfers', function () { - it('via transferAndCall', async function () { - const receipt = await this.token.methods['transferAndCall(address,uint256)'](this.receiver.address, balance, { - from: owner, - }); + beforeEach(async function () { + expect(await this.token.balanceOf(this.holder)).to.be.equal(value); + expect(await this.token.balanceOf(this.mock)).to.be.equal(0n); + }); - expectEvent(receipt, 'Transfer', { from: owner, to: this.receiver.address, value: balance }); + afterEach(async function () { + expect(await this.token.balanceOf(this.holder)).to.be.equal(0n); + expect(await this.token.balanceOf(this.mock)).to.be.equal(value); + }); - expect(await this.token.balanceOf(owner)).to.be.bignumber.equal('0'); - expect(await this.token.balanceOf(this.receiver.address)).to.be.bignumber.equal(balance); + it('via transferAndCall', async function () { + await expect( + this.token.connect(this.holder).getFunction('transferAndCall(address,uint256,bytes)')(this.mock, value, data), + ) + .to.emit(this.token, 'Transfer') + .withArgs(this.holder.address, this.mock.target, value); }); it('via transferFromAndCall', async function () { - await this.token.approve(spender, balance, { from: owner }); - - const receipt = await this.token.methods['transferFromAndCall(address,address,uint256)']( - owner, - this.receiver.address, - balance, - { - from: spender, - }, - ); - - expectEvent(receipt, 'Transfer', { from: owner, to: this.receiver.address, value: balance }); + await this.token.connect(this.holder).approve(this.spender, value); - expect(await this.token.balanceOf(owner)).to.be.bignumber.equal('0'); - expect(await this.token.balanceOf(this.receiver.address)).to.be.bignumber.equal(balance); + await expect( + this.token.connect(this.spender).getFunction('transferFromAndCall(address,address,uint256,bytes)')( + this.holder, + this.mock, + value, + data, + ), + ) + .to.emit(this.token, 'Transfer') + .withArgs(this.holder.address, this.mock.target, value); }); }); describe('receives ERC1363 token approvals', function () { it('via approveAndCall', async function () { - const receipt = await this.token.methods['approveAndCall(address,uint256)'](this.receiver.address, balance, { - from: owner, - }); + expect(await this.token.allowance(this.holder, this.mock)).to.be.equal(0n); - expectEvent(receipt, 'Approval', { owner, spender: this.receiver.address, value: balance }); + await expect( + this.token.connect(this.holder).getFunction('approveAndCall(address,uint256,bytes)')(this.mock, value, data), + ) + .to.emit(this.token, 'Approval') + .withArgs(this.holder.address, this.mock.target, value); - expect(await this.token.allowance(owner, this.receiver.address)).to.be.bignumber.equal(balance); + expect(await this.token.allowance(this.holder, this.mock)).to.be.equal(value); }); }); }); diff --git a/test/utils/introspection/SupportsInterface.behavior.js b/test/utils/introspection/SupportsInterface.behavior.js index 7affbf97efb..d4d422d699c 100644 --- a/test/utils/introspection/SupportsInterface.behavior.js +++ b/test/utils/introspection/SupportsInterface.behavior.js @@ -138,7 +138,10 @@ function shouldSupportInterfaces(interfaces = []) { for (const fnName of INTERFACES[k]) { if (this.contractUnderTest.abi) { const fnSig = FN_SIGNATURES[fnName]; - expect(this.contractUnderTest.abi.filter(fn => fn.signature === fnSig).length).to.equal(1, `did not find ${fnName}`); + expect(this.contractUnderTest.abi.filter(fn => fn.signature === fnSig).length).to.equal( + 1, + `did not find ${fnName}`, + ); } else { expect(this.contractUnderTest.interface.getFunction(fnName)).to.be.an('object', `did not find ${fnName}`); } From a84420742da81c5d7309eb302b8a6fb8907965c4 Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Tue, 17 Oct 2023 11:43:11 +0200 Subject: [PATCH 48/82] improve SupportInterface.behavior.js --- .../introspection/SupportsInterface.behavior.js | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/test/utils/introspection/SupportsInterface.behavior.js b/test/utils/introspection/SupportsInterface.behavior.js index d4d422d699c..6761cc8982e 100644 --- a/test/utils/introspection/SupportsInterface.behavior.js +++ b/test/utils/introspection/SupportsInterface.behavior.js @@ -136,15 +136,13 @@ function shouldSupportInterfaces(interfaces = []) { // skip interfaces for which we don't have a function list if (INTERFACES[k] === undefined) continue; for (const fnName of INTERFACES[k]) { - if (this.contractUnderTest.abi) { - const fnSig = FN_SIGNATURES[fnName]; - expect(this.contractUnderTest.abi.filter(fn => fn.signature === fnSig).length).to.equal( - 1, - `did not find ${fnName}`, - ); - } else { - expect(this.contractUnderTest.interface.getFunction(fnName)).to.be.an('object', `did not find ${fnName}`); - } + const fnSig = FN_SIGNATURES[fnName]; + + const lookup = + this.contractUnderTest?.interface?.getFunction(fnName) ?? + this.contractUnderTest?.abi?.find(fn => fn.type === 'function' && fn.signature === fnSig); + + expect(lookup).to.be.an('object', `did not find ${fnName} that is required by ${k}`); } } }); From 178ff7d99248290be71b995a11da59ad7ace46c0 Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Tue, 17 Oct 2023 11:45:14 +0200 Subject: [PATCH 49/82] codespell --- contracts/token/ERC20/utils/SafeERC20.sol | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/contracts/token/ERC20/utils/SafeERC20.sol b/contracts/token/ERC20/utils/SafeERC20.sol index 91df39fbb1d..7bd70dd39fa 100644 --- a/contracts/token/ERC20/utils/SafeERC20.sol +++ b/contracts/token/ERC20/utils/SafeERC20.sol @@ -85,7 +85,7 @@ library SafeERC20 { /** * @dev Perform an {ERC1363} transferAndCall, with a fallback to the simple {ERC20} transfer if the target has no * code. This can be used to implement an {ERC721}-like safe transfer that rely on {ERC1363} checks when - * targetting contracts. + * targeting contracts. */ function transferAndCallRelaxed(IERC1363 token, address to, uint256 value, bytes memory data) internal { if (to.code.length == 0) { @@ -98,7 +98,7 @@ library SafeERC20 { /** * @dev Perform an {ERC1363} transferFromAndCall, with a fallback to the simple {ERC20} transferFrom if the target * has no code. This can be used to implement an {ERC721}-like safe transfer that rely on {ERC1363} checks when - * targetting contracts. + * targeting contracts. */ function transferFromAndCallRelaxed( IERC1363 token, @@ -117,7 +117,7 @@ library SafeERC20 { /** * @dev Perform an {ERC1363} approveAndCall, with a fallback to the simple {ERC20} approve if the target has no * code. This can be used to implement an {ERC721}-like safe transfer that rely on {ERC1363} checks when - * targetting contracts. + * targeting contracts. */ function approveAndCallRelaxed(IERC1363 token, address to, uint256 value, bytes memory data) internal { if (to.code.length == 0) { From 34a7a8de6e290807fbdd1a58cba7159668dd0957 Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Tue, 17 Oct 2023 12:08:28 +0200 Subject: [PATCH 50/82] test SafeERC20 relaxed functions --- test/token/ERC20/utils/SafeERC20.test.js | 80 +++++++++++++++++++++++- 1 file changed, 79 insertions(+), 1 deletion(-) diff --git a/test/token/ERC20/utils/SafeERC20.test.js b/test/token/ERC20/utils/SafeERC20.test.js index 4ff27f14d39..f8211f32cc7 100644 --- a/test/token/ERC20/utils/SafeERC20.test.js +++ b/test/token/ERC20/utils/SafeERC20.test.js @@ -5,6 +5,7 @@ const ERC20ReturnFalseMock = artifacts.require('$ERC20ReturnFalseMock'); const ERC20ReturnTrueMock = artifacts.require('$ERC20'); // default implementation returns true const ERC20NoReturnMock = artifacts.require('$ERC20NoReturnMock'); const ERC20ForceApproveMock = artifacts.require('$ERC20ForceApproveMock'); +const ERC1363 = artifacts.require('$ERC1363'); const { expectRevertCustomError } = require('../../../helpers/customError'); @@ -12,7 +13,7 @@ const name = 'ERC20Mock'; const symbol = 'ERC20Mock'; contract('SafeERC20', function (accounts) { - const [hasNoCode, receiver, spender] = accounts; + const [hasNoCode, owner, receiver, spender, other] = accounts; before(async function () { this.mock = await SafeERC20.new(); @@ -144,6 +145,83 @@ contract('SafeERC20', function (accounts) { }); }); }); + + describe('with ERC1363', function () { + const value = web3.utils.toBN(100); + + beforeEach(async function () { + this.token = await ERC1363.new(name, symbol); + }); + + shouldOnlyRevertOnErrors(accounts); + + describe('transferAndCall', function () { + it('cannot transferAndCall to an EOA directly', async function () { + await this.token.$_mint(owner, 100); + + await expectRevertCustomError( + this.token.methods['transferAndCall(address,uint256,bytes)'](receiver, value, '0x', { from: owner }), + 'ERC1363InvalidReceiver', + [receiver], + ); + }); + + it('can transferAndCall to an EOA using helper', async function () { + await this.token.$_mint(this.mock.address, value); + + const { tx } = await this.mock.$transferAndCallRelaxed(this.token.address, receiver, value, '0x'); + await expectEvent.inTransaction(tx, this.token, 'Transfer', { + from: this.mock.address, + to: receiver, + value, + }); + }); + }); + + describe('transferFromAndCall', function () { + it('cannot transferFromAndCall to an EOA directly', async function () { + await this.token.$_mint(owner, value); + await this.token.approve(other, constants.MAX_UINT256, { from: owner }); + + await expectRevertCustomError( + this.token.methods['transferFromAndCall(address,address,uint256,bytes)'](owner, receiver, value, '0x', { from: other }), + 'ERC1363InvalidReceiver', + [receiver], + ); + }); + + it('can transferFromAndCall to an EOA using helper', async function () { + await this.token.$_mint(owner, value); + await this.token.approve(this.mock.address, constants.MAX_UINT256, { from: owner }); + + const { tx } = await this.mock.$transferFromAndCallRelaxed(this.token.address, owner, receiver, value, '0x'); + await expectEvent.inTransaction(tx, this.token, 'Transfer', { + from: owner, + to: receiver, + value, + }); + }); + }); + + describe('approveAndCall', function () { + it('cannot approveAndCall to an EOA directly', async function () { + await expectRevertCustomError( + this.token.methods['approveAndCall(address,uint256,bytes)'](receiver, value, '0x'), + 'ERC1363InvalidSpender', + [receiver], + ); + }); + + it('can approveAndCall to an EOA using helper', async function () { + const { tx } = await this.mock.$approveAndCallRelaxed(this.token.address, receiver, value, '0x'); + await expectEvent.inTransaction(tx, this.token, 'Approval', { + owner: this.mock.address, + spender: receiver, + value, + }); + }); + }); + }); }); function shouldOnlyRevertOnErrors([owner, receiver, spender]) { From 7fde72d4e695cae7fcb46fa18e57470de4903f80 Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Tue, 17 Oct 2023 15:10:01 +0200 Subject: [PATCH 51/82] fix lint --- test/token/ERC20/utils/SafeERC20.test.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/test/token/ERC20/utils/SafeERC20.test.js b/test/token/ERC20/utils/SafeERC20.test.js index f8211f32cc7..c40b2105085 100644 --- a/test/token/ERC20/utils/SafeERC20.test.js +++ b/test/token/ERC20/utils/SafeERC20.test.js @@ -184,7 +184,9 @@ contract('SafeERC20', function (accounts) { await this.token.approve(other, constants.MAX_UINT256, { from: owner }); await expectRevertCustomError( - this.token.methods['transferFromAndCall(address,address,uint256,bytes)'](owner, receiver, value, '0x', { from: other }), + this.token.methods['transferFromAndCall(address,address,uint256,bytes)'](owner, receiver, value, '0x', { + from: other, + }), 'ERC1363InvalidReceiver', [receiver], ); From 5ec39cb50dfff92ce1523f0c3c8f5b9ced416e80 Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Tue, 17 Oct 2023 21:53:33 +0200 Subject: [PATCH 52/82] Apply suggestions from code review MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Ernesto García --- contracts/token/ERC20/extensions/ERC1363.sol | 19 +++++++------------ 1 file changed, 7 insertions(+), 12 deletions(-) diff --git a/contracts/token/ERC20/extensions/ERC1363.sol b/contracts/token/ERC20/extensions/ERC1363.sol index 5770543338c..39a81c1d335 100644 --- a/contracts/token/ERC20/extensions/ERC1363.sol +++ b/contracts/token/ERC20/extensions/ERC1363.sol @@ -11,7 +11,9 @@ import {IERC1363Spender} from "../../../interfaces/IERC1363Spender.sol"; /** * @title ERC1363 - * @dev Implementation of the ERC1363 interface. + * @dev Extension of {ERC20} tokens that adds support for code execution after transfers and approvals + * on recipient contracts. Calls after transfers are enabled through the {ERC1363-transferAndCall} and + * {ERC1363-transferFromAndCall} methods while calls after approvals can be made with {ERC1363-approveAndCall} */ abstract contract ERC1363 is ERC20, ERC165, IERC1363 { /** @@ -87,15 +89,11 @@ abstract contract ERC1363 is ERC20, ERC165, IERC1363 { } /** - * @dev Private function to invoke `onTransferReceived` on a target address. - * This will revert if the target doesn't implement the `IERC1363Receiver` interface or + * @dev Performs a call to {IERC1363-onTransferReceived} on a target address. + * This will revert if the target doesn't implement the {IERC1363Receiver} interface or * if the target doesn't accept the token transfer or * if the target address is not a contract. * - * @param from Address representing the previous owner of the given token amount. - * @param to Target address that will receive the tokens. - * @param value The amount of tokens to be transferred. - * @param data Optional data to send along with the call. */ function _checkOnTransferReceived(address from, address to, uint256 value, bytes memory data) private { if (to.code.length == 0) { @@ -119,14 +117,11 @@ abstract contract ERC1363 is ERC20, ERC165, IERC1363 { } /** - * @dev Private function to invoke `onApprovalReceived` on a target address. - * This will revert if the target doesn't implement the `IERC1363Spender` interface or + * @dev Performs a call to {IERC1363-onApprovalReceived} on a target address. + * This will revert if the target doesn't implement the {IERC1363Spender} interface or * if the target doesn't accept the token approval or * if the target address is not a contract. * - * @param spender The address which will spend the funds. - * @param value The amount of tokens to be spent. - * @param data Optional data to send along with the call. */ function _checkOnApprovalReceived(address spender, uint256 value, bytes memory data) private { if (spender.code.length == 0) { From eb0fb78d889d8899c40db22c9f1da72f35b0941c Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Tue, 17 Oct 2023 22:39:58 +0200 Subject: [PATCH 53/82] remove ERC1363Holder --- contracts/token/ERC20/README.adoc | 3 - contracts/token/ERC20/extensions/ERC1363.sol | 2 +- contracts/token/ERC20/utils/ERC1363Holder.sol | 32 -------- test/token/ERC20/utils/ERC1363Holder.test.js | 76 ------------------- 4 files changed, 1 insertion(+), 112 deletions(-) delete mode 100644 contracts/token/ERC20/utils/ERC1363Holder.sol delete mode 100644 test/token/ERC20/utils/ERC1363Holder.test.js diff --git a/contracts/token/ERC20/README.adoc b/contracts/token/ERC20/README.adoc index 10a95498614..e7fde62db7a 100644 --- a/contracts/token/ERC20/README.adoc +++ b/contracts/token/ERC20/README.adoc @@ -28,7 +28,6 @@ Additionally there are multiple custom extensions, including: Finally, there are some utilities to interact with ERC20 contracts in various ways: * {SafeERC20}: a wrapper around the interface that eliminates the need to handle boolean return values. -* {ERC1363Holder}: implementation of `IERC1363Receiver` and `IERC1363Spender` that will allow a contract to receive ERC1363 token transfers or approval. Other utilities that support ERC20 assets can be found in codebase: @@ -69,5 +68,3 @@ NOTE: This core set of contracts is designed to be unopinionated, allowing devel == Utilities {{SafeERC20}} - -{{ERC1363Holder}} diff --git a/contracts/token/ERC20/extensions/ERC1363.sol b/contracts/token/ERC20/extensions/ERC1363.sol index 39a81c1d335..0fe7a27c23a 100644 --- a/contracts/token/ERC20/extensions/ERC1363.sol +++ b/contracts/token/ERC20/extensions/ERC1363.sol @@ -13,7 +13,7 @@ import {IERC1363Spender} from "../../../interfaces/IERC1363Spender.sol"; * @title ERC1363 * @dev Extension of {ERC20} tokens that adds support for code execution after transfers and approvals * on recipient contracts. Calls after transfers are enabled through the {ERC1363-transferAndCall} and - * {ERC1363-transferFromAndCall} methods while calls after approvals can be made with {ERC1363-approveAndCall} + * {ERC1363-transferFromAndCall} methods while calls after approvals can be made with {ERC1363-approveAndCall} */ abstract contract ERC1363 is ERC20, ERC165, IERC1363 { /** diff --git a/contracts/token/ERC20/utils/ERC1363Holder.sol b/contracts/token/ERC20/utils/ERC1363Holder.sol deleted file mode 100644 index c1a784dd90a..00000000000 --- a/contracts/token/ERC20/utils/ERC1363Holder.sol +++ /dev/null @@ -1,32 +0,0 @@ -// SPDX-License-Identifier: MIT - -pragma solidity ^0.8.20; - -import {IERC1363Receiver} from "../../../interfaces/IERC1363Receiver.sol"; -import {IERC1363Spender} from "../../../interfaces/IERC1363Spender.sol"; - -/** - * @title ERC1363Holder - * @dev Implementation of `IERC1363Receiver` and `IERC1363Spender` that will allow a contract to receive ERC1363 token - * transfers or approval. - * - * IMPORTANT: When inheriting this contract, you must include a way to use the received tokens or spend the allowance, - * otherwise they will be stuck. - */ -abstract contract ERC1363Holder is IERC1363Receiver, IERC1363Spender { - /* - * NOTE: always returns `IERC1363Receiver.onTransferReceived.selector`. - * @inheritdoc IERC1363Receiver - */ - function onTransferReceived(address, address, uint256, bytes calldata) public virtual override returns (bytes4) { - return this.onTransferReceived.selector; - } - - /* - * NOTE: always returns `IERC1363Spender.onApprovalReceived.selector`. - * @inheritdoc IERC1363Spender - */ - function onApprovalReceived(address, uint256, bytes calldata) public virtual override returns (bytes4) { - return this.onApprovalReceived.selector; - } -} diff --git a/test/token/ERC20/utils/ERC1363Holder.test.js b/test/token/ERC20/utils/ERC1363Holder.test.js deleted file mode 100644 index e3e384d477a..00000000000 --- a/test/token/ERC20/utils/ERC1363Holder.test.js +++ /dev/null @@ -1,76 +0,0 @@ -const { ethers } = require('hardhat'); -const { expect } = require('chai'); -const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers'); - -const name = 'My Token'; -const symbol = 'MTKN'; -const value = 1000n; -const data = '0x123456'; - -async function fixture() { - const [holder, spender] = await ethers.getSigners(); - const mock = await ethers.deployContract('$ERC1363Holder'); - const token = await ethers.deployContract('$ERC1363', [name, symbol]); - await token.$_mint(holder, value); - return { - token, - mock, - holder, - spender, - }; -} - -describe('ERC1363Holder', function () { - beforeEach(async function () { - Object.assign(this, await loadFixture(fixture)); - }); - - describe('receives ERC1363 token transfers', function () { - beforeEach(async function () { - expect(await this.token.balanceOf(this.holder)).to.be.equal(value); - expect(await this.token.balanceOf(this.mock)).to.be.equal(0n); - }); - - afterEach(async function () { - expect(await this.token.balanceOf(this.holder)).to.be.equal(0n); - expect(await this.token.balanceOf(this.mock)).to.be.equal(value); - }); - - it('via transferAndCall', async function () { - await expect( - this.token.connect(this.holder).getFunction('transferAndCall(address,uint256,bytes)')(this.mock, value, data), - ) - .to.emit(this.token, 'Transfer') - .withArgs(this.holder.address, this.mock.target, value); - }); - - it('via transferFromAndCall', async function () { - await this.token.connect(this.holder).approve(this.spender, value); - - await expect( - this.token.connect(this.spender).getFunction('transferFromAndCall(address,address,uint256,bytes)')( - this.holder, - this.mock, - value, - data, - ), - ) - .to.emit(this.token, 'Transfer') - .withArgs(this.holder.address, this.mock.target, value); - }); - }); - - describe('receives ERC1363 token approvals', function () { - it('via approveAndCall', async function () { - expect(await this.token.allowance(this.holder, this.mock)).to.be.equal(0n); - - await expect( - this.token.connect(this.holder).getFunction('approveAndCall(address,uint256,bytes)')(this.mock, value, data), - ) - .to.emit(this.token, 'Approval') - .withArgs(this.holder.address, this.mock.target, value); - - expect(await this.token.allowance(this.holder, this.mock)).to.be.equal(value); - }); - }); -}); From b893eec04c3ab42e892d50ba4db9bb1d576d6e16 Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Tue, 17 Oct 2023 22:46:23 +0200 Subject: [PATCH 54/82] add changeset --- .changeset/nice-paws-pull.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/nice-paws-pull.md diff --git a/.changeset/nice-paws-pull.md b/.changeset/nice-paws-pull.md new file mode 100644 index 00000000000..11f48d51f82 --- /dev/null +++ b/.changeset/nice-paws-pull.md @@ -0,0 +1,5 @@ +--- +'openzeppelin-solidity': minor +--- + +`SafeERC20`: Add "relaxed" function for interacting with ERC-1363 functions in a way that is compatible with EOAs. From e13712188acb2394161a22acb001df39dfa07742 Mon Sep 17 00:00:00 2001 From: Vittorio Minacori Date: Wed, 18 Oct 2023 09:54:56 +0200 Subject: [PATCH 55/82] Fix documentation reference --- contracts/token/ERC20/extensions/ERC1363.sol | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/contracts/token/ERC20/extensions/ERC1363.sol b/contracts/token/ERC20/extensions/ERC1363.sol index 0fe7a27c23a..fadbf9e9364 100644 --- a/contracts/token/ERC20/extensions/ERC1363.sol +++ b/contracts/token/ERC20/extensions/ERC1363.sol @@ -89,11 +89,10 @@ abstract contract ERC1363 is ERC20, ERC165, IERC1363 { } /** - * @dev Performs a call to {IERC1363-onTransferReceived} on a target address. + * @dev Performs a call to {IERC1363Receiver-onTransferReceived} on a target address. * This will revert if the target doesn't implement the {IERC1363Receiver} interface or * if the target doesn't accept the token transfer or * if the target address is not a contract. - * */ function _checkOnTransferReceived(address from, address to, uint256 value, bytes memory data) private { if (to.code.length == 0) { @@ -117,11 +116,10 @@ abstract contract ERC1363 is ERC20, ERC165, IERC1363 { } /** - * @dev Performs a call to {IERC1363-onApprovalReceived} on a target address. + * @dev Performs a call to {IERC1363Spender-onApprovalReceived} on a target address. * This will revert if the target doesn't implement the {IERC1363Spender} interface or * if the target doesn't accept the token approval or * if the target address is not a contract. - * */ function _checkOnApprovalReceived(address spender, uint256 value, bytes memory data) private { if (spender.code.length == 0) { From 07bdbfd23aa4e2b256bbced476065990f5ba19ec Mon Sep 17 00:00:00 2001 From: Vittorio Minacori Date: Fri, 20 Oct 2023 11:20:15 +0200 Subject: [PATCH 56/82] Use external instead of public in mock (as it is in interface) --- contracts/mocks/token/ERC1363ReceiverMock.sol | 2 +- contracts/mocks/token/ERC1363SpenderMock.sol | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/contracts/mocks/token/ERC1363ReceiverMock.sol b/contracts/mocks/token/ERC1363ReceiverMock.sol index e92f8516504..d33e05e42f7 100644 --- a/contracts/mocks/token/ERC1363ReceiverMock.sol +++ b/contracts/mocks/token/ERC1363ReceiverMock.sol @@ -34,7 +34,7 @@ contract ERC1363ReceiverMock is IERC1363Receiver { address from, uint256 value, bytes calldata data - ) public override returns (bytes4) { + ) external override returns (bytes4) { if (_error == RevertType.RevertWithoutMessage) { revert(); } else if (_error == RevertType.RevertWithMessage) { diff --git a/contracts/mocks/token/ERC1363SpenderMock.sol b/contracts/mocks/token/ERC1363SpenderMock.sol index 52f84472ef7..b12c4c1d981 100644 --- a/contracts/mocks/token/ERC1363SpenderMock.sol +++ b/contracts/mocks/token/ERC1363SpenderMock.sol @@ -29,7 +29,7 @@ contract ERC1363SpenderMock is IERC1363Spender { _error = error; } - function onApprovalReceived(address owner, uint256 value, bytes calldata data) public override returns (bytes4) { + function onApprovalReceived(address owner, uint256 value, bytes calldata data) external override returns (bytes4) { if (_error == RevertType.RevertWithoutMessage) { revert(); } else if (_error == RevertType.RevertWithMessage) { From 44403450ca07ca23a4b1adda685e2d7eaf79ae48 Mon Sep 17 00:00:00 2001 From: Vittorio Minacori Date: Thu, 26 Oct 2023 23:11:10 +0200 Subject: [PATCH 57/82] Typo --- contracts/interfaces/IERC1363.sol | 3 +-- contracts/interfaces/IERC1363Spender.sol | 4 ++-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/contracts/interfaces/IERC1363.sol b/contracts/interfaces/IERC1363.sol index 7b39463acf8..e1e2fdd4e98 100644 --- a/contracts/interfaces/IERC1363.sol +++ b/contracts/interfaces/IERC1363.sol @@ -8,8 +8,7 @@ import {IERC165} from "./IERC165.sol"; /** * @title IERC1363 - * @dev Interface of the ERC1363 standard as defined in the - * https://eips.ethereum.org/EIPS/eip-1363[EIP-1363]. + * @dev Interface of the ERC1363 standard as defined in the https://eips.ethereum.org/EIPS/eip-1363[ERC-1363]. * * Defines an extension interface for ERC20 tokens that supports executing code on a recipient contract * after `transfer` or `transferFrom`, or code on a spender contract after `approve`, in a single transaction. diff --git a/contracts/interfaces/IERC1363Spender.sol b/contracts/interfaces/IERC1363Spender.sol index a76376a7bb2..ce0d5134093 100644 --- a/contracts/interfaces/IERC1363Spender.sol +++ b/contracts/interfaces/IERC1363Spender.sol @@ -10,8 +10,8 @@ pragma solidity ^0.8.20; */ interface IERC1363Spender { /** - * @dev Whenever an ERC1363 tokens `owner` approved this contract via `approveAndCall` - * to spent their tokens, this function is called. + * @dev Whenever an ERC-1363 token `owner` approves this contract via `approveAndCall` + * to spend their tokens, this function is called. * * NOTE: To accept the approval, this must return * `bytes4(keccak256("onApprovalReceived(address,uint256,bytes)"))` From 3dff6e270b64773a3e59c272ad1b6d287b0f776a Mon Sep 17 00:00:00 2001 From: Vittorio Minacori Date: Fri, 27 Oct 2023 10:16:43 +0200 Subject: [PATCH 58/82] Update contracts/interfaces/IERC1363Spender.sol Co-authored-by: Hadrien Croubois --- contracts/interfaces/IERC1363Spender.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/interfaces/IERC1363Spender.sol b/contracts/interfaces/IERC1363Spender.sol index ce0d5134093..09469f87701 100644 --- a/contracts/interfaces/IERC1363Spender.sol +++ b/contracts/interfaces/IERC1363Spender.sol @@ -10,7 +10,7 @@ pragma solidity ^0.8.20; */ interface IERC1363Spender { /** - * @dev Whenever an ERC-1363 token `owner` approves this contract via `approveAndCall` + * @dev Whenever an ERC-1363 token `owner` approves this contract via `approveAndCall` * to spend their tokens, this function is called. * * NOTE: To accept the approval, this must return From 81d191fa7eb805cb2fe40374773627fea78bf2de Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Fri, 27 Oct 2023 10:34:58 +0200 Subject: [PATCH 59/82] Apply suggestions from code review MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Ernesto García --- contracts/interfaces/IERC1363.sol | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/contracts/interfaces/IERC1363.sol b/contracts/interfaces/IERC1363.sol index e1e2fdd4e98..000bd45b749 100644 --- a/contracts/interfaces/IERC1363.sol +++ b/contracts/interfaces/IERC1363.sol @@ -27,7 +27,7 @@ interface IERC1363 is IERC20, IERC165 { /** * @dev Moves a `value` amount of tokens from the caller's account to `to` - * and then calls `onTransferReceived` on `to`. + * and then calls {IERC1363Receiver-onTransferReceived} on `to`. * @param to The address which you want to transfer to. * @param value The amount of tokens to be transferred. * @return A boolean value indicating whether the operation succeeded unless throwing. @@ -36,7 +36,7 @@ interface IERC1363 is IERC20, IERC165 { /** * @dev Moves a `value` amount of tokens from the caller's account to `to` - * and then calls `onTransferReceived` on `to`. + * and then calls {IERC1363Receiver-onTransferReceived} on `to`. * @param to The address which you want to transfer to. * @param value The amount of tokens to be transferred. * @param data Additional data with no specified format, sent in call to `to`. @@ -46,7 +46,7 @@ interface IERC1363 is IERC20, IERC165 { /** * @dev Moves a `value` amount of tokens from `from` to `to` using the allowance mechanism - * and then calls `onTransferReceived` on `to`. + * and then calls {IERC1363Receiver-onTransferReceived} on `to`. * @param from The address which you want to send tokens from. * @param to The address which you want to transfer to. * @param value The amount of tokens to be transferred. @@ -56,7 +56,7 @@ interface IERC1363 is IERC20, IERC165 { /** * @dev Moves a `value` amount of tokens from `from` to `to` using the allowance mechanism - * and then calls `onTransferReceived` on `to`. + * and then calls {IERC1363Receiver-onTransferReceived} on `to`. * @param from The address which you want to send tokens from. * @param to The address which you want to transfer to. * @param value The amount of tokens to be transferred. @@ -67,7 +67,7 @@ interface IERC1363 is IERC20, IERC165 { /** * @dev Sets a `value` amount of tokens as the allowance of `spender` over the - * caller's tokens and then calls `onApprovalReceived` on `spender`. + * caller's tokens and then calls {IERC1363Spender-onApprovalReceived} on `spender`. * @param spender The address which will spend the funds. * @param value The amount of tokens to be spent. * @return A boolean value indicating whether the operation succeeded unless throwing. @@ -76,7 +76,7 @@ interface IERC1363 is IERC20, IERC165 { /** * @dev Sets a `value` amount of tokens as the allowance of `spender` over the - * caller's tokens and then calls `onApprovalReceived` on `spender`. + * caller's tokens and then calls {IERC1363Spender-onApprovalReceived} on `spender`. * @param spender The address which will spend the funds. * @param value The amount of tokens to be spent. * @param data Additional data with no specified format, sent in call to `spender`. From c3fa31409f4c150b1bd3142393029c1711bdb3a9 Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Fri, 27 Oct 2023 16:02:59 +0200 Subject: [PATCH 60/82] test coverage --- test/token/ERC20/utils/SafeERC20.test.js | 70 ++++++++++++++++++++++-- 1 file changed, 64 insertions(+), 6 deletions(-) diff --git a/test/token/ERC20/utils/SafeERC20.test.js b/test/token/ERC20/utils/SafeERC20.test.js index c40b2105085..10d04ce3393 100644 --- a/test/token/ERC20/utils/SafeERC20.test.js +++ b/test/token/ERC20/utils/SafeERC20.test.js @@ -6,6 +6,8 @@ const ERC20ReturnTrueMock = artifacts.require('$ERC20'); // default implementati const ERC20NoReturnMock = artifacts.require('$ERC20NoReturnMock'); const ERC20ForceApproveMock = artifacts.require('$ERC20ForceApproveMock'); const ERC1363 = artifacts.require('$ERC1363'); +const ERC1363ReceiverMock = artifacts.require('ERC1363ReceiverMock'); +const ERC1363SpenderMock = artifacts.require('ERC1363SpenderMock'); const { expectRevertCustomError } = require('../../../helpers/customError'); @@ -148,6 +150,7 @@ contract('SafeERC20', function (accounts) { describe('with ERC1363', function () { const value = web3.utils.toBN(100); + const data = '0x12345678'; beforeEach(async function () { this.token = await ERC1363.new(name, symbol); @@ -160,7 +163,7 @@ contract('SafeERC20', function (accounts) { await this.token.$_mint(owner, 100); await expectRevertCustomError( - this.token.methods['transferAndCall(address,uint256,bytes)'](receiver, value, '0x', { from: owner }), + this.token.methods['transferAndCall(address,uint256,bytes)'](receiver, value, data, { from: owner }), 'ERC1363InvalidReceiver', [receiver], ); @@ -169,13 +172,32 @@ contract('SafeERC20', function (accounts) { it('can transferAndCall to an EOA using helper', async function () { await this.token.$_mint(this.mock.address, value); - const { tx } = await this.mock.$transferAndCallRelaxed(this.token.address, receiver, value, '0x'); + const { tx } = await this.mock.$transferAndCallRelaxed(this.token.address, receiver, value, data); await expectEvent.inTransaction(tx, this.token, 'Transfer', { from: this.mock.address, to: receiver, value, }); }); + + it('can transferAndCall to an ERC1363Receiver using helper', async function () { + const receiver = await ERC1363ReceiverMock.new(); + + await this.token.$_mint(this.mock.address, value); + + const { tx } = await this.mock.$transferAndCallRelaxed(this.token.address, receiver.address, value, data); + await expectEvent.inTransaction(tx, this.token, 'Transfer', { + from: this.mock.address, + to: receiver.address, + value, + }); + await expectEvent.inTransaction(tx, receiver, 'Received', { + operator: this.mock.address, + from: this.mock.address, + value, + data, + }); + }); }); describe('transferFromAndCall', function () { @@ -184,7 +206,7 @@ contract('SafeERC20', function (accounts) { await this.token.approve(other, constants.MAX_UINT256, { from: owner }); await expectRevertCustomError( - this.token.methods['transferFromAndCall(address,address,uint256,bytes)'](owner, receiver, value, '0x', { + this.token.methods['transferFromAndCall(address,address,uint256,bytes)'](owner, receiver, value, data, { from: other, }), 'ERC1363InvalidReceiver', @@ -196,32 +218,68 @@ contract('SafeERC20', function (accounts) { await this.token.$_mint(owner, value); await this.token.approve(this.mock.address, constants.MAX_UINT256, { from: owner }); - const { tx } = await this.mock.$transferFromAndCallRelaxed(this.token.address, owner, receiver, value, '0x'); + const { tx } = await this.mock.$transferFromAndCallRelaxed(this.token.address, owner, receiver, value, data); await expectEvent.inTransaction(tx, this.token, 'Transfer', { from: owner, to: receiver, value, }); }); + + it('can transferFromAndCall to an ERC1363Receiver using helper', async function () { + const receiver = await ERC1363ReceiverMock.new(); + + await this.token.$_mint(owner, value); + await this.token.approve(this.mock.address, constants.MAX_UINT256, { from: owner }); + + const { tx } = await this.mock.$transferFromAndCallRelaxed(this.token.address, owner, receiver.address, value, data); + await expectEvent.inTransaction(tx, this.token, 'Transfer', { + from: owner, + to: receiver.address, + value, + }); + await expectEvent.inTransaction(tx, receiver, 'Received', { + operator: this.mock.address, + from: owner, + value, + data, + }); + }); }); describe('approveAndCall', function () { it('cannot approveAndCall to an EOA directly', async function () { await expectRevertCustomError( - this.token.methods['approveAndCall(address,uint256,bytes)'](receiver, value, '0x'), + this.token.methods['approveAndCall(address,uint256,bytes)'](receiver, value, data), 'ERC1363InvalidSpender', [receiver], ); }); it('can approveAndCall to an EOA using helper', async function () { - const { tx } = await this.mock.$approveAndCallRelaxed(this.token.address, receiver, value, '0x'); + const { tx } = await this.mock.$approveAndCallRelaxed(this.token.address, receiver, value, data); await expectEvent.inTransaction(tx, this.token, 'Approval', { owner: this.mock.address, spender: receiver, value, }); }); + + it('can approveAndCall to an ERC1363Spender using helper', async function () { + const spender = await ERC1363SpenderMock.new(); + + const { tx } = await this.mock.$approveAndCallRelaxed(this.token.address, spender.address, value, data); + await expectEvent.inTransaction(tx, this.token, 'Approval', { + owner: this.mock.address, + spender: spender.address, + value, + }); + await expectEvent.inTransaction(tx, spender, 'Approved', { + owner: this.mock.address, + value, + data, + }); + }); }); }); }); From 09049d35377eceb18cb9de5d9774b7af88aa0061 Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Fri, 27 Oct 2023 16:19:11 +0200 Subject: [PATCH 61/82] lint --- test/token/ERC20/utils/SafeERC20.test.js | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/test/token/ERC20/utils/SafeERC20.test.js b/test/token/ERC20/utils/SafeERC20.test.js index 10d04ce3393..5b1de14c318 100644 --- a/test/token/ERC20/utils/SafeERC20.test.js +++ b/test/token/ERC20/utils/SafeERC20.test.js @@ -150,7 +150,7 @@ contract('SafeERC20', function (accounts) { describe('with ERC1363', function () { const value = web3.utils.toBN(100); - const data = '0x12345678'; + const data = '0x12345678'; beforeEach(async function () { this.token = await ERC1363.new(name, symbol); @@ -232,7 +232,13 @@ contract('SafeERC20', function (accounts) { await this.token.$_mint(owner, value); await this.token.approve(this.mock.address, constants.MAX_UINT256, { from: owner }); - const { tx } = await this.mock.$transferFromAndCallRelaxed(this.token.address, owner, receiver.address, value, data); + const { tx } = await this.mock.$transferFromAndCallRelaxed( + this.token.address, + owner, + receiver.address, + value, + data, + ); await expectEvent.inTransaction(tx, this.token, 'Transfer', { from: owner, to: receiver.address, From 924b50d788c6066ffa2a97269bab6826a3de0434 Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Fri, 15 Dec 2023 10:27:50 +0100 Subject: [PATCH 62/82] re-enable shouldBehaveLikeERC20 + minor test cleanup --- test/helpers/enums.js | 2 + test/token/ERC1155/ERC1155.behavior.js | 3 +- test/token/ERC20/ERC20.behavior.js | 96 ++++++++++--------- test/token/ERC20/ERC20.test.js | 38 ++++---- test/token/ERC20/extensions/ERC1363.test.js | 19 ++-- .../ERC20/extensions/ERC20FlashMint.test.js | 8 +- .../ERC20/extensions/ERC20Permit.test.js | 8 +- .../ERC20/extensions/ERC20Wrapper.test.js | 90 ++++++++--------- test/token/ERC721/ERC721.behavior.js | 6 +- 9 files changed, 139 insertions(+), 131 deletions(-) diff --git a/test/helpers/enums.js b/test/helpers/enums.js index b75e73ba8dc..4d5a16ae35f 100644 --- a/test/helpers/enums.js +++ b/test/helpers/enums.js @@ -14,6 +14,8 @@ function createExport(Enum) { VoteType: Enum('Against', 'For', 'Abstain'), Rounding: Enum('Floor', 'Ceil', 'Trunc', 'Expand'), OperationState: Enum('Unset', 'Waiting', 'Ready', 'Done'), + // used by mocks + RevertType: Enum('None', 'RevertWithoutMessage', 'RevertWithMessage', 'RevertWithCustomError', 'Panic'), }; } diff --git a/test/token/ERC1155/ERC1155.behavior.js b/test/token/ERC1155/ERC1155.behavior.js index 8df30a81460..799b3df9b43 100644 --- a/test/token/ERC1155/ERC1155.behavior.js +++ b/test/token/ERC1155/ERC1155.behavior.js @@ -4,10 +4,9 @@ const { ZERO_ADDRESS } = constants; const { shouldSupportInterfaces } = require('../../utils/introspection/SupportsInterface.behavior'); const { expectRevertCustomError } = require('../../helpers/customError'); -const { Enum } = require('../../helpers/enums'); +const { RevertType } = require('../../helpers/enums'); const ERC1155ReceiverMock = artifacts.require('ERC1155ReceiverMock'); -const RevertType = Enum('None', 'RevertWithoutMessage', 'RevertWithMessage', 'RevertWithCustomError', 'Panic'); function shouldBehaveLikeERC1155([minter, firstTokenHolder, secondTokenHolder, multiTokenHolder, recipient, proxy]) { const firstTokenId = new BN(1); diff --git a/test/token/ERC20/ERC20.behavior.js b/test/token/ERC20/ERC20.behavior.js index 522df3fcf0e..48d96fafd4f 100644 --- a/test/token/ERC20/ERC20.behavior.js +++ b/test/token/ERC20/ERC20.behavior.js @@ -4,17 +4,21 @@ const { expect } = require('chai'); function shouldBehaveLikeERC20(initialSupply, opts = {}) { const { forcedApproval } = opts; + beforeEach(async function () { + [this.holder, this.recipient, this.other] = this.accounts; + }); + it('total supply: returns the total token value', async function () { expect(await this.token.totalSupply()).to.equal(initialSupply); }); describe('balanceOf', function () { it('returns zero when the requested account has no tokens', async function () { - expect(await this.token.balanceOf(this.anotherAccount)).to.equal(0n); + expect(await this.token.balanceOf(this.other)).to.equal(0n); }); it('returns the total token value when the requested account has some tokens', async function () { - expect(await this.token.balanceOf(this.initialHolder)).to.equal(initialSupply); + expect(await this.token.balanceOf(this.holder)).to.equal(initialSupply); }); }); @@ -31,7 +35,7 @@ function shouldBehaveLikeERC20(initialSupply, opts = {}) { describe('when the recipient is not the zero address', function () { describe('when the spender has enough allowance', function () { beforeEach(async function () { - await this.token.connect(this.initialHolder).approve(this.recipient, initialSupply); + await this.token.connect(this.holder).approve(this.recipient, initialSupply); }); describe('when the token owner has enough balance', function () { @@ -40,25 +44,25 @@ function shouldBehaveLikeERC20(initialSupply, opts = {}) { beforeEach(async function () { this.tx = await this.token .connect(this.recipient) - .transferFrom(this.initialHolder, this.anotherAccount, value); + .transferFrom(this.holder, this.other, value); }); it('transfers the requested value', async function () { await expect(this.tx).to.changeTokenBalances( this.token, - [this.initialHolder, this.anotherAccount], + [this.holder, this.other], [-value, value], ); }); it('decreases the spender allowance', async function () { - expect(await this.token.allowance(this.initialHolder, this.recipient)).to.equal(0n); + expect(await this.token.allowance(this.holder, this.recipient)).to.equal(0n); }); it('emits a transfer event', async function () { await expect(this.tx) .to.emit(this.token, 'Transfer') - .withArgs(this.initialHolder.address, this.anotherAccount.address, value); + .withArgs(this.holder.address, this.other.address, value); }); if (forcedApproval) { @@ -66,9 +70,9 @@ function shouldBehaveLikeERC20(initialSupply, opts = {}) { await expect(this.tx) .to.emit(this.token, 'Approval') .withArgs( - this.initialHolder.address, + this.holder.address, this.recipient.address, - await this.token.allowance(this.initialHolder, this.recipient), + await this.token.allowance(this.holder, this.recipient), ); }); } else { @@ -80,12 +84,12 @@ function shouldBehaveLikeERC20(initialSupply, opts = {}) { it('reverts when the token owner does not have enough balance', async function () { const value = initialSupply; - await this.token.connect(this.initialHolder).transfer(this.anotherAccount, 1n); + await this.token.connect(this.holder).transfer(this.other, 1n); await expect( - this.token.connect(this.recipient).transferFrom(this.initialHolder, this.anotherAccount, value), + this.token.connect(this.recipient).transferFrom(this.holder, this.other, value), ) .to.revertedWithCustomError(this.token, 'ERC20InsufficientBalance') - .withArgs(this.initialHolder.address, value - 1n, value); + .withArgs(this.holder.address, value - 1n, value); }); }); @@ -93,13 +97,13 @@ function shouldBehaveLikeERC20(initialSupply, opts = {}) { const allowance = initialSupply - 1n; beforeEach(async function () { - await this.token.connect(this.initialHolder).approve(this.recipient, allowance); + await this.token.connect(this.holder).approve(this.recipient, allowance); }); it('reverts when the token owner has enough balance', async function () { const value = initialSupply; await expect( - this.token.connect(this.recipient).transferFrom(this.initialHolder, this.anotherAccount, value), + this.token.connect(this.recipient).transferFrom(this.holder, this.other, value), ) .to.be.revertedWithCustomError(this.token, 'ERC20InsufficientAllowance') .withArgs(this.recipient.address, allowance, value); @@ -107,25 +111,25 @@ function shouldBehaveLikeERC20(initialSupply, opts = {}) { it('reverts when the token owner does not have enough balance', async function () { const value = allowance; - await this.token.connect(this.initialHolder).transfer(this.anotherAccount, 2); + await this.token.connect(this.holder).transfer(this.other, 2); await expect( - this.token.connect(this.recipient).transferFrom(this.initialHolder, this.anotherAccount, value), + this.token.connect(this.recipient).transferFrom(this.holder, this.other, value), ) .to.be.revertedWithCustomError(this.token, 'ERC20InsufficientBalance') - .withArgs(this.initialHolder.address, value - 1n, value); + .withArgs(this.holder.address, value - 1n, value); }); }); describe('when the spender has unlimited allowance', function () { beforeEach(async function () { - await this.token.connect(this.initialHolder).approve(this.recipient, ethers.MaxUint256); + await this.token.connect(this.holder).approve(this.recipient, ethers.MaxUint256); this.tx = await this.token .connect(this.recipient) - .transferFrom(this.initialHolder, this.anotherAccount, 1n); + .transferFrom(this.holder, this.other, 1n); }); it('does not decrease the spender allowance', async function () { - expect(await this.token.allowance(this.initialHolder, this.recipient)).to.equal(ethers.MaxUint256); + expect(await this.token.allowance(this.holder, this.recipient)).to.equal(ethers.MaxUint256); }); it('does not emit an approval event', async function () { @@ -136,8 +140,8 @@ function shouldBehaveLikeERC20(initialSupply, opts = {}) { it('reverts when the recipient is the zero address', async function () { const value = initialSupply; - await this.token.connect(this.initialHolder).approve(this.recipient, value); - await expect(this.token.connect(this.recipient).transferFrom(this.initialHolder, ethers.ZeroAddress, value)) + await this.token.connect(this.holder).approve(this.recipient, value); + await expect(this.token.connect(this.recipient).transferFrom(this.holder, ethers.ZeroAddress, value)) .to.be.revertedWithCustomError(this.token, 'ERC20InvalidReceiver') .withArgs(ethers.ZeroAddress); }); @@ -164,26 +168,26 @@ function shouldBehaveLikeERC20Transfer(balance) { describe('when the recipient is not the zero address', function () { it('reverts when the sender does not have enough balance', async function () { const value = balance + 1n; - await expect(this.transfer(this.initialHolder, this.recipient, value)) + await expect(this.transfer(this.holder, this.recipient, value)) .to.be.revertedWithCustomError(this.token, 'ERC20InsufficientBalance') - .withArgs(this.initialHolder.address, balance, value); + .withArgs(this.holder.address, balance, value); }); describe('when the sender transfers all balance', function () { const value = balance; beforeEach(async function () { - this.tx = await this.transfer(this.initialHolder, this.recipient, value); + this.tx = await this.transfer(this.holder, this.recipient, value); }); it('transfers the requested value', async function () { - await expect(this.tx).to.changeTokenBalances(this.token, [this.initialHolder, this.recipient], [-value, value]); + await expect(this.tx).to.changeTokenBalances(this.token, [this.holder, this.recipient], [-value, value]); }); it('emits a transfer event', async function () { await expect(this.tx) .to.emit(this.token, 'Transfer') - .withArgs(this.initialHolder.address, this.recipient.address, value); + .withArgs(this.holder.address, this.recipient.address, value); }); }); @@ -191,23 +195,23 @@ function shouldBehaveLikeERC20Transfer(balance) { const value = 0n; beforeEach(async function () { - this.tx = await this.transfer(this.initialHolder, this.recipient, value); + this.tx = await this.transfer(this.holder, this.recipient, value); }); it('transfers the requested value', async function () { - await expect(this.tx).to.changeTokenBalances(this.token, [this.initialHolder, this.recipient], [0n, 0n]); + await expect(this.tx).to.changeTokenBalances(this.token, [this.holder, this.recipient], [0n, 0n]); }); it('emits a transfer event', async function () { await expect(this.tx) .to.emit(this.token, 'Transfer') - .withArgs(this.initialHolder.address, this.recipient.address, value); + .withArgs(this.holder.address, this.recipient.address, value); }); }); }); it('reverts when the recipient is the zero address', async function () { - await expect(this.transfer(this.initialHolder, ethers.ZeroAddress, balance)) + await expect(this.transfer(this.holder, ethers.ZeroAddress, balance)) .to.be.revertedWithCustomError(this.token, 'ERC20InvalidReceiver') .withArgs(ethers.ZeroAddress); }); @@ -219,22 +223,22 @@ function shouldBehaveLikeERC20Approve(supply) { const value = supply; it('emits an approval event', async function () { - await expect(this.approve(this.initialHolder, this.recipient, value)) + await expect(this.approve(this.holder, this.recipient, value)) .to.emit(this.token, 'Approval') - .withArgs(this.initialHolder.address, this.recipient.address, value); + .withArgs(this.holder.address, this.recipient.address, value); }); it('approves the requested value when there was no approved value before', async function () { - await this.approve(this.initialHolder, this.recipient, value); + await this.approve(this.holder, this.recipient, value); - expect(await this.token.allowance(this.initialHolder, this.recipient)).to.equal(value); + expect(await this.token.allowance(this.holder, this.recipient)).to.equal(value); }); it('approves the requested value and replaces the previous one when the spender had an approved value', async function () { - await this.approve(this.initialHolder, this.recipient, 1n); - await this.approve(this.initialHolder, this.recipient, value); + await this.approve(this.holder, this.recipient, 1n); + await this.approve(this.holder, this.recipient, value); - expect(await this.token.allowance(this.initialHolder, this.recipient)).to.equal(value); + expect(await this.token.allowance(this.holder, this.recipient)).to.equal(value); }); }); @@ -242,28 +246,28 @@ function shouldBehaveLikeERC20Approve(supply) { const value = supply + 1n; it('emits an approval event', async function () { - await expect(this.approve(this.initialHolder, this.recipient, value)) + await expect(this.approve(this.holder, this.recipient, value)) .to.emit(this.token, 'Approval') - .withArgs(this.initialHolder.address, this.recipient.address, value); + .withArgs(this.holder.address, this.recipient.address, value); }); it('approves the requested value when there was no approved value before', async function () { - await this.approve(this.initialHolder, this.recipient, value); + await this.approve(this.holder, this.recipient, value); - expect(await this.token.allowance(this.initialHolder, this.recipient)).to.equal(value); + expect(await this.token.allowance(this.holder, this.recipient)).to.equal(value); }); it('approves the requested value and replaces the previous one when the spender had an approved value', async function () { - await this.approve(this.initialHolder, this.recipient, 1n); - await this.approve(this.initialHolder, this.recipient, value); + await this.approve(this.holder, this.recipient, 1n); + await this.approve(this.holder, this.recipient, value); - expect(await this.token.allowance(this.initialHolder, this.recipient)).to.equal(value); + expect(await this.token.allowance(this.holder, this.recipient)).to.equal(value); }); }); }); it('reverts when the spender is the zero address', async function () { - await expect(this.approve(this.initialHolder, ethers.ZeroAddress, supply)) + await expect(this.approve(this.holder, ethers.ZeroAddress, supply)) .to.be.revertedWithCustomError(this.token, `ERC20InvalidSpender`) .withArgs(ethers.ZeroAddress); }); diff --git a/test/token/ERC20/ERC20.test.js b/test/token/ERC20/ERC20.test.js index 97037a697a7..b73f934ee78 100644 --- a/test/token/ERC20/ERC20.test.js +++ b/test/token/ERC20/ERC20.test.js @@ -19,12 +19,14 @@ describe('ERC20', function () { for (const { Token, forcedApproval } of TOKENS) { describe(Token, function () { const fixture = async () => { - const [initialHolder, recipient, anotherAccount] = await ethers.getSigners(); + // this.accounts is used by shouldBehaveLikeERC20 + const accounts = await ethers.getSigners(); + const [holder, recipient] = accounts; const token = await ethers.deployContract(Token, [name, symbol]); - await token.$_mint(initialHolder, initialSupply); + await token.$_mint(holder, initialSupply); - return { initialHolder, recipient, anotherAccount, token }; + return { accounts, holder, recipient, token }; }; beforeEach(async function () { @@ -89,29 +91,29 @@ describe('ERC20', function () { describe('for a non zero account', function () { it('rejects burning more than balance', async function () { - await expect(this.token.$_burn(this.initialHolder, initialSupply + 1n)) + await expect(this.token.$_burn(this.holder, initialSupply + 1n)) .to.be.revertedWithCustomError(this.token, 'ERC20InsufficientBalance') - .withArgs(this.initialHolder.address, initialSupply, initialSupply + 1n); + .withArgs(this.holder.address, initialSupply, initialSupply + 1n); }); const describeBurn = function (description, value) { describe(description, function () { beforeEach('burning', async function () { - this.tx = await this.token.$_burn(this.initialHolder, value); + this.tx = await this.token.$_burn(this.holder, value); }); it('decrements totalSupply', async function () { expect(await this.token.totalSupply()).to.equal(initialSupply - value); }); - it('decrements initialHolder balance', async function () { - await expect(this.tx).to.changeTokenBalance(this.token, this.initialHolder, -value); + it('decrements holder balance', async function () { + await expect(this.tx).to.changeTokenBalance(this.token, this.holder, -value); }); it('emits Transfer event', async function () { await expect(this.tx) .to.emit(this.token, 'Transfer') - .withArgs(this.initialHolder.address, ethers.ZeroAddress, value); + .withArgs(this.holder.address, ethers.ZeroAddress, value); }); }); }; @@ -129,23 +131,23 @@ describe('ERC20', function () { }); it('from is the zero address', async function () { - const tx = await this.token.$_update(ethers.ZeroAddress, this.initialHolder, value); + const tx = await this.token.$_update(ethers.ZeroAddress, this.holder, value); await expect(tx) .to.emit(this.token, 'Transfer') - .withArgs(ethers.ZeroAddress, this.initialHolder.address, value); + .withArgs(ethers.ZeroAddress, this.holder.address, value); expect(await this.token.totalSupply()).to.equal(this.totalSupply + value); - await expect(tx).to.changeTokenBalance(this.token, this.initialHolder, value); + await expect(tx).to.changeTokenBalance(this.token, this.holder, value); }); it('to is the zero address', async function () { - const tx = await this.token.$_update(this.initialHolder, ethers.ZeroAddress, value); + const tx = await this.token.$_update(this.holder, ethers.ZeroAddress, value); await expect(tx) .to.emit(this.token, 'Transfer') - .withArgs(this.initialHolder.address, ethers.ZeroAddress, value); + .withArgs(this.holder.address, ethers.ZeroAddress, value); expect(await this.token.totalSupply()).to.equal(this.totalSupply - value); - await expect(tx).to.changeTokenBalance(this.token, this.initialHolder, -value); + await expect(tx).to.changeTokenBalance(this.token, this.holder, -value); }); describe('from and to are the same address', function () { @@ -165,11 +167,11 @@ describe('ERC20', function () { }); it('executes with balance', async function () { - const tx = await this.token.$_update(this.initialHolder, this.initialHolder, value); - await expect(tx).to.changeTokenBalance(this.token, this.initialHolder, 0n); + const tx = await this.token.$_update(this.holder, this.holder, value); + await expect(tx).to.changeTokenBalance(this.token, this.holder, 0n); await expect(tx) .to.emit(this.token, 'Transfer') - .withArgs(this.initialHolder.address, this.initialHolder.address, value); + .withArgs(this.holder.address, this.holder.address, value); }); }); }); diff --git a/test/token/ERC20/extensions/ERC1363.test.js b/test/token/ERC20/extensions/ERC1363.test.js index f7e3c3ddb29..967f23c99e1 100644 --- a/test/token/ERC20/extensions/ERC1363.test.js +++ b/test/token/ERC20/extensions/ERC1363.test.js @@ -2,11 +2,9 @@ const { ethers } = require('hardhat'); const { expect } = require('chai'); const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers'); +const { shouldBehaveLikeERC20 } = require('../ERC20.behavior.js'); const { shouldSupportInterfaces } = require('../../../utils/introspection/SupportsInterface.behavior'); -const { - bigint: { Enum }, -} = require('../../../helpers/enums.js'); -const RevertType = Enum('None', 'RevertWithoutMessage', 'RevertWithMessage', 'RevertWithCustomError', 'Panic'); +const { bigint: { RevertType } } = require('../../../helpers/enums.js'); const name = 'My Token'; const symbol = 'MTKN'; @@ -14,15 +12,21 @@ const value = 1000n; const data = '0x123456'; async function fixture() { - const [holder, other] = await ethers.getSigners(); + // this.accounts is used by shouldBehaveLikeERC20 + const accounts = await ethers.getSigners(); + const [holder, other] = accounts; + const receiver = await ethers.deployContract('ERC1363ReceiverMock'); const spender = await ethers.deployContract('ERC1363SpenderMock'); const token = await ethers.deployContract('$ERC1363', [name, symbol]); + await token.$_mint(holder, value); + return { - token, + accounts, holder, other, + token, receiver, spender, selectors: { @@ -37,8 +41,7 @@ describe('ERC1363', function () { Object.assign(this, await loadFixture(fixture)); }); - // TODO: check ERC20 behavior when behavior is migrated to ethers - + shouldBehaveLikeERC20(value); shouldSupportInterfaces(['ERC165', 'ERC1363']); describe('transferAndCall', function () { diff --git a/test/token/ERC20/extensions/ERC20FlashMint.test.js b/test/token/ERC20/extensions/ERC20FlashMint.test.js index cee00db0f44..e65318865d2 100644 --- a/test/token/ERC20/extensions/ERC20FlashMint.test.js +++ b/test/token/ERC20/extensions/ERC20FlashMint.test.js @@ -8,12 +8,12 @@ const initialSupply = 100n; const loanValue = 10_000_000_000_000n; async function fixture() { - const [initialHolder, other, anotherAccount] = await ethers.getSigners(); + const [holder, other] = await ethers.getSigners(); const token = await ethers.deployContract('$ERC20FlashMintMock', [name, symbol]); - await token.$_mint(initialHolder, initialSupply); + await token.$_mint(holder, initialSupply); - return { initialHolder, other, anotherAccount, token }; + return { holder, other, token }; } describe('ERC20FlashMint', function () { @@ -134,7 +134,7 @@ describe('ERC20FlashMint', function () { }); it('custom flash fee receiver', async function () { - const flashFeeReceiverAddress = this.anotherAccount; + const flashFeeReceiverAddress = this.other; await this.token.setFlashFeeReceiver(flashFeeReceiverAddress); expect(await this.token.$_flashFeeReceiver()).to.equal(flashFeeReceiverAddress.address); diff --git a/test/token/ERC20/extensions/ERC20Permit.test.js b/test/token/ERC20/extensions/ERC20Permit.test.js index e27a98239bb..496b9f716fc 100644 --- a/test/token/ERC20/extensions/ERC20Permit.test.js +++ b/test/token/ERC20/extensions/ERC20Permit.test.js @@ -12,13 +12,13 @@ const symbol = 'MTKN'; const initialSupply = 100n; async function fixture() { - const [initialHolder, spender, owner, other] = await ethers.getSigners(); + const [holder, spender, owner, other] = await ethers.getSigners(); const token = await ethers.deployContract('$ERC20Permit', [name, symbol, name]); - await token.$_mint(initialHolder, initialSupply); + await token.$_mint(holder, initialSupply); return { - initialHolder, + holder, spender, owner, other, @@ -32,7 +32,7 @@ describe('ERC20Permit', function () { }); it('initial nonce is 0', async function () { - expect(await this.token.nonces(this.initialHolder)).to.equal(0n); + expect(await this.token.nonces(this.holder)).to.equal(0n); }); it('domain separator', async function () { diff --git a/test/token/ERC20/extensions/ERC20Wrapper.test.js b/test/token/ERC20/extensions/ERC20Wrapper.test.js index af746d65a54..63d0a88f204 100644 --- a/test/token/ERC20/extensions/ERC20Wrapper.test.js +++ b/test/token/ERC20/extensions/ERC20Wrapper.test.js @@ -10,14 +10,16 @@ const decimals = 9n; const initialSupply = 100n; async function fixture() { - const [initialHolder, recipient, anotherAccount] = await ethers.getSigners(); + // this.accounts is used by shouldBehaveLikeERC20 + const accounts = await ethers.getSigners(); + const [holder, recipient, other] = accounts const underlying = await ethers.deployContract('$ERC20DecimalsMock', [name, symbol, decimals]); - await underlying.$_mint(initialHolder, initialSupply); + await underlying.$_mint(holder, initialSupply); const token = await ethers.deployContract('$ERC20Wrapper', [`Wrapped ${name}`, `W${symbol}`, underlying]); - return { initialHolder, recipient, anotherAccount, underlying, token }; + return { accounts, holder, recipient, other, underlying, token }; } describe('ERC20Wrapper', function () { @@ -53,57 +55,57 @@ describe('ERC20Wrapper', function () { describe('deposit', function () { it('executes with approval', async function () { - await this.underlying.connect(this.initialHolder).approve(this.token, initialSupply); + await this.underlying.connect(this.holder).approve(this.token, initialSupply); - const tx = await this.token.connect(this.initialHolder).depositFor(this.initialHolder, initialSupply); + const tx = await this.token.connect(this.holder).depositFor(this.holder, initialSupply); await expect(tx) .to.emit(this.underlying, 'Transfer') - .withArgs(this.initialHolder.address, this.token.target, initialSupply) + .withArgs(this.holder.address, this.token.target, initialSupply) .to.emit(this.token, 'Transfer') - .withArgs(ethers.ZeroAddress, this.initialHolder.address, initialSupply); + .withArgs(ethers.ZeroAddress, this.holder.address, initialSupply); await expect(tx).to.changeTokenBalances( this.underlying, - [this.initialHolder, this.token], + [this.holder, this.token], [-initialSupply, initialSupply], ); - await expect(tx).to.changeTokenBalance(this.token, this.initialHolder, initialSupply); + await expect(tx).to.changeTokenBalance(this.token, this.holder, initialSupply); }); it('reverts when missing approval', async function () { - await expect(this.token.connect(this.initialHolder).depositFor(this.initialHolder, initialSupply)) + await expect(this.token.connect(this.holder).depositFor(this.holder, initialSupply)) .to.be.revertedWithCustomError(this.underlying, 'ERC20InsufficientAllowance') .withArgs(this.token.target, 0, initialSupply); }); it('reverts when inssuficient balance', async function () { - await this.underlying.connect(this.initialHolder).approve(this.token, ethers.MaxUint256); + await this.underlying.connect(this.holder).approve(this.token, ethers.MaxUint256); - await expect(this.token.connect(this.initialHolder).depositFor(this.initialHolder, ethers.MaxUint256)) + await expect(this.token.connect(this.holder).depositFor(this.holder, ethers.MaxUint256)) .to.be.revertedWithCustomError(this.underlying, 'ERC20InsufficientBalance') - .withArgs(this.initialHolder.address, initialSupply, ethers.MaxUint256); + .withArgs(this.holder.address, initialSupply, ethers.MaxUint256); }); it('deposits to other account', async function () { - await this.underlying.connect(this.initialHolder).approve(this.token, initialSupply); + await this.underlying.connect(this.holder).approve(this.token, initialSupply); - const tx = await this.token.connect(this.initialHolder).depositFor(this.recipient, initialSupply); + const tx = await this.token.connect(this.holder).depositFor(this.recipient, initialSupply); await expect(tx) .to.emit(this.underlying, 'Transfer') - .withArgs(this.initialHolder.address, this.token.target, initialSupply) + .withArgs(this.holder.address, this.token.target, initialSupply) .to.emit(this.token, 'Transfer') .withArgs(ethers.ZeroAddress, this.recipient.address, initialSupply); await expect(tx).to.changeTokenBalances( this.underlying, - [this.initialHolder, this.token], + [this.holder, this.token], [-initialSupply, initialSupply], ); - await expect(tx).to.changeTokenBalances(this.token, [this.initialHolder, this.recipient], [0, initialSupply]); + await expect(tx).to.changeTokenBalances(this.token, [this.holder, this.recipient], [0, initialSupply]); }); it('reverts minting to the wrapper contract', async function () { - await this.underlying.connect(this.initialHolder).approve(this.token, ethers.MaxUint256); + await this.underlying.connect(this.holder).approve(this.token, ethers.MaxUint256); - await expect(this.token.connect(this.initialHolder).depositFor(this.token, ethers.MaxUint256)) + await expect(this.token.connect(this.holder).depositFor(this.token, ethers.MaxUint256)) .to.be.revertedWithCustomError(this.token, 'ERC20InvalidReceiver') .withArgs(this.token.target); }); @@ -111,61 +113,61 @@ describe('ERC20Wrapper', function () { describe('withdraw', function () { beforeEach(async function () { - await this.underlying.connect(this.initialHolder).approve(this.token, initialSupply); - await this.token.connect(this.initialHolder).depositFor(this.initialHolder, initialSupply); + await this.underlying.connect(this.holder).approve(this.token, initialSupply); + await this.token.connect(this.holder).depositFor(this.holder, initialSupply); }); it('reverts when inssuficient balance', async function () { - await expect(this.token.connect(this.initialHolder).withdrawTo(this.initialHolder, ethers.MaxInt256)) + await expect(this.token.connect(this.holder).withdrawTo(this.holder, ethers.MaxInt256)) .to.be.revertedWithCustomError(this.token, 'ERC20InsufficientBalance') - .withArgs(this.initialHolder.address, initialSupply, ethers.MaxInt256); + .withArgs(this.holder.address, initialSupply, ethers.MaxInt256); }); it('executes when operation is valid', async function () { const value = 42n; - const tx = await this.token.connect(this.initialHolder).withdrawTo(this.initialHolder, value); + const tx = await this.token.connect(this.holder).withdrawTo(this.holder, value); await expect(tx) .to.emit(this.underlying, 'Transfer') - .withArgs(this.token.target, this.initialHolder.address, value) + .withArgs(this.token.target, this.holder.address, value) .to.emit(this.token, 'Transfer') - .withArgs(this.initialHolder.address, ethers.ZeroAddress, value); - await expect(tx).to.changeTokenBalances(this.underlying, [this.token, this.initialHolder], [-value, value]); - await expect(tx).to.changeTokenBalance(this.token, this.initialHolder, -value); + .withArgs(this.holder.address, ethers.ZeroAddress, value); + await expect(tx).to.changeTokenBalances(this.underlying, [this.token, this.holder], [-value, value]); + await expect(tx).to.changeTokenBalance(this.token, this.holder, -value); }); it('entire balance', async function () { - const tx = await this.token.connect(this.initialHolder).withdrawTo(this.initialHolder, initialSupply); + const tx = await this.token.connect(this.holder).withdrawTo(this.holder, initialSupply); await expect(tx) .to.emit(this.underlying, 'Transfer') - .withArgs(this.token.target, this.initialHolder.address, initialSupply) + .withArgs(this.token.target, this.holder.address, initialSupply) .to.emit(this.token, 'Transfer') - .withArgs(this.initialHolder.address, ethers.ZeroAddress, initialSupply); + .withArgs(this.holder.address, ethers.ZeroAddress, initialSupply); await expect(tx).to.changeTokenBalances( this.underlying, - [this.token, this.initialHolder], + [this.token, this.holder], [-initialSupply, initialSupply], ); - await expect(tx).to.changeTokenBalance(this.token, this.initialHolder, -initialSupply); + await expect(tx).to.changeTokenBalance(this.token, this.holder, -initialSupply); }); it('to other account', async function () { - const tx = await this.token.connect(this.initialHolder).withdrawTo(this.recipient, initialSupply); + const tx = await this.token.connect(this.holder).withdrawTo(this.recipient, initialSupply); await expect(tx) .to.emit(this.underlying, 'Transfer') .withArgs(this.token.target, this.recipient.address, initialSupply) .to.emit(this.token, 'Transfer') - .withArgs(this.initialHolder.address, ethers.ZeroAddress, initialSupply); + .withArgs(this.holder.address, ethers.ZeroAddress, initialSupply); await expect(tx).to.changeTokenBalances( this.underlying, - [this.token, this.initialHolder, this.recipient], + [this.token, this.holder, this.recipient], [-initialSupply, 0, initialSupply], ); - await expect(tx).to.changeTokenBalance(this.token, this.initialHolder, -initialSupply); + await expect(tx).to.changeTokenBalance(this.token, this.holder, -initialSupply); }); it('reverts withdrawing to the wrapper contract', async function () { - await expect(this.token.connect(this.initialHolder).withdrawTo(this.token, initialSupply)) + await expect(this.token.connect(this.holder).withdrawTo(this.token, initialSupply)) .to.be.revertedWithCustomError(this.token, 'ERC20InvalidReceiver') .withArgs(this.token.target); }); @@ -173,8 +175,8 @@ describe('ERC20Wrapper', function () { describe('recover', function () { it('nothing to recover', async function () { - await this.underlying.connect(this.initialHolder).approve(this.token, initialSupply); - await this.token.connect(this.initialHolder).depositFor(this.initialHolder, initialSupply); + await this.underlying.connect(this.holder).approve(this.token, initialSupply); + await this.token.connect(this.holder).depositFor(this.holder, initialSupply); const tx = await this.token.$_recover(this.recipient); await expect(tx).to.emit(this.token, 'Transfer').withArgs(ethers.ZeroAddress, this.recipient.address, 0n); @@ -182,7 +184,7 @@ describe('ERC20Wrapper', function () { }); it('something to recover', async function () { - await this.underlying.connect(this.initialHolder).transfer(this.token, initialSupply); + await this.underlying.connect(this.holder).transfer(this.token, initialSupply); const tx = await this.token.$_recover(this.recipient); await expect(tx) @@ -194,8 +196,8 @@ describe('ERC20Wrapper', function () { describe('erc20 behaviour', function () { beforeEach(async function () { - await this.underlying.connect(this.initialHolder).approve(this.token, initialSupply); - await this.token.connect(this.initialHolder).depositFor(this.initialHolder, initialSupply); + await this.underlying.connect(this.holder).approve(this.token, initialSupply); + await this.token.connect(this.holder).depositFor(this.holder, initialSupply); }); shouldBehaveLikeERC20(initialSupply); diff --git a/test/token/ERC721/ERC721.behavior.js b/test/token/ERC721/ERC721.behavior.js index 32d67d90d98..8406c09b027 100644 --- a/test/token/ERC721/ERC721.behavior.js +++ b/test/token/ERC721/ERC721.behavior.js @@ -4,11 +4,7 @@ const { PANIC_CODES } = require('@nomicfoundation/hardhat-chai-matchers/panic'); const { anyValue } = require('@nomicfoundation/hardhat-chai-matchers/withArgs'); const { shouldSupportInterfaces } = require('../../utils/introspection/SupportsInterface.behavior'); -const { - bigint: { Enum }, -} = require('../../helpers/enums'); - -const RevertType = Enum('None', 'RevertWithoutMessage', 'RevertWithMessage', 'RevertWithCustomError', 'Panic'); +const { bigint: { RevertType } } = require('../../helpers/enums'); const firstTokenId = 5042n; const secondTokenId = 79217n; From 7d5e66e63e0a81ff7521a6108667e0e4bec3777b Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Fri, 15 Dec 2023 10:34:38 +0100 Subject: [PATCH 63/82] document the 1363 functions not passing the standardized tests --- test/token/ERC20/extensions/ERC1363.test.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/test/token/ERC20/extensions/ERC1363.test.js b/test/token/ERC20/extensions/ERC1363.test.js index 967f23c99e1..827257473c1 100644 --- a/test/token/ERC20/extensions/ERC1363.test.js +++ b/test/token/ERC20/extensions/ERC1363.test.js @@ -41,8 +41,12 @@ describe('ERC1363', function () { Object.assign(this, await loadFixture(fixture)); }); - shouldBehaveLikeERC20(value); shouldSupportInterfaces(['ERC165', 'ERC1363']); + shouldBehaveLikeERC20(value); + // Note: transferAndCall(address,uint256) fails "shouldBehaveLikeERC20Transfer" because it revert on EOAs + // Note: transferAndCall(address,uint256,bytes) fails "shouldBehaveLikeERC20Transfer" because it revert on EOAs + // Note: approveAndCall(address,uint256) fails "shouldBehaveLikeERC20Approve" because it revert on EOAs + // Note: approveAndCall(address,uint256,bytes) fails "shouldBehaveLikeERC20Approve" because it revert on EOAs describe('transferAndCall', function () { it('to an EOA', async function () { From f0d3a1763cb16f9a1cd1c2f84a2fd961d769f8b4 Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Fri, 15 Dec 2023 10:35:16 +0100 Subject: [PATCH 64/82] fix lint --- test/token/ERC20/ERC20.behavior.js | 26 +++++-------------- test/token/ERC20/ERC20.test.js | 8 ++---- test/token/ERC20/extensions/ERC1363.test.js | 4 ++- .../ERC20/extensions/ERC20Wrapper.test.js | 2 +- test/token/ERC721/ERC721.behavior.js | 4 ++- 5 files changed, 15 insertions(+), 29 deletions(-) diff --git a/test/token/ERC20/ERC20.behavior.js b/test/token/ERC20/ERC20.behavior.js index 48d96fafd4f..7c39e420f10 100644 --- a/test/token/ERC20/ERC20.behavior.js +++ b/test/token/ERC20/ERC20.behavior.js @@ -42,17 +42,11 @@ function shouldBehaveLikeERC20(initialSupply, opts = {}) { const value = initialSupply; beforeEach(async function () { - this.tx = await this.token - .connect(this.recipient) - .transferFrom(this.holder, this.other, value); + this.tx = await this.token.connect(this.recipient).transferFrom(this.holder, this.other, value); }); it('transfers the requested value', async function () { - await expect(this.tx).to.changeTokenBalances( - this.token, - [this.holder, this.other], - [-value, value], - ); + await expect(this.tx).to.changeTokenBalances(this.token, [this.holder, this.other], [-value, value]); }); it('decreases the spender allowance', async function () { @@ -85,9 +79,7 @@ function shouldBehaveLikeERC20(initialSupply, opts = {}) { it('reverts when the token owner does not have enough balance', async function () { const value = initialSupply; await this.token.connect(this.holder).transfer(this.other, 1n); - await expect( - this.token.connect(this.recipient).transferFrom(this.holder, this.other, value), - ) + await expect(this.token.connect(this.recipient).transferFrom(this.holder, this.other, value)) .to.revertedWithCustomError(this.token, 'ERC20InsufficientBalance') .withArgs(this.holder.address, value - 1n, value); }); @@ -102,9 +94,7 @@ function shouldBehaveLikeERC20(initialSupply, opts = {}) { it('reverts when the token owner has enough balance', async function () { const value = initialSupply; - await expect( - this.token.connect(this.recipient).transferFrom(this.holder, this.other, value), - ) + await expect(this.token.connect(this.recipient).transferFrom(this.holder, this.other, value)) .to.be.revertedWithCustomError(this.token, 'ERC20InsufficientAllowance') .withArgs(this.recipient.address, allowance, value); }); @@ -112,9 +102,7 @@ function shouldBehaveLikeERC20(initialSupply, opts = {}) { it('reverts when the token owner does not have enough balance', async function () { const value = allowance; await this.token.connect(this.holder).transfer(this.other, 2); - await expect( - this.token.connect(this.recipient).transferFrom(this.holder, this.other, value), - ) + await expect(this.token.connect(this.recipient).transferFrom(this.holder, this.other, value)) .to.be.revertedWithCustomError(this.token, 'ERC20InsufficientBalance') .withArgs(this.holder.address, value - 1n, value); }); @@ -123,9 +111,7 @@ function shouldBehaveLikeERC20(initialSupply, opts = {}) { describe('when the spender has unlimited allowance', function () { beforeEach(async function () { await this.token.connect(this.holder).approve(this.recipient, ethers.MaxUint256); - this.tx = await this.token - .connect(this.recipient) - .transferFrom(this.holder, this.other, 1n); + this.tx = await this.token.connect(this.recipient).transferFrom(this.holder, this.other, 1n); }); it('does not decrease the spender allowance', async function () { diff --git a/test/token/ERC20/ERC20.test.js b/test/token/ERC20/ERC20.test.js index b73f934ee78..e6a75e27ace 100644 --- a/test/token/ERC20/ERC20.test.js +++ b/test/token/ERC20/ERC20.test.js @@ -132,9 +132,7 @@ describe('ERC20', function () { it('from is the zero address', async function () { const tx = await this.token.$_update(ethers.ZeroAddress, this.holder, value); - await expect(tx) - .to.emit(this.token, 'Transfer') - .withArgs(ethers.ZeroAddress, this.holder.address, value); + await expect(tx).to.emit(this.token, 'Transfer').withArgs(ethers.ZeroAddress, this.holder.address, value); expect(await this.token.totalSupply()).to.equal(this.totalSupply + value); await expect(tx).to.changeTokenBalance(this.token, this.holder, value); @@ -142,9 +140,7 @@ describe('ERC20', function () { it('to is the zero address', async function () { const tx = await this.token.$_update(this.holder, ethers.ZeroAddress, value); - await expect(tx) - .to.emit(this.token, 'Transfer') - .withArgs(this.holder.address, ethers.ZeroAddress, value); + await expect(tx).to.emit(this.token, 'Transfer').withArgs(this.holder.address, ethers.ZeroAddress, value); expect(await this.token.totalSupply()).to.equal(this.totalSupply - value); await expect(tx).to.changeTokenBalance(this.token, this.holder, -value); diff --git a/test/token/ERC20/extensions/ERC1363.test.js b/test/token/ERC20/extensions/ERC1363.test.js index 827257473c1..e1ee0ab4067 100644 --- a/test/token/ERC20/extensions/ERC1363.test.js +++ b/test/token/ERC20/extensions/ERC1363.test.js @@ -4,7 +4,9 @@ const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers'); const { shouldBehaveLikeERC20 } = require('../ERC20.behavior.js'); const { shouldSupportInterfaces } = require('../../../utils/introspection/SupportsInterface.behavior'); -const { bigint: { RevertType } } = require('../../../helpers/enums.js'); +const { + bigint: { RevertType }, +} = require('../../../helpers/enums.js'); const name = 'My Token'; const symbol = 'MTKN'; diff --git a/test/token/ERC20/extensions/ERC20Wrapper.test.js b/test/token/ERC20/extensions/ERC20Wrapper.test.js index 63d0a88f204..100dd9c9f5f 100644 --- a/test/token/ERC20/extensions/ERC20Wrapper.test.js +++ b/test/token/ERC20/extensions/ERC20Wrapper.test.js @@ -12,7 +12,7 @@ const initialSupply = 100n; async function fixture() { // this.accounts is used by shouldBehaveLikeERC20 const accounts = await ethers.getSigners(); - const [holder, recipient, other] = accounts + const [holder, recipient, other] = accounts; const underlying = await ethers.deployContract('$ERC20DecimalsMock', [name, symbol, decimals]); await underlying.$_mint(holder, initialSupply); diff --git a/test/token/ERC721/ERC721.behavior.js b/test/token/ERC721/ERC721.behavior.js index 8406c09b027..ff441f5e61b 100644 --- a/test/token/ERC721/ERC721.behavior.js +++ b/test/token/ERC721/ERC721.behavior.js @@ -4,7 +4,9 @@ const { PANIC_CODES } = require('@nomicfoundation/hardhat-chai-matchers/panic'); const { anyValue } = require('@nomicfoundation/hardhat-chai-matchers/withArgs'); const { shouldSupportInterfaces } = require('../../utils/introspection/SupportsInterface.behavior'); -const { bigint: { RevertType } } = require('../../helpers/enums'); +const { + bigint: { RevertType }, +} = require('../../helpers/enums'); const firstTokenId = 5042n; const secondTokenId = 79217n; From 8f7c2bffb937a30cfa8dd60f5092c402e98e7097 Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Tue, 2 Jan 2024 15:20:05 +0100 Subject: [PATCH 65/82] fix merge --- test/token/ERC20/extensions/ERC1363.test.js | 4 +- test/token/ERC20/utils/SafeERC20.test.js | 158 +++++++------------- 2 files changed, 59 insertions(+), 103 deletions(-) diff --git a/test/token/ERC20/extensions/ERC1363.test.js b/test/token/ERC20/extensions/ERC1363.test.js index e1ee0ab4067..567984d8118 100644 --- a/test/token/ERC20/extensions/ERC1363.test.js +++ b/test/token/ERC20/extensions/ERC1363.test.js @@ -4,9 +4,7 @@ const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers'); const { shouldBehaveLikeERC20 } = require('../ERC20.behavior.js'); const { shouldSupportInterfaces } = require('../../../utils/introspection/SupportsInterface.behavior'); -const { - bigint: { RevertType }, -} = require('../../../helpers/enums.js'); +const { RevertType } = require('../../../helpers/enums.js'); const name = 'My Token'; const symbol = 'MTKN'; diff --git a/test/token/ERC20/utils/SafeERC20.test.js b/test/token/ERC20/utils/SafeERC20.test.js index 84949157225..8907cb7742b 100644 --- a/test/token/ERC20/utils/SafeERC20.test.js +++ b/test/token/ERC20/utils/SafeERC20.test.js @@ -6,24 +6,31 @@ const name = 'ERC20Mock'; const symbol = 'ERC20Mock'; async function fixture() { - const [hasNoCode, owner, receiver, spender] = await ethers.getSigners(); + const [hasNoCode, owner, receiver, spender, other] = await ethers.getSigners(); const mock = await ethers.deployContract('$SafeERC20'); const erc20ReturnFalseMock = await ethers.deployContract('$ERC20ReturnFalseMock', [name, symbol]); const erc20ReturnTrueMock = await ethers.deployContract('$ERC20', [name, symbol]); // default implementation returns true const erc20NoReturnMock = await ethers.deployContract('$ERC20NoReturnMock', [name, symbol]); const erc20ForceApproveMock = await ethers.deployContract('$ERC20ForceApproveMock', [name, symbol]); + const erc1363Mock = await ethers.deployContract('$ERC1363', [name, symbol]); + const erc1363Receiver = await ethers.deployContract('$ERC1363ReceiverMock'); + const erc1363Spender = await ethers.deployContract('$ERC1363SpenderMock'); return { hasNoCode, owner, receiver, spender, + other, mock, erc20ReturnFalseMock, erc20ReturnTrueMock, erc20NoReturnMock, erc20ForceApproveMock, + erc1363Mock, + erc1363Receiver, + erc1363Spender, }; } @@ -146,142 +153,93 @@ describe('SafeERC20', function () { }); describe('with ERC1363', function () { - const value = web3.utils.toBN(100); + const value = 100n; const data = '0x12345678'; beforeEach(async function () { - this.token = await ERC1363.new(name, symbol); + this.token = this.erc1363Mock; }); - shouldOnlyRevertOnErrors(accounts); + shouldOnlyRevertOnErrors(); describe('transferAndCall', function () { it('cannot transferAndCall to an EOA directly', async function () { - await this.token.$_mint(owner, 100); + await this.token.$_mint(this.owner, 100n); - await expectRevertCustomError( - this.token.methods['transferAndCall(address,uint256,bytes)'](receiver, value, data, { from: owner }), - 'ERC1363InvalidReceiver', - [receiver], - ); + await expect(this.token.connect(this.owner).transferAndCall(this.receiver, value, ethers.Typed.bytes(data))) + .to.be.revertedWithCustomError(this.token, 'ERC1363InvalidReceiver') + .withArgs(this.receiver); }); it('can transferAndCall to an EOA using helper', async function () { - await this.token.$_mint(this.mock.address, value); - - const { tx } = await this.mock.$transferAndCallRelaxed(this.token.address, receiver, value, data); - await expectEvent.inTransaction(tx, this.token, 'Transfer', { - from: this.mock.address, - to: receiver, - value, - }); + await this.token.$_mint(this.mock, value); + + await expect(this.mock.$transferAndCallRelaxed(this.token, this.receiver, value, data)) + .to.emit(this.token, 'Transfer') + .withArgs(this.mock, this.receiver, value); }); it('can transferAndCall to an ERC1363Receiver using helper', async function () { - const receiver = await ERC1363ReceiverMock.new(); - - await this.token.$_mint(this.mock.address, value); - - const { tx } = await this.mock.$transferAndCallRelaxed(this.token.address, receiver.address, value, data); - await expectEvent.inTransaction(tx, this.token, 'Transfer', { - from: this.mock.address, - to: receiver.address, - value, - }); - await expectEvent.inTransaction(tx, receiver, 'Received', { - operator: this.mock.address, - from: this.mock.address, - value, - data, - }); + await this.token.$_mint(this.mock, value); + + await expect(this.mock.$transferAndCallRelaxed(this.token, this.erc1363Receiver, value, data)) + .to.emit(this.token, 'Transfer') + .withArgs(this.mock, this.erc1363Receiver, value) + .to.emit(this.erc1363Receiver, 'Received') + .withArgs(this.mock, this.mock, value, data); }); }); describe('transferFromAndCall', function () { it('cannot transferFromAndCall to an EOA directly', async function () { - await this.token.$_mint(owner, value); - await this.token.approve(other, constants.MAX_UINT256, { from: owner }); - - await expectRevertCustomError( - this.token.methods['transferFromAndCall(address,address,uint256,bytes)'](owner, receiver, value, data, { - from: other, - }), - 'ERC1363InvalidReceiver', - [receiver], - ); + await this.token.$_mint(this.owner, value); + await this.token.$_approve(this.owner, this.other, ethers.MaxUint256); + + await expect(this.token.connect(this.other).transferFromAndCall(this.owner, this.receiver, value, ethers.Typed.bytes(data))) + .to.be.revertedWithCustomError(this.token, 'ERC1363InvalidReceiver') + .withArgs(this.receiver); }); it('can transferFromAndCall to an EOA using helper', async function () { - await this.token.$_mint(owner, value); - await this.token.approve(this.mock.address, constants.MAX_UINT256, { from: owner }); - - const { tx } = await this.mock.$transferFromAndCallRelaxed(this.token.address, owner, receiver, value, data); - await expectEvent.inTransaction(tx, this.token, 'Transfer', { - from: owner, - to: receiver, - value, - }); + await this.token.$_mint(this.owner, value); + await this.token.$_approve(this.owner, this.mock, ethers.MaxUint256); + + await expect(this.mock.$transferFromAndCallRelaxed(this.token, this.owner, this.receiver, value, data)) + .to.emit(this.token, 'Transfer') + .withArgs(this.owner, this.receiver, value); }); it('can transferFromAndCall to an ERC1363Receiver using helper', async function () { - const receiver = await ERC1363ReceiverMock.new(); - - await this.token.$_mint(owner, value); - await this.token.approve(this.mock.address, constants.MAX_UINT256, { from: owner }); - - const { tx } = await this.mock.$transferFromAndCallRelaxed( - this.token.address, - owner, - receiver.address, - value, - data, - ); - await expectEvent.inTransaction(tx, this.token, 'Transfer', { - from: owner, - to: receiver.address, - value, - }); - await expectEvent.inTransaction(tx, receiver, 'Received', { - operator: this.mock.address, - from: owner, - value, - data, - }); + await this.token.$_mint(this.owner, value); + await this.token.$_approve(this.owner, this.mock, ethers.MaxUint256); + + await expect(this.mock.$transferFromAndCallRelaxed(this.token, this.owner, this.erc1363Receiver, value, data)) + .to.emit(this.token, 'Transfer') + .withArgs(this.owner, this.erc1363Receiver, value) + .to.emit(this.erc1363Receiver, 'Received') + .withArgs(this.mock, this.owner, value, data); }); }); describe('approveAndCall', function () { it('cannot approveAndCall to an EOA directly', async function () { - await expectRevertCustomError( - this.token.methods['approveAndCall(address,uint256,bytes)'](receiver, value, data), - 'ERC1363InvalidSpender', - [receiver], - ); + await expect(this.token.approveAndCall(this.receiver, value, ethers.Typed.bytes(data))) + .to.revertedWithCustomError(this.token, 'ERC1363InvalidSpender') + .withArgs(this.receiver); }); it('can approveAndCall to an EOA using helper', async function () { - const { tx } = await this.mock.$approveAndCallRelaxed(this.token.address, receiver, value, data); - await expectEvent.inTransaction(tx, this.token, 'Approval', { - owner: this.mock.address, - spender: receiver, - value, - }); + await expect(this.mock.$approveAndCallRelaxed(this.token, this.receiver, value, data)) + .to.emit(this.token, 'Approval') + .withArgs(this.mock, this.receiver, value); }); it('can approveAndCall to an ERC1363Spender using helper', async function () { - const spender = await ERC1363SpenderMock.new(); - - const { tx } = await this.mock.$approveAndCallRelaxed(this.token.address, spender.address, value, data); - await expectEvent.inTransaction(tx, this.token, 'Approval', { - owner: this.mock.address, - spender: spender.address, - value, - }); - await expectEvent.inTransaction(tx, spender, 'Approved', { - owner: this.mock.address, - value, - data, - }); + await expect(this.mock.$approveAndCallRelaxed(this.token, this.erc1363Spender, value, data)) + .to.emit(this.token, 'Approval') + .withArgs(this.mock, this.erc1363Spender, value) + .to.emit(this.erc1363Spender, 'Approved') + .withArgs(this.mock, value, data); }); }); }); From feb824eccfc028daeb686859b66ec49bc6ae49e5 Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Tue, 2 Jan 2024 17:08:33 +0100 Subject: [PATCH 66/82] fix lint --- test/token/ERC20/ERC20.behavior.js | 4 +--- test/token/ERC20/ERC20.test.js | 4 +--- test/token/ERC20/utils/SafeERC20.test.js | 6 +++++- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/test/token/ERC20/ERC20.behavior.js b/test/token/ERC20/ERC20.behavior.js index 35d38ffad0c..6754bff336a 100644 --- a/test/token/ERC20/ERC20.behavior.js +++ b/test/token/ERC20/ERC20.behavior.js @@ -54,9 +54,7 @@ function shouldBehaveLikeERC20(initialSupply, opts = {}) { }); it('emits a transfer event', async function () { - await expect(this.tx) - .to.emit(this.token, 'Transfer') - .withArgs(this.holder, this.other, value); + await expect(this.tx).to.emit(this.token, 'Transfer').withArgs(this.holder, this.other, value); }); if (forcedApproval) { diff --git a/test/token/ERC20/ERC20.test.js b/test/token/ERC20/ERC20.test.js index f5df90c96cb..2d9eefe1c89 100644 --- a/test/token/ERC20/ERC20.test.js +++ b/test/token/ERC20/ERC20.test.js @@ -109,9 +109,7 @@ describe('ERC20', function () { }); it('emits Transfer event', async function () { - await expect(this.tx) - .to.emit(this.token, 'Transfer') - .withArgs(this.holder, ethers.ZeroAddress, value); + await expect(this.tx).to.emit(this.token, 'Transfer').withArgs(this.holder, ethers.ZeroAddress, value); }); }); }; diff --git a/test/token/ERC20/utils/SafeERC20.test.js b/test/token/ERC20/utils/SafeERC20.test.js index 8907cb7742b..9e2b38c38f3 100644 --- a/test/token/ERC20/utils/SafeERC20.test.js +++ b/test/token/ERC20/utils/SafeERC20.test.js @@ -195,7 +195,11 @@ describe('SafeERC20', function () { await this.token.$_mint(this.owner, value); await this.token.$_approve(this.owner, this.other, ethers.MaxUint256); - await expect(this.token.connect(this.other).transferFromAndCall(this.owner, this.receiver, value, ethers.Typed.bytes(data))) + await expect( + this.token + .connect(this.other) + .transferFromAndCall(this.owner, this.receiver, value, ethers.Typed.bytes(data)), + ) .to.be.revertedWithCustomError(this.token, 'ERC1363InvalidReceiver') .withArgs(this.receiver); }); From af285c6bb9d0aaa50f972e3fd17851ff2091e6a3 Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Tue, 2 Jan 2024 17:18:15 +0100 Subject: [PATCH 67/82] add notices --- contracts/token/ERC20/extensions/ERC1363.sol | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/contracts/token/ERC20/extensions/ERC1363.sol b/contracts/token/ERC20/extensions/ERC1363.sol index fadbf9e9364..af79a86ef74 100644 --- a/contracts/token/ERC20/extensions/ERC1363.sol +++ b/contracts/token/ERC20/extensions/ERC1363.sol @@ -37,6 +37,9 @@ abstract contract ERC1363 is ERC20, ERC165, IERC1363 { /** * @inheritdoc IERC1363 + * + * @notice Reverts if `to` is an account without code or the recipient contract does not implement + * {IERC1363Receiver-onTransferReceived} */ function transferAndCall(address to, uint256 value) public virtual returns (bool) { return transferAndCall(to, value, ""); @@ -44,6 +47,9 @@ abstract contract ERC1363 is ERC20, ERC165, IERC1363 { /** * @inheritdoc IERC1363 + * + * @notice Reverts if `to` is an account without code or the recipient contract does not implement + * {IERC1363Receiver-onTransferReceived} */ function transferAndCall(address to, uint256 value, bytes memory data) public virtual returns (bool) { transfer(to, value); @@ -53,6 +59,9 @@ abstract contract ERC1363 is ERC20, ERC165, IERC1363 { /** * @inheritdoc IERC1363 + * + * @notice Reverts if `to` is an account without code or the recipient contract does not implement + * {IERC1363Receiver-onTransferReceived} */ function transferFromAndCall(address from, address to, uint256 value) public virtual returns (bool) { return transferFromAndCall(from, to, value, ""); @@ -60,6 +69,9 @@ abstract contract ERC1363 is ERC20, ERC165, IERC1363 { /** * @inheritdoc IERC1363 + * + * @notice Reverts if `to` is an account without code or the recipient contract does not implement + * {IERC1363Receiver-onTransferReceived} */ function transferFromAndCall( address from, @@ -74,6 +86,9 @@ abstract contract ERC1363 is ERC20, ERC165, IERC1363 { /** * @inheritdoc IERC1363 + * + * @notice Reverts if `spender` is an account without code or the spender contract does not implement + * {IERC1363Spender-onApprovalReceived} */ function approveAndCall(address spender, uint256 value) public virtual returns (bool) { return approveAndCall(spender, value, ""); @@ -81,6 +96,9 @@ abstract contract ERC1363 is ERC20, ERC165, IERC1363 { /** * @inheritdoc IERC1363 + * + * @notice Reverts if `spender` is an account without code or the spender contract does not implement + * {IERC1363Spender-onApprovalReceived} */ function approveAndCall(address spender, uint256 value, bytes memory data) public virtual returns (bool) { approve(spender, value); From d9cba07ac1d6eb4ee1d2e22d87998f31a7cb4f3d Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Thu, 18 Jan 2024 10:17:16 +0100 Subject: [PATCH 68/82] forge update --- lib/forge-std | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/forge-std b/lib/forge-std index eb980e1d4f0..ae570fec082 160000 --- a/lib/forge-std +++ b/lib/forge-std @@ -1 +1 @@ -Subproject commit eb980e1d4f0e8173ec27da77297ae411840c8ccb +Subproject commit ae570fec082bfe1c1f45b0acca4a2b4f84d345ce From ed594f0919723b954dafa36e6555b0585acf3b3b Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Thu, 18 Jan 2024 10:19:06 +0100 Subject: [PATCH 69/82] Update .changeset/friendly-nails-push.md --- .changeset/friendly-nails-push.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/friendly-nails-push.md b/.changeset/friendly-nails-push.md index 581b17883b9..157bf05561a 100644 --- a/.changeset/friendly-nails-push.md +++ b/.changeset/friendly-nails-push.md @@ -2,4 +2,4 @@ 'openzeppelin-solidity': minor --- -`ERC1363`: add `ERC1363` implementation other than `IERC1363Errors`, `ERC1363Holder` and tests. +`ERC1363`: Add implementation of the token payable standard allowing execution of contract code after transfers and approvals. From ab19effc48ddbc46e93b7297845efcd40b9f7d75 Mon Sep 17 00:00:00 2001 From: ernestognw Date: Thu, 18 Jan 2024 17:50:58 -0600 Subject: [PATCH 70/82] Optionally evaluate return value when calling ERC1363 using `transferAndCallRelaxed` --- contracts/token/ERC20/utils/SafeERC20.sol | 40 +++++++++++++++++++++-- 1 file changed, 37 insertions(+), 3 deletions(-) diff --git a/contracts/token/ERC20/utils/SafeERC20.sol b/contracts/token/ERC20/utils/SafeERC20.sol index 8e3924142e9..46c4aac0e9c 100644 --- a/contracts/token/ERC20/utils/SafeERC20.sol +++ b/contracts/token/ERC20/utils/SafeERC20.sol @@ -86,12 +86,23 @@ library SafeERC20 { * @dev Perform an {ERC1363} transferAndCall, with a fallback to the simple {ERC20} transfer if the target has no * code. This can be used to implement an {ERC721}-like safe transfer that rely on {ERC1363} checks when * targeting contracts. + * + * Revert if returned value is `false`. If `token` returns no value, non-reverting calls are assumed to be successful. */ function transferAndCallRelaxed(IERC1363 token, address to, uint256 value, bytes memory data) internal { if (to.code.length == 0) { safeTransfer(token, to, value); } else { - token.transferAndCall(to, value, data); + _callOptionalReturn( + token, + // Can't use abi.encodeCall since `token.transferAndCall` is not an unique identifier. + abi.encodeWithSelector( + 0x4000aea0, // bytes4(keccak256("transferAndCall(address,uint256,bytes)")) + to, + value, + data + ) + ); } } @@ -99,6 +110,8 @@ library SafeERC20 { * @dev Perform an {ERC1363} transferFromAndCall, with a fallback to the simple {ERC20} transferFrom if the target * has no code. This can be used to implement an {ERC721}-like safe transfer that rely on {ERC1363} checks when * targeting contracts. + * + * Revert if returned value is `false`. If `token` returns no value, non-reverting calls are assumed to be successful. */ function transferFromAndCallRelaxed( IERC1363 token, @@ -110,7 +123,17 @@ library SafeERC20 { if (to.code.length == 0) { safeTransferFrom(token, from, to, value); } else { - token.transferFromAndCall(from, to, value, data); + _callOptionalReturn( + token, + // Can't use abi.encodeCall since `token.transferFromAndCall` is not an unique identifier. + abi.encodeWithSelector( + 0xc1d34b89, // bytes4(keccak256("transferFromAndCall(address,address,uint256,bytes)")) + from, + to, + value, + data + ) + ); } } @@ -118,12 +141,23 @@ library SafeERC20 { * @dev Perform an {ERC1363} approveAndCall, with a fallback to the simple {ERC20} approve if the target has no * code. This can be used to implement an {ERC721}-like safe transfer that rely on {ERC1363} checks when * targeting contracts. + * + * Revert if returned value is `false`. If `token` returns no value, non-reverting calls are assumed to be successful. */ function approveAndCallRelaxed(IERC1363 token, address to, uint256 value, bytes memory data) internal { if (to.code.length == 0) { forceApprove(token, to, value); } else { - token.approveAndCall(to, value, data); + _callOptionalReturn( + token, + // Can't use abi.encodeCall since `token.approveAndCall` is not an unique identifier. + abi.encodeWithSelector( + 0xcae9ca51, // bytes4(keccak256("approveAndCall(address,uint256,bytes)")) + to, + value, + data + ) + ); } } From 5efabe60a004157c0edc1b4004860a5843cbc870 Mon Sep 17 00:00:00 2001 From: ernestognw Date: Thu, 18 Jan 2024 17:57:11 -0600 Subject: [PATCH 71/82] Apply review comments --- contracts/token/ERC20/README.adoc | 2 +- contracts/token/ERC20/extensions/ERC1363.sol | 54 ++++++++++++-------- 2 files changed, 34 insertions(+), 22 deletions(-) diff --git a/contracts/token/ERC20/README.adoc b/contracts/token/ERC20/README.adoc index 266c8735893..938784ff996 100644 --- a/contracts/token/ERC20/README.adoc +++ b/contracts/token/ERC20/README.adoc @@ -22,7 +22,7 @@ Additionally there are multiple custom extensions, including: * {ERC20FlashMint}: token level support for flash loans through the minting and burning of ephemeral tokens (standardized as ERC-3156). * {ERC20Votes}: support for voting and vote delegation. * {ERC20Wrapper}: wrapper to create an ERC-20 backed by another ERC-20, with deposit and withdraw methods. Useful in conjunction with {ERC20Votes}. -* {ERC1363}: implementation of the ERC-1363 interface that supports executing code on a recipient contract after transfers, or code on a spender contract after approvals, in a single transaction. +* {ERC1363}: support for calling the target of a transfer or approval, enabling code execution on the receiver within a single transaction. * {ERC4626}: tokenized vault that manages shares (represented as ERC-20) that are backed by assets (another ERC-20). Finally, there are some utilities to interact with ERC-20 contracts in various ways: diff --git a/contracts/token/ERC20/extensions/ERC1363.sol b/contracts/token/ERC20/extensions/ERC1363.sol index af79a86ef74..52b7032ef64 100644 --- a/contracts/token/ERC20/extensions/ERC1363.sol +++ b/contracts/token/ERC20/extensions/ERC1363.sol @@ -36,19 +36,21 @@ abstract contract ERC1363 is ERC20, ERC165, IERC1363 { } /** - * @inheritdoc IERC1363 + * @dev Moves a `value` amount of tokens from the caller's account to `to` + * and then calls {IERC1363Receiver-onTransferReceived} on `to`. * - * @notice Reverts if `to` is an account without code or the recipient contract does not implement + * NOTE: Reverts if `to` is an account without code or the recipient contract does not implement * {IERC1363Receiver-onTransferReceived} */ - function transferAndCall(address to, uint256 value) public virtual returns (bool) { + function transferAndCall(address to, uint256 value) public returns (bool) { return transferAndCall(to, value, ""); } /** - * @inheritdoc IERC1363 + * @dev Moves a `value` amount of tokens from the caller's account to `to` + * and then calls {IERC1363Receiver-onTransferReceived} on `to`. * - * @notice Reverts if `to` is an account without code or the recipient contract does not implement + * NOTE: Reverts if `to` is an account without code or the recipient contract does not implement * {IERC1363Receiver-onTransferReceived} */ function transferAndCall(address to, uint256 value, bytes memory data) public virtual returns (bool) { @@ -58,19 +60,21 @@ abstract contract ERC1363 is ERC20, ERC165, IERC1363 { } /** - * @inheritdoc IERC1363 + * @dev Moves a `value` amount of tokens from `from` to `to` using the allowance mechanism + * and then calls {IERC1363Receiver-onTransferReceived} on `to`. * - * @notice Reverts if `to` is an account without code or the recipient contract does not implement + * NOTE: Reverts if `to` is an account without code or the recipient contract does not implement * {IERC1363Receiver-onTransferReceived} */ - function transferFromAndCall(address from, address to, uint256 value) public virtual returns (bool) { + function transferFromAndCall(address from, address to, uint256 value) public returns (bool) { return transferFromAndCall(from, to, value, ""); } /** - * @inheritdoc IERC1363 + * @dev Moves a `value` amount of tokens from `from` to `to` using the allowance mechanism + * and then calls {IERC1363Receiver-onTransferReceived} on `to`. * - * @notice Reverts if `to` is an account without code or the recipient contract does not implement + * NOTE: Reverts if `to` is an account without code or the recipient contract does not implement * {IERC1363Receiver-onTransferReceived} */ function transferFromAndCall( @@ -85,19 +89,21 @@ abstract contract ERC1363 is ERC20, ERC165, IERC1363 { } /** - * @inheritdoc IERC1363 + * @dev Sets a `value` amount of tokens as the allowance of `spender` over the + * caller's tokens and then calls {IERC1363Spender-onApprovalReceived} on `spender`. * - * @notice Reverts if `spender` is an account without code or the spender contract does not implement + * NOTE: Reverts if `spender` is an account without code or the spender contract does not implement * {IERC1363Spender-onApprovalReceived} */ - function approveAndCall(address spender, uint256 value) public virtual returns (bool) { + function approveAndCall(address spender, uint256 value) public returns (bool) { return approveAndCall(spender, value, ""); } /** - * @inheritdoc IERC1363 + * @dev Sets a `value` amount of tokens as the allowance of `spender` over the + * caller's tokens and then calls {IERC1363Spender-onApprovalReceived} on `spender`. * - * @notice Reverts if `spender` is an account without code or the spender contract does not implement + * NOTE: Reverts if `spender` is an account without code or the spender contract does not implement * {IERC1363Spender-onApprovalReceived} */ function approveAndCall(address spender, uint256 value, bytes memory data) public virtual returns (bool) { @@ -108,9 +114,12 @@ abstract contract ERC1363 is ERC20, ERC165, IERC1363 { /** * @dev Performs a call to {IERC1363Receiver-onTransferReceived} on a target address. - * This will revert if the target doesn't implement the {IERC1363Receiver} interface or - * if the target doesn't accept the token transfer or - * if the target address is not a contract. + * + * Requirements: + * + * - The target has code (i.e. is a contract). + * - The target `to` must implement the {IERC1363Receiver} interface. + * - The target should return the {IERC1363Receiver} interface id. */ function _checkOnTransferReceived(address from, address to, uint256 value, bytes memory data) private { if (to.code.length == 0) { @@ -135,9 +144,12 @@ abstract contract ERC1363 is ERC20, ERC165, IERC1363 { /** * @dev Performs a call to {IERC1363Spender-onApprovalReceived} on a target address. - * This will revert if the target doesn't implement the {IERC1363Spender} interface or - * if the target doesn't accept the token approval or - * if the target address is not a contract. + * + * Requirements: + * + * - The target has code (i.e. is a contract). + * - The target `to` must implement the {IERC1363Spender} interface. + * - The target should return the {IERC1363Spender} interface id. */ function _checkOnApprovalReceived(address spender, uint256 value, bytes memory data) private { if (spender.code.length == 0) { From 07f1e4da7848627918aac5432eb190198a6d429e Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Fri, 19 Jan 2024 10:39:53 +0100 Subject: [PATCH 72/82] Update test/token/ERC20/utils/SafeERC20.test.js MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Ernesto García --- test/token/ERC20/utils/SafeERC20.test.js | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/test/token/ERC20/utils/SafeERC20.test.js b/test/token/ERC20/utils/SafeERC20.test.js index fb502d030c4..6d6bd81eabd 100644 --- a/test/token/ERC20/utils/SafeERC20.test.js +++ b/test/token/ERC20/utils/SafeERC20.test.js @@ -191,18 +191,6 @@ describe('SafeERC20', function () { }); describe('transferFromAndCall', function () { - it('cannot transferFromAndCall to an EOA directly', async function () { - await this.token.$_mint(this.owner, value); - await this.token.$_approve(this.owner, this.other, ethers.MaxUint256); - - await expect( - this.token - .connect(this.other) - .transferFromAndCall(this.owner, this.receiver, value, ethers.Typed.bytes(data)), - ) - .to.be.revertedWithCustomError(this.token, 'ERC1363InvalidReceiver') - .withArgs(this.receiver); - }); it('can transferFromAndCall to an EOA using helper', async function () { await this.token.$_mint(this.owner, value); From fb78a86b3a801222287a2ca153a00564f36131e4 Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Fri, 19 Jan 2024 10:48:48 +0100 Subject: [PATCH 73/82] Update test/token/ERC20/utils/SafeERC20.test.js MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Ernesto García --- test/token/ERC20/utils/SafeERC20.test.js | 5 ----- 1 file changed, 5 deletions(-) diff --git a/test/token/ERC20/utils/SafeERC20.test.js b/test/token/ERC20/utils/SafeERC20.test.js index 6d6bd81eabd..76eed044093 100644 --- a/test/token/ERC20/utils/SafeERC20.test.js +++ b/test/token/ERC20/utils/SafeERC20.test.js @@ -214,11 +214,6 @@ describe('SafeERC20', function () { }); describe('approveAndCall', function () { - it('cannot approveAndCall to an EOA directly', async function () { - await expect(this.token.approveAndCall(this.receiver, value, ethers.Typed.bytes(data))) - .to.revertedWithCustomError(this.token, 'ERC1363InvalidSpender') - .withArgs(this.receiver); - }); it('can approveAndCall to an EOA using helper', async function () { await expect(this.mock.$approveAndCallRelaxed(this.token, this.receiver, value, data)) From 8639885789660e13ef13dcb329e01bfd27993edd Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Tue, 23 Jan 2024 15:35:56 +0100 Subject: [PATCH 74/82] check values --- contracts/token/ERC20/extensions/ERC1363.sol | 27 +++++++++++-- contracts/token/ERC20/utils/SafeERC20.sol | 40 +++----------------- test/token/ERC20/utils/SafeERC20.test.js | 2 - 3 files changed, 30 insertions(+), 39 deletions(-) diff --git a/contracts/token/ERC20/extensions/ERC1363.sol b/contracts/token/ERC20/extensions/ERC1363.sol index 52b7032ef64..8c11d10804c 100644 --- a/contracts/token/ERC20/extensions/ERC1363.sol +++ b/contracts/token/ERC20/extensions/ERC1363.sol @@ -28,6 +28,21 @@ abstract contract ERC1363 is ERC20, ERC165, IERC1363 { */ error ERC1363InvalidSpender(address spender); + /** + * @dev Indicates a failure within the {transfer} part of a transferAndCall operation. + */ + error ERC1363TransferFailed(address to, uint256 value); + + /** + * @dev Indicates a failure within the {transferFrom} part of a transferFromAndCall operation. + */ + error ERC1363TransferFromFailed(address from, address to, uint256 value); + + /** + * @dev Indicates a failure within the {approve} part of a approveAndCall operation. + */ + error ERC1363ApproveFailed(address spender, uint256 value); + /** * @inheritdoc IERC165 */ @@ -54,7 +69,9 @@ abstract contract ERC1363 is ERC20, ERC165, IERC1363 { * {IERC1363Receiver-onTransferReceived} */ function transferAndCall(address to, uint256 value, bytes memory data) public virtual returns (bool) { - transfer(to, value); + if (!transfer(to, value)) { + revert ERC1363TransferFailed(to, value); + } _checkOnTransferReceived(_msgSender(), to, value, data); return true; } @@ -83,7 +100,9 @@ abstract contract ERC1363 is ERC20, ERC165, IERC1363 { uint256 value, bytes memory data ) public virtual returns (bool) { - transferFrom(from, to, value); + if (!transferFrom(from, to, value)) { + revert ERC1363TransferFromFailed(from, to, value); + } _checkOnTransferReceived(from, to, value, data); return true; } @@ -107,7 +126,9 @@ abstract contract ERC1363 is ERC20, ERC165, IERC1363 { * {IERC1363Spender-onApprovalReceived} */ function approveAndCall(address spender, uint256 value, bytes memory data) public virtual returns (bool) { - approve(spender, value); + if (!approve(spender, value)) { + revert ERC1363ApproveFailed(spender, value); + } _checkOnApprovalReceived(spender, value, data); return true; } diff --git a/contracts/token/ERC20/utils/SafeERC20.sol b/contracts/token/ERC20/utils/SafeERC20.sol index 46c4aac0e9c..3a1017acc19 100644 --- a/contracts/token/ERC20/utils/SafeERC20.sol +++ b/contracts/token/ERC20/utils/SafeERC20.sol @@ -92,17 +92,8 @@ library SafeERC20 { function transferAndCallRelaxed(IERC1363 token, address to, uint256 value, bytes memory data) internal { if (to.code.length == 0) { safeTransfer(token, to, value); - } else { - _callOptionalReturn( - token, - // Can't use abi.encodeCall since `token.transferAndCall` is not an unique identifier. - abi.encodeWithSelector( - 0x4000aea0, // bytes4(keccak256("transferAndCall(address,uint256,bytes)")) - to, - value, - data - ) - ); + } else if (!token.transferAndCall(to, value, data)) { + revert SafeERC20FailedOperation(address(token)); } } @@ -122,18 +113,8 @@ library SafeERC20 { ) internal { if (to.code.length == 0) { safeTransferFrom(token, from, to, value); - } else { - _callOptionalReturn( - token, - // Can't use abi.encodeCall since `token.transferFromAndCall` is not an unique identifier. - abi.encodeWithSelector( - 0xc1d34b89, // bytes4(keccak256("transferFromAndCall(address,address,uint256,bytes)")) - from, - to, - value, - data - ) - ); + } else if (!token.transferFromAndCall(from, to, value, data)) { + revert SafeERC20FailedOperation(address(token)); } } @@ -147,17 +128,8 @@ library SafeERC20 { function approveAndCallRelaxed(IERC1363 token, address to, uint256 value, bytes memory data) internal { if (to.code.length == 0) { forceApprove(token, to, value); - } else { - _callOptionalReturn( - token, - // Can't use abi.encodeCall since `token.approveAndCall` is not an unique identifier. - abi.encodeWithSelector( - 0xcae9ca51, // bytes4(keccak256("approveAndCall(address,uint256,bytes)")) - to, - value, - data - ) - ); + } else if (!token.approveAndCall(to, value, data)) { + revert SafeERC20FailedOperation(address(token)); } } diff --git a/test/token/ERC20/utils/SafeERC20.test.js b/test/token/ERC20/utils/SafeERC20.test.js index 76eed044093..b5628373c5c 100644 --- a/test/token/ERC20/utils/SafeERC20.test.js +++ b/test/token/ERC20/utils/SafeERC20.test.js @@ -191,7 +191,6 @@ describe('SafeERC20', function () { }); describe('transferFromAndCall', function () { - it('can transferFromAndCall to an EOA using helper', async function () { await this.token.$_mint(this.owner, value); await this.token.$_approve(this.owner, this.mock, ethers.MaxUint256); @@ -214,7 +213,6 @@ describe('SafeERC20', function () { }); describe('approveAndCall', function () { - it('can approveAndCall to an EOA using helper', async function () { await expect(this.mock.$approveAndCallRelaxed(this.token, this.receiver, value, data)) .to.emit(this.token, 'Approval') From 10f4d1dbcae332997c628d31bd5ad89a06cdc43f Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Tue, 23 Jan 2024 16:07:57 +0100 Subject: [PATCH 75/82] test SafeERC20 against non compliant ERC1363 tokens --- contracts/mocks/token/ERC1363NoReturnMock.sol | 34 ++++++++ .../mocks/token/ERC1363ReturnFalseMock.sol | 34 ++++++++ test/token/ERC20/utils/SafeERC20.test.js | 85 ++++++++++++++++++- 3 files changed, 149 insertions(+), 4 deletions(-) create mode 100644 contracts/mocks/token/ERC1363NoReturnMock.sol create mode 100644 contracts/mocks/token/ERC1363ReturnFalseMock.sol diff --git a/contracts/mocks/token/ERC1363NoReturnMock.sol b/contracts/mocks/token/ERC1363NoReturnMock.sol new file mode 100644 index 00000000000..748d2341375 --- /dev/null +++ b/contracts/mocks/token/ERC1363NoReturnMock.sol @@ -0,0 +1,34 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.20; + +import {IERC20, ERC20} from "../../token/ERC20/ERC20.sol"; +import {ERC1363} from "../../token/ERC20/extensions/ERC1363.sol"; + +abstract contract ERC1363NoReturnMock is ERC1363 { + function transferAndCall(address to, uint256 value, bytes memory data) public override returns (bool) { + super.transferAndCall(to, value, data); + assembly { + return(0, 0) + } + } + + function transferFromAndCall( + address from, + address to, + uint256 value, + bytes memory data + ) public override returns (bool) { + super.transferFromAndCall(from, to, value, data); + assembly { + return(0, 0) + } + } + + function approveAndCall(address spender, uint256 value, bytes memory data) public override returns (bool) { + super.approveAndCall(spender, value, data); + assembly { + return(0, 0) + } + } +} diff --git a/contracts/mocks/token/ERC1363ReturnFalseMock.sol b/contracts/mocks/token/ERC1363ReturnFalseMock.sol new file mode 100644 index 00000000000..fcff7283b05 --- /dev/null +++ b/contracts/mocks/token/ERC1363ReturnFalseMock.sol @@ -0,0 +1,34 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.20; + +import {IERC20, ERC20} from "../../token/ERC20/ERC20.sol"; +import {ERC1363} from "../../token/ERC20/extensions/ERC1363.sol"; + +abstract contract ERC1363ReturnFalseMock_1 is ERC1363 { + function transfer(address, uint256) public pure override(IERC20, ERC20) returns (bool) { + return false; + } + + function transferFrom(address, address, uint256) public pure override(IERC20, ERC20) returns (bool) { + return false; + } + + function approve(address, uint256) public pure override(IERC20, ERC20) returns (bool) { + return false; + } +} + +abstract contract ERC1363ReturnFalseMock_2 is ERC1363 { + function transferAndCall(address, uint256, bytes memory) public pure override returns (bool) { + return false; + } + + function transferFromAndCall(address, address, uint256, bytes memory) public pure override returns (bool) { + return false; + } + + function approveAndCall(address, uint256, bytes memory) public pure override returns (bool) { + return false; + } +} diff --git a/test/token/ERC20/utils/SafeERC20.test.js b/test/token/ERC20/utils/SafeERC20.test.js index b5628373c5c..be76f002cf8 100644 --- a/test/token/ERC20/utils/SafeERC20.test.js +++ b/test/token/ERC20/utils/SafeERC20.test.js @@ -4,6 +4,8 @@ const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers'); const name = 'ERC20Mock'; const symbol = 'ERC20Mock'; +const value = 100n; +const data = '0x12345678'; async function fixture() { const [hasNoCode, owner, receiver, spender, other] = await ethers.getSigners(); @@ -14,6 +16,9 @@ async function fixture() { const erc20NoReturnMock = await ethers.deployContract('$ERC20NoReturnMock', [name, symbol]); const erc20ForceApproveMock = await ethers.deployContract('$ERC20ForceApproveMock', [name, symbol]); const erc1363Mock = await ethers.deployContract('$ERC1363', [name, symbol]); + const erc1363ReturnFalseMock_1 = await ethers.deployContract('$ERC1363ReturnFalseMock_1', [name, symbol]); + const erc1363ReturnFalseMock_2 = await ethers.deployContract('$ERC1363ReturnFalseMock_2', [name, symbol]); + const erc1363NoReturnMock = await ethers.deployContract('$ERC1363NoReturnMock', [name, symbol]); const erc1363Receiver = await ethers.deployContract('$ERC1363ReceiverMock'); const erc1363Spender = await ethers.deployContract('$ERC1363SpenderMock'); @@ -29,6 +34,9 @@ async function fixture() { erc20NoReturnMock, erc20ForceApproveMock, erc1363Mock, + erc1363ReturnFalseMock_1, + erc1363ReturnFalseMock_2, + erc1363NoReturnMock, erc1363Receiver, erc1363Spender, }; @@ -152,10 +160,7 @@ describe('SafeERC20', function () { }); }); - describe('with ERC1363', function () { - const value = 100n; - const data = '0x12345678'; - + describe('with standard ERC1363', function () { beforeEach(async function () { this.token = this.erc1363Mock; }); @@ -228,6 +233,78 @@ describe('SafeERC20', function () { }); }); }); + + describe('with ERC1363 that returns false on all ERC20 calls', function () { + beforeEach(async function () { + this.token = this.erc1363ReturnFalseMock_1; + }); + + it('reverts on transferAndCallRelaxed', async function () { + await expect(this.mock.$transferAndCallRelaxed(this.token, this.erc1363Receiver, 0n, data)) + .to.be.revertedWithCustomError(this.token, 'ERC1363TransferFailed') + .withArgs(this.erc1363Receiver, 0n); + }); + + it('reverts on transferFromAndCallRelaxed', async function () { + await expect(this.mock.$transferFromAndCallRelaxed(this.token, this.mock, this.erc1363Receiver, 0n, data)) + .to.be.revertedWithCustomError(this.token, 'ERC1363TransferFromFailed') + .withArgs(this.mock, this.erc1363Receiver, 0n); + }); + + it('reverts on approveAndCallRelaxed', async function () { + await expect(this.mock.$approveAndCallRelaxed(this.token, this.erc1363Spender, 0n, data)) + .to.be.revertedWithCustomError(this.token, 'ERC1363ApproveFailed') + .withArgs(this.erc1363Spender, 0n); + }); + }); + + describe('with ERC1363 that returns false on all ERC1363 calls', function () { + beforeEach(async function () { + this.token = this.erc1363ReturnFalseMock_2; + }); + + it('reverts on transferAndCallRelaxed', async function () { + await expect(this.mock.$transferAndCallRelaxed(this.token, this.erc1363Receiver, 0n, data)) + .to.be.revertedWithCustomError(this.mock, 'SafeERC20FailedOperation') + .withArgs(this.token); + }); + + it('reverts on transferFromAndCallRelaxed', async function () { + await expect(this.mock.$transferFromAndCallRelaxed(this.token, this.mock, this.erc1363Receiver, 0n, data)) + .to.be.revertedWithCustomError(this.mock, 'SafeERC20FailedOperation') + .withArgs(this.token); + }); + + it('reverts on approveAndCallRelaxed', async function () { + await expect(this.mock.$approveAndCallRelaxed(this.token, this.erc1363Spender, 0n, data)) + .to.be.revertedWithCustomError(this.mock, 'SafeERC20FailedOperation') + .withArgs(this.token); + }); + }); + + describe('with ERC1363 that returns no boolean values', function () { + beforeEach(async function () { + this.token = this.erc1363NoReturnMock; + }); + + it('reverts on transferAndCallRelaxed', async function () { + await expect( + this.mock.$transferAndCallRelaxed(this.token, this.erc1363Receiver, 0n, data), + ).to.be.revertedWithoutReason(); + }); + + it('reverts on transferFromAndCallRelaxed', async function () { + await expect( + this.mock.$transferFromAndCallRelaxed(this.token, this.mock, this.erc1363Receiver, 0n, data), + ).to.be.revertedWithoutReason(); + }); + + it('reverts on approveAndCallRelaxed', async function () { + await expect( + this.mock.$approveAndCallRelaxed(this.token, this.erc1363Spender, 0n, data), + ).to.be.revertedWithoutReason(); + }); + }); }); function shouldOnlyRevertOnErrors() { From 327aca5118c27be95da4b5096dc12cb24d0ccac3 Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Tue, 23 Jan 2024 17:06:24 +0100 Subject: [PATCH 76/82] lint --- contracts/mocks/token/ERC1363ReturnFalseMock.sol | 4 ++-- test/token/ERC20/utils/SafeERC20.test.js | 12 ++++++------ 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/contracts/mocks/token/ERC1363ReturnFalseMock.sol b/contracts/mocks/token/ERC1363ReturnFalseMock.sol index fcff7283b05..af54ec87ac5 100644 --- a/contracts/mocks/token/ERC1363ReturnFalseMock.sol +++ b/contracts/mocks/token/ERC1363ReturnFalseMock.sol @@ -5,7 +5,7 @@ pragma solidity ^0.8.20; import {IERC20, ERC20} from "../../token/ERC20/ERC20.sol"; import {ERC1363} from "../../token/ERC20/extensions/ERC1363.sol"; -abstract contract ERC1363ReturnFalseMock_1 is ERC1363 { +abstract contract ERC1363ReturnFalseMock1 is ERC1363 { function transfer(address, uint256) public pure override(IERC20, ERC20) returns (bool) { return false; } @@ -19,7 +19,7 @@ abstract contract ERC1363ReturnFalseMock_1 is ERC1363 { } } -abstract contract ERC1363ReturnFalseMock_2 is ERC1363 { +abstract contract ERC1363ReturnFalseMock2 is ERC1363 { function transferAndCall(address, uint256, bytes memory) public pure override returns (bool) { return false; } diff --git a/test/token/ERC20/utils/SafeERC20.test.js b/test/token/ERC20/utils/SafeERC20.test.js index be76f002cf8..4c6d5ffb5ee 100644 --- a/test/token/ERC20/utils/SafeERC20.test.js +++ b/test/token/ERC20/utils/SafeERC20.test.js @@ -16,8 +16,8 @@ async function fixture() { const erc20NoReturnMock = await ethers.deployContract('$ERC20NoReturnMock', [name, symbol]); const erc20ForceApproveMock = await ethers.deployContract('$ERC20ForceApproveMock', [name, symbol]); const erc1363Mock = await ethers.deployContract('$ERC1363', [name, symbol]); - const erc1363ReturnFalseMock_1 = await ethers.deployContract('$ERC1363ReturnFalseMock_1', [name, symbol]); - const erc1363ReturnFalseMock_2 = await ethers.deployContract('$ERC1363ReturnFalseMock_2', [name, symbol]); + const erc1363ReturnFalseMock1 = await ethers.deployContract('$ERC1363ReturnFalseMock1', [name, symbol]); + const erc1363ReturnFalseMock2 = await ethers.deployContract('$ERC1363ReturnFalseMock2', [name, symbol]); const erc1363NoReturnMock = await ethers.deployContract('$ERC1363NoReturnMock', [name, symbol]); const erc1363Receiver = await ethers.deployContract('$ERC1363ReceiverMock'); const erc1363Spender = await ethers.deployContract('$ERC1363SpenderMock'); @@ -34,8 +34,8 @@ async function fixture() { erc20NoReturnMock, erc20ForceApproveMock, erc1363Mock, - erc1363ReturnFalseMock_1, - erc1363ReturnFalseMock_2, + erc1363ReturnFalseMock1, + erc1363ReturnFalseMock2, erc1363NoReturnMock, erc1363Receiver, erc1363Spender, @@ -236,7 +236,7 @@ describe('SafeERC20', function () { describe('with ERC1363 that returns false on all ERC20 calls', function () { beforeEach(async function () { - this.token = this.erc1363ReturnFalseMock_1; + this.token = this.erc1363ReturnFalseMock1; }); it('reverts on transferAndCallRelaxed', async function () { @@ -260,7 +260,7 @@ describe('SafeERC20', function () { describe('with ERC1363 that returns false on all ERC1363 calls', function () { beforeEach(async function () { - this.token = this.erc1363ReturnFalseMock_2; + this.token = this.erc1363ReturnFalseMock2; }); it('reverts on transferAndCallRelaxed', async function () { From cba6a94e64e7daf6beb0b0cf51bff3ac2d919687 Mon Sep 17 00:00:00 2001 From: ernestognw Date: Tue, 23 Jan 2024 10:12:06 -0600 Subject: [PATCH 77/82] Nits --- contracts/mocks/token/ERC1363ReturnFalseMock.sol | 4 ++-- contracts/token/ERC20/utils/SafeERC20.sol | 12 ++++++------ test/token/ERC20/utils/SafeERC20.test.js | 14 +++++++------- 3 files changed, 15 insertions(+), 15 deletions(-) diff --git a/contracts/mocks/token/ERC1363ReturnFalseMock.sol b/contracts/mocks/token/ERC1363ReturnFalseMock.sol index af54ec87ac5..afdd01f3ee3 100644 --- a/contracts/mocks/token/ERC1363ReturnFalseMock.sol +++ b/contracts/mocks/token/ERC1363ReturnFalseMock.sol @@ -5,7 +5,7 @@ pragma solidity ^0.8.20; import {IERC20, ERC20} from "../../token/ERC20/ERC20.sol"; import {ERC1363} from "../../token/ERC20/extensions/ERC1363.sol"; -abstract contract ERC1363ReturnFalseMock1 is ERC1363 { +abstract contract ERC1363ReturnFalseOnERC20Mock is ERC1363 { function transfer(address, uint256) public pure override(IERC20, ERC20) returns (bool) { return false; } @@ -19,7 +19,7 @@ abstract contract ERC1363ReturnFalseMock1 is ERC1363 { } } -abstract contract ERC1363ReturnFalseMock2 is ERC1363 { +abstract contract ERC1363ReturnFalseMock is ERC1363 { function transferAndCall(address, uint256, bytes memory) public pure override returns (bool) { return false; } diff --git a/contracts/token/ERC20/utils/SafeERC20.sol b/contracts/token/ERC20/utils/SafeERC20.sol index 3a1017acc19..197043a9622 100644 --- a/contracts/token/ERC20/utils/SafeERC20.sol +++ b/contracts/token/ERC20/utils/SafeERC20.sol @@ -83,11 +83,11 @@ library SafeERC20 { } /** - * @dev Perform an {ERC1363} transferAndCall, with a fallback to the simple {ERC20} transfer if the target has no + * @dev Performs an {ERC1363} transferAndCall, with a fallback to the simple {ERC20} transfer if the target has no * code. This can be used to implement an {ERC721}-like safe transfer that rely on {ERC1363} checks when * targeting contracts. * - * Revert if returned value is `false`. If `token` returns no value, non-reverting calls are assumed to be successful. + * Revert if the returned value is other than `true`. */ function transferAndCallRelaxed(IERC1363 token, address to, uint256 value, bytes memory data) internal { if (to.code.length == 0) { @@ -98,11 +98,11 @@ library SafeERC20 { } /** - * @dev Perform an {ERC1363} transferFromAndCall, with a fallback to the simple {ERC20} transferFrom if the target + * @dev Performs an {ERC1363} transferFromAndCall, with a fallback to the simple {ERC20} transferFrom if the target * has no code. This can be used to implement an {ERC721}-like safe transfer that rely on {ERC1363} checks when * targeting contracts. * - * Revert if returned value is `false`. If `token` returns no value, non-reverting calls are assumed to be successful. + * Revert if the returned value is other than `true`. */ function transferFromAndCallRelaxed( IERC1363 token, @@ -119,11 +119,11 @@ library SafeERC20 { } /** - * @dev Perform an {ERC1363} approveAndCall, with a fallback to the simple {ERC20} approve if the target has no + * @dev Performs an {ERC1363} approveAndCall, with a fallback to the simple {ERC20} approve if the target has no * code. This can be used to implement an {ERC721}-like safe transfer that rely on {ERC1363} checks when * targeting contracts. * - * Revert if returned value is `false`. If `token` returns no value, non-reverting calls are assumed to be successful. + * Revert if the returned value is other than `true`. */ function approveAndCallRelaxed(IERC1363 token, address to, uint256 value, bytes memory data) internal { if (to.code.length == 0) { diff --git a/test/token/ERC20/utils/SafeERC20.test.js b/test/token/ERC20/utils/SafeERC20.test.js index 4c6d5ffb5ee..dbbb61dcc3e 100644 --- a/test/token/ERC20/utils/SafeERC20.test.js +++ b/test/token/ERC20/utils/SafeERC20.test.js @@ -16,8 +16,8 @@ async function fixture() { const erc20NoReturnMock = await ethers.deployContract('$ERC20NoReturnMock', [name, symbol]); const erc20ForceApproveMock = await ethers.deployContract('$ERC20ForceApproveMock', [name, symbol]); const erc1363Mock = await ethers.deployContract('$ERC1363', [name, symbol]); - const erc1363ReturnFalseMock1 = await ethers.deployContract('$ERC1363ReturnFalseMock1', [name, symbol]); - const erc1363ReturnFalseMock2 = await ethers.deployContract('$ERC1363ReturnFalseMock2', [name, symbol]); + const erc1363ReturnFalseOnErc20Mock = await ethers.deployContract('$ERC1363ReturnFalseOnERC20Mock', [name, symbol]); + const erc1363ReturnFalseMock = await ethers.deployContract('$ERC1363ReturnFalseMock', [name, symbol]); const erc1363NoReturnMock = await ethers.deployContract('$ERC1363NoReturnMock', [name, symbol]); const erc1363Receiver = await ethers.deployContract('$ERC1363ReceiverMock'); const erc1363Spender = await ethers.deployContract('$ERC1363SpenderMock'); @@ -34,8 +34,8 @@ async function fixture() { erc20NoReturnMock, erc20ForceApproveMock, erc1363Mock, - erc1363ReturnFalseMock1, - erc1363ReturnFalseMock2, + erc1363ReturnFalseOnErc20Mock, + erc1363ReturnFalseMock, erc1363NoReturnMock, erc1363Receiver, erc1363Spender, @@ -133,7 +133,7 @@ describe('SafeERC20', function () { shouldOnlyRevertOnErrors(); }); - describe('with usdt approval beaviour', function () { + describe('with usdt approval behaviour', function () { beforeEach(async function () { this.token = this.erc20ForceApproveMock; }); @@ -236,7 +236,7 @@ describe('SafeERC20', function () { describe('with ERC1363 that returns false on all ERC20 calls', function () { beforeEach(async function () { - this.token = this.erc1363ReturnFalseMock1; + this.token = this.erc1363ReturnFalseOnErc20Mock; }); it('reverts on transferAndCallRelaxed', async function () { @@ -260,7 +260,7 @@ describe('SafeERC20', function () { describe('with ERC1363 that returns false on all ERC1363 calls', function () { beforeEach(async function () { - this.token = this.erc1363ReturnFalseMock2; + this.token = this.erc1363ReturnFalseMock; }); it('reverts on transferAndCallRelaxed', async function () { From 6747cb810590d9facd00b23e523c2616e45fc7e6 Mon Sep 17 00:00:00 2001 From: ernestognw Date: Tue, 23 Jan 2024 11:18:57 -0600 Subject: [PATCH 78/82] Add note for usdt-like approval requirements in approveAndCallRelaxed --- .../mocks/token/ERC1363ForceApproveMock.sol | 14 ++++++++ contracts/token/ERC20/utils/SafeERC20.sol | 4 +++ test/token/ERC20/utils/SafeERC20.test.js | 36 +++++++++++++++++++ 3 files changed, 54 insertions(+) create mode 100644 contracts/mocks/token/ERC1363ForceApproveMock.sol diff --git a/contracts/mocks/token/ERC1363ForceApproveMock.sol b/contracts/mocks/token/ERC1363ForceApproveMock.sol new file mode 100644 index 00000000000..d911a0aceae --- /dev/null +++ b/contracts/mocks/token/ERC1363ForceApproveMock.sol @@ -0,0 +1,14 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.20; + +import {IERC20} from "../../interfaces/IERC20.sol"; +import {ERC20, ERC1363} from "../../token/ERC20/extensions/ERC1363.sol"; + +// contract that replicate USDT approval behavior in approveAndCall +abstract contract ERC1363ForceApproveMock is ERC1363 { + function approveAndCall(address spender, uint256 amount, bytes memory data) public virtual override returns (bool) { + require(amount == 0 || allowance(msg.sender, spender) == 0, "USDT approval failure"); + return super.approveAndCall(spender, amount, data); + } +} diff --git a/contracts/token/ERC20/utils/SafeERC20.sol b/contracts/token/ERC20/utils/SafeERC20.sol index 197043a9622..c916a16880e 100644 --- a/contracts/token/ERC20/utils/SafeERC20.sol +++ b/contracts/token/ERC20/utils/SafeERC20.sol @@ -123,6 +123,10 @@ library SafeERC20 { * code. This can be used to implement an {ERC721}-like safe transfer that rely on {ERC1363} checks when * targeting contracts. * + * NOTE: When the recipient address (`to`) has no code (i.e. is an EOA), this function behaves as {forceApprove}. + * Opposedly, when the recipient address (`to`) has code, this function only attempts to call {ERC1363-approveAndCall} + * once without retrying, and relies on the returned value to be true. + * * Revert if the returned value is other than `true`. */ function approveAndCallRelaxed(IERC1363 token, address to, uint256 value, bytes memory data) internal { diff --git a/test/token/ERC20/utils/SafeERC20.test.js b/test/token/ERC20/utils/SafeERC20.test.js index dbbb61dcc3e..60bcc55461e 100644 --- a/test/token/ERC20/utils/SafeERC20.test.js +++ b/test/token/ERC20/utils/SafeERC20.test.js @@ -19,6 +19,7 @@ async function fixture() { const erc1363ReturnFalseOnErc20Mock = await ethers.deployContract('$ERC1363ReturnFalseOnERC20Mock', [name, symbol]); const erc1363ReturnFalseMock = await ethers.deployContract('$ERC1363ReturnFalseMock', [name, symbol]); const erc1363NoReturnMock = await ethers.deployContract('$ERC1363NoReturnMock', [name, symbol]); + const erc1363ForceApproveMock = await ethers.deployContract('$ERC1363ForceApproveMock', [name, symbol]); const erc1363Receiver = await ethers.deployContract('$ERC1363ReceiverMock'); const erc1363Spender = await ethers.deployContract('$ERC1363SpenderMock'); @@ -37,6 +38,7 @@ async function fixture() { erc1363ReturnFalseOnErc20Mock, erc1363ReturnFalseMock, erc1363NoReturnMock, + erc1363ForceApproveMock, erc1363Receiver, erc1363Spender, }; @@ -305,6 +307,40 @@ describe('SafeERC20', function () { ).to.be.revertedWithoutReason(); }); }); + + describe('with ERC1363 with usdt approval behaviour', function () { + beforeEach(async function () { + this.token = this.erc1363ForceApproveMock; + }); + + describe('without initial approval', function () { + it('approveAndCallRelaxed works when recipient is an EOA', async function () { + await this.mock.$approveAndCallRelaxed(this.token, this.spender, 10n, data); + expect(await this.token.allowance(this.mock, this.spender)).to.equal(10n); + }); + + it('approveAndCallRelaxed works when recipient is a contract', async function () { + await this.mock.$approveAndCallRelaxed(this.token, this.erc1363Spender, 10n, data); + expect(await this.token.allowance(this.mock, this.erc1363Spender)).to.equal(10n); + }); + }); + + describe('with initial approval', function () { + it('approveAndCallRelaxed works when recipient is an EOA', async function () { + await this.token.$_approve(this.mock, this.spender, 100n); + + await this.mock.$approveAndCallRelaxed(this.token, this.spender, 10n, data); + expect(await this.token.allowance(this.mock, this.spender)).to.equal(10n); + }); + + it('approveAndCallRelaxed reverts when recipient is a contract', async function () { + await this.token.$_approve(this.mock, this.erc1363Spender, 100n); + await expect(this.mock.$approveAndCallRelaxed(this.token, this.erc1363Spender, 10n, data)).to.be.revertedWith( + 'USDT approval failure', + ); + }); + }); + }); }); function shouldOnlyRevertOnErrors() { From 96551b069f5519306dbf06aa9fd8fbf897ab5d39 Mon Sep 17 00:00:00 2001 From: ernestognw Date: Tue, 23 Jan 2024 11:29:21 -0600 Subject: [PATCH 79/82] Moar nits --- contracts/mocks/token/ERC1363ReceiverMock.sol | 4 +- contracts/token/ERC20/extensions/ERC1363.sol | 45 ++++++++++--------- 2 files changed, 26 insertions(+), 23 deletions(-) diff --git a/contracts/mocks/token/ERC1363ReceiverMock.sol b/contracts/mocks/token/ERC1363ReceiverMock.sol index d33e05e42f7..8be21dab60b 100644 --- a/contracts/mocks/token/ERC1363ReceiverMock.sol +++ b/contracts/mocks/token/ERC1363ReceiverMock.sol @@ -24,9 +24,9 @@ contract ERC1363ReceiverMock is IERC1363Receiver { _error = RevertType.None; } - function setUp(bytes4 retval, RevertType error) public { + function setUp(bytes4 retval, RevertType err) public { _retval = retval; - _error = error; + _error = err; } function onTransferReceived( diff --git a/contracts/token/ERC20/extensions/ERC1363.sol b/contracts/token/ERC20/extensions/ERC1363.sol index 8c11d10804c..ed2f739c89c 100644 --- a/contracts/token/ERC20/extensions/ERC1363.sol +++ b/contracts/token/ERC20/extensions/ERC1363.sol @@ -54,19 +54,20 @@ abstract contract ERC1363 is ERC20, ERC165, IERC1363 { * @dev Moves a `value` amount of tokens from the caller's account to `to` * and then calls {IERC1363Receiver-onTransferReceived} on `to`. * - * NOTE: Reverts if `to` is an account without code or the recipient contract does not implement - * {IERC1363Receiver-onTransferReceived} + * Requirements: + * + * - The target has code (i.e. is a contract). + * - The target `to` must implement the {IERC1363Receiver} interface. + * - The target should return the {IERC1363Receiver} interface id. + * - The internal {transfer} must have succeeded (returned `true`) */ function transferAndCall(address to, uint256 value) public returns (bool) { return transferAndCall(to, value, ""); } /** - * @dev Moves a `value` amount of tokens from the caller's account to `to` - * and then calls {IERC1363Receiver-onTransferReceived} on `to`. - * - * NOTE: Reverts if `to` is an account without code or the recipient contract does not implement - * {IERC1363Receiver-onTransferReceived} + * @dev Variant of {transferAndCall} that accepts an additional `data` parameter with + * no specified format. */ function transferAndCall(address to, uint256 value, bytes memory data) public virtual returns (bool) { if (!transfer(to, value)) { @@ -80,19 +81,20 @@ abstract contract ERC1363 is ERC20, ERC165, IERC1363 { * @dev Moves a `value` amount of tokens from `from` to `to` using the allowance mechanism * and then calls {IERC1363Receiver-onTransferReceived} on `to`. * - * NOTE: Reverts if `to` is an account without code or the recipient contract does not implement - * {IERC1363Receiver-onTransferReceived} + * Requirements: + * + * - The target has code (i.e. is a contract). + * - The target `to` must implement the {IERC1363Receiver} interface. + * - The target should return the {IERC1363Receiver} interface id. + * - The internal {transferFrom} must have succeeded (returned `true`) */ function transferFromAndCall(address from, address to, uint256 value) public returns (bool) { return transferFromAndCall(from, to, value, ""); } /** - * @dev Moves a `value` amount of tokens from `from` to `to` using the allowance mechanism - * and then calls {IERC1363Receiver-onTransferReceived} on `to`. - * - * NOTE: Reverts if `to` is an account without code or the recipient contract does not implement - * {IERC1363Receiver-onTransferReceived} + * @dev Variant of {transferFromAndCall} that accepts an additional `data` parameter with + * no specified format. */ function transferFromAndCall( address from, @@ -111,19 +113,20 @@ abstract contract ERC1363 is ERC20, ERC165, IERC1363 { * @dev Sets a `value` amount of tokens as the allowance of `spender` over the * caller's tokens and then calls {IERC1363Spender-onApprovalReceived} on `spender`. * - * NOTE: Reverts if `spender` is an account without code or the spender contract does not implement - * {IERC1363Spender-onApprovalReceived} + * Requirements: + * + * - The target has code (i.e. is a contract). + * - The target `to` must implement the {IERC1363Spender} interface. + * - The target should return the {IERC1363Spender} interface id. + * - The internal {approve} must have succeeded (returned `true`) */ function approveAndCall(address spender, uint256 value) public returns (bool) { return approveAndCall(spender, value, ""); } /** - * @dev Sets a `value` amount of tokens as the allowance of `spender` over the - * caller's tokens and then calls {IERC1363Spender-onApprovalReceived} on `spender`. - * - * NOTE: Reverts if `spender` is an account without code or the spender contract does not implement - * {IERC1363Spender-onApprovalReceived} + * @dev Variant of {approveAndCall} that accepts an additional `data` parameter with + * no specified format. */ function approveAndCall(address spender, uint256 value, bytes memory data) public virtual returns (bool) { if (!approve(spender, value)) { From 54f06e77653eb3ce80bd0d6d25bd070148969e30 Mon Sep 17 00:00:00 2001 From: ernestognw Date: Tue, 23 Jan 2024 12:00:41 -0600 Subject: [PATCH 80/82] Improve ERC1363 tests --- contracts/token/ERC20/extensions/ERC1363.sol | 12 +-- contracts/token/ERC20/utils/SafeERC20.sol | 6 +- test/token/ERC20/extensions/ERC1363.test.js | 85 +++++++++++++------- 3 files changed, 66 insertions(+), 37 deletions(-) diff --git a/contracts/token/ERC20/extensions/ERC1363.sol b/contracts/token/ERC20/extensions/ERC1363.sol index ed2f739c89c..bcc4d807f8c 100644 --- a/contracts/token/ERC20/extensions/ERC1363.sol +++ b/contracts/token/ERC20/extensions/ERC1363.sol @@ -59,14 +59,14 @@ abstract contract ERC1363 is ERC20, ERC165, IERC1363 { * - The target has code (i.e. is a contract). * - The target `to` must implement the {IERC1363Receiver} interface. * - The target should return the {IERC1363Receiver} interface id. - * - The internal {transfer} must have succeeded (returned `true`) + * - The internal {transfer} must have succeeded (returned `true`). */ function transferAndCall(address to, uint256 value) public returns (bool) { return transferAndCall(to, value, ""); } /** - * @dev Variant of {transferAndCall} that accepts an additional `data` parameter with + * @dev Variant of {transferAndCall} that accepts an additional `data` parameter with * no specified format. */ function transferAndCall(address to, uint256 value, bytes memory data) public virtual returns (bool) { @@ -86,14 +86,14 @@ abstract contract ERC1363 is ERC20, ERC165, IERC1363 { * - The target has code (i.e. is a contract). * - The target `to` must implement the {IERC1363Receiver} interface. * - The target should return the {IERC1363Receiver} interface id. - * - The internal {transferFrom} must have succeeded (returned `true`) + * - The internal {transferFrom} must have succeeded (returned `true`). */ function transferFromAndCall(address from, address to, uint256 value) public returns (bool) { return transferFromAndCall(from, to, value, ""); } /** - * @dev Variant of {transferFromAndCall} that accepts an additional `data` parameter with + * @dev Variant of {transferFromAndCall} that accepts an additional `data` parameter with * no specified format. */ function transferFromAndCall( @@ -118,14 +118,14 @@ abstract contract ERC1363 is ERC20, ERC165, IERC1363 { * - The target has code (i.e. is a contract). * - The target `to` must implement the {IERC1363Spender} interface. * - The target should return the {IERC1363Spender} interface id. - * - The internal {approve} must have succeeded (returned `true`) + * - The internal {approve} must have succeeded (returned `true`). */ function approveAndCall(address spender, uint256 value) public returns (bool) { return approveAndCall(spender, value, ""); } /** - * @dev Variant of {approveAndCall} that accepts an additional `data` parameter with + * @dev Variant of {approveAndCall} that accepts an additional `data` parameter with * no specified format. */ function approveAndCall(address spender, uint256 value, bytes memory data) public virtual returns (bool) { diff --git a/contracts/token/ERC20/utils/SafeERC20.sol b/contracts/token/ERC20/utils/SafeERC20.sol index c916a16880e..58f9fcf4d68 100644 --- a/contracts/token/ERC20/utils/SafeERC20.sol +++ b/contracts/token/ERC20/utils/SafeERC20.sol @@ -87,7 +87,7 @@ library SafeERC20 { * code. This can be used to implement an {ERC721}-like safe transfer that rely on {ERC1363} checks when * targeting contracts. * - * Revert if the returned value is other than `true`. + * Reverts if the returned value is other than `true`. */ function transferAndCallRelaxed(IERC1363 token, address to, uint256 value, bytes memory data) internal { if (to.code.length == 0) { @@ -102,7 +102,7 @@ library SafeERC20 { * has no code. This can be used to implement an {ERC721}-like safe transfer that rely on {ERC1363} checks when * targeting contracts. * - * Revert if the returned value is other than `true`. + * Reverts if the returned value is other than `true`. */ function transferFromAndCallRelaxed( IERC1363 token, @@ -127,7 +127,7 @@ library SafeERC20 { * Opposedly, when the recipient address (`to`) has code, this function only attempts to call {ERC1363-approveAndCall} * once without retrying, and relies on the returned value to be true. * - * Revert if the returned value is other than `true`. + * Reverts if the returned value is other than `true`. */ function approveAndCallRelaxed(IERC1363 token, address to, uint256 value, bytes memory data) internal { if (to.code.length == 0) { diff --git a/test/token/ERC20/extensions/ERC1363.test.js b/test/token/ERC20/extensions/ERC1363.test.js index 567984d8118..d9fa7f44d3b 100644 --- a/test/token/ERC20/extensions/ERC1363.test.js +++ b/test/token/ERC20/extensions/ERC1363.test.js @@ -2,7 +2,11 @@ const { ethers } = require('hardhat'); const { expect } = require('chai'); const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers'); -const { shouldBehaveLikeERC20 } = require('../ERC20.behavior.js'); +const { + shouldBehaveLikeERC20, + shouldBehaveLikeERC20Transfer, + shouldBehaveLikeERC20Approve, +} = require('../ERC20.behavior.js'); const { shouldSupportInterfaces } = require('../../../utils/introspection/SupportsInterface.behavior'); const { RevertType } = require('../../../helpers/enums.js'); @@ -43,19 +47,25 @@ describe('ERC1363', function () { shouldSupportInterfaces(['ERC165', 'ERC1363']); shouldBehaveLikeERC20(value); - // Note: transferAndCall(address,uint256) fails "shouldBehaveLikeERC20Transfer" because it revert on EOAs - // Note: transferAndCall(address,uint256,bytes) fails "shouldBehaveLikeERC20Transfer" because it revert on EOAs - // Note: approveAndCall(address,uint256) fails "shouldBehaveLikeERC20Approve" because it revert on EOAs - // Note: approveAndCall(address,uint256,bytes) fails "shouldBehaveLikeERC20Approve" because it revert on EOAs describe('transferAndCall', function () { - it('to an EOA', async function () { + describe('as a transfer', function () { + beforeEach(async function () { + this.recipient = this.receiver; + this.transfer = (holder, ...rest) => + this.token.connect(holder).getFunction('transferAndCall(address,uint256)')(...rest); + }); + + shouldBehaveLikeERC20Transfer(value); + }); + + it('reverts transfering to an EOA', async function () { await expect(this.token.connect(this.holder).getFunction('transferAndCall(address,uint256)')(this.other, value)) .to.be.revertedWithCustomError(this.token, 'ERC1363InvalidReceiver') .withArgs(this.other.address); }); - it('without data', async function () { + it('succeeds without data', async function () { await expect( this.token.connect(this.holder).getFunction('transferAndCall(address,uint256)')(this.receiver, value), ) @@ -65,7 +75,7 @@ describe('ERC1363', function () { .withArgs(this.holder.address, this.holder.address, value, '0x'); }); - it('with data', async function () { + it('succeeds with data', async function () { await expect( this.token.connect(this.holder).getFunction('transferAndCall(address,uint256,bytes)')( this.receiver, @@ -79,7 +89,7 @@ describe('ERC1363', function () { .withArgs(this.holder.address, this.holder.address, value, data); }); - it('with reverting hook (without reason)', async function () { + it('reverts with reverting hook (without reason)', async function () { await this.receiver.setUp(this.selectors.onTransferReceived, RevertType.RevertWithoutMessage); await expect( @@ -93,7 +103,7 @@ describe('ERC1363', function () { .withArgs(this.receiver.target); }); - it('with reverting hook (with reason)', async function () { + it('reverts with reverting hook (with reason)', async function () { await this.receiver.setUp(this.selectors.onTransferReceived, RevertType.RevertWithMessage); await expect( @@ -105,7 +115,7 @@ describe('ERC1363', function () { ).to.be.revertedWith('ERC1363ReceiverMock: reverting'); }); - it('with reverting hook (with custom error)', async function () { + it('reverts with reverting hook (with custom error)', async function () { const reason = '0x12345678'; await this.receiver.setUp(reason, RevertType.RevertWithCustomError); @@ -120,7 +130,7 @@ describe('ERC1363', function () { .withArgs(reason); }); - it('with reverting hook (with panic)', async function () { + it('panics with reverting hook (with panic)', async function () { await this.receiver.setUp(this.selectors.onTransferReceived, RevertType.Panic); await expect( @@ -132,7 +142,7 @@ describe('ERC1363', function () { ).to.be.revertedWithPanic(); }); - it('with bad return value', async function () { + it('reverts with bad return value', async function () { await this.receiver.setUp('0x12345678', RevertType.None); await expect( @@ -152,7 +162,16 @@ describe('ERC1363', function () { await this.token.connect(this.holder).approve(this.other, ethers.MaxUint256); }); - it('to an EOA', async function () { + describe('as a transfer', function () { + beforeEach(async function () { + this.recipient = this.receiver; + this.transfer = this.token.connect(this.other).getFunction('transferFromAndCall(address,address,uint256)'); + }); + + shouldBehaveLikeERC20Transfer(value); + }); + + it('reverts transfering to an EOA', async function () { await expect( this.token.connect(this.other).getFunction('transferFromAndCall(address,address,uint256)')( this.holder, @@ -164,7 +183,7 @@ describe('ERC1363', function () { .withArgs(this.other.address); }); - it('without data', async function () { + it('succeeds without data', async function () { await expect( this.token.connect(this.other).getFunction('transferFromAndCall(address,address,uint256)')( this.holder, @@ -178,7 +197,7 @@ describe('ERC1363', function () { .withArgs(this.other.address, this.holder.address, value, '0x'); }); - it('with data', async function () { + it('succeeds with data', async function () { await expect( this.token.connect(this.other).getFunction('transferFromAndCall(address,address,uint256,bytes)')( this.holder, @@ -193,7 +212,7 @@ describe('ERC1363', function () { .withArgs(this.other.address, this.holder.address, value, data); }); - it('with reverting hook (without reason)', async function () { + it('reverts with reverting hook (without reason)', async function () { await this.receiver.setUp(this.selectors.onTransferReceived, RevertType.RevertWithoutMessage); await expect( @@ -208,7 +227,7 @@ describe('ERC1363', function () { .withArgs(this.receiver.target); }); - it('with reverting hook (with reason)', async function () { + it('reverts with reverting hook (with reason)', async function () { await this.receiver.setUp(this.selectors.onTransferReceived, RevertType.RevertWithMessage); await expect( @@ -221,7 +240,7 @@ describe('ERC1363', function () { ).to.be.revertedWith('ERC1363ReceiverMock: reverting'); }); - it('with reverting hook (with custom error)', async function () { + it('reverts with reverting hook (with custom error)', async function () { const reason = '0x12345678'; await this.receiver.setUp(reason, RevertType.RevertWithCustomError); @@ -237,7 +256,7 @@ describe('ERC1363', function () { .withArgs(reason); }); - it('with reverting hook (with panic)', async function () { + it('panics with reverting hook (with panic)', async function () { await this.receiver.setUp(this.selectors.onTransferReceived, RevertType.Panic); await expect( @@ -250,7 +269,7 @@ describe('ERC1363', function () { ).to.be.revertedWithPanic(); }); - it('with bad return value', async function () { + it('reverts with bad return value', async function () { await this.receiver.setUp('0x12345678', RevertType.None); await expect( @@ -267,13 +286,23 @@ describe('ERC1363', function () { }); describe('approveAndCall', function () { - it('an EOA', async function () { + describe('as an approval', function () { + beforeEach(async function () { + this.recipient = this.spender; + this.approve = (holder, ...rest) => + this.token.connect(holder).getFunction('approveAndCall(address,uint256)')(...rest); + }); + + shouldBehaveLikeERC20Approve(value); + }); + + it('reverts approving an EOA', async function () { await expect(this.token.connect(this.holder).getFunction('approveAndCall(address,uint256)')(this.other, value)) .to.be.revertedWithCustomError(this.token, 'ERC1363InvalidSpender') .withArgs(this.other.address); }); - it('without data', async function () { + it('succeeds without data', async function () { await expect(this.token.connect(this.holder).getFunction('approveAndCall(address,uint256)')(this.spender, value)) .to.emit(this.token, 'Approval') .withArgs(this.holder.address, this.spender.target, value) @@ -281,7 +310,7 @@ describe('ERC1363', function () { .withArgs(this.holder.address, value, '0x'); }); - it('with data', async function () { + it('succeeds with data', async function () { await expect( this.token.connect(this.holder).getFunction('approveAndCall(address,uint256,bytes)')(this.spender, value, data), ) @@ -301,7 +330,7 @@ describe('ERC1363', function () { .withArgs(this.spender.target); }); - it('with reverting hook (with reason)', async function () { + it('reverts with reverting hook (with reason)', async function () { await this.spender.setUp(this.selectors.onApprovalReceived, RevertType.RevertWithMessage); await expect( @@ -309,7 +338,7 @@ describe('ERC1363', function () { ).to.be.revertedWith('ERC1363SpenderMock: reverting'); }); - it('with reverting hook (with custom error)', async function () { + it('reverts with reverting hook (with custom error)', async function () { const reason = '0x12345678'; await this.spender.setUp(reason, RevertType.RevertWithCustomError); @@ -320,7 +349,7 @@ describe('ERC1363', function () { .withArgs(reason); }); - it('with reverting hook (with panic)', async function () { + it('panics with reverting hook (with panic)', async function () { await this.spender.setUp(this.selectors.onApprovalReceived, RevertType.Panic); await expect( @@ -328,7 +357,7 @@ describe('ERC1363', function () { ).to.be.revertedWithPanic(); }); - it('with bad return value', async function () { + it('reverts with bad return value', async function () { await this.spender.setUp('0x12345678', RevertType.None); await expect( From 12e29527d801cf9b9356f9ea85c5ea1bb2e32540 Mon Sep 17 00:00:00 2001 From: ernestognw Date: Tue, 23 Jan 2024 12:02:30 -0600 Subject: [PATCH 81/82] Fix codespell --- test/token/ERC20/extensions/ERC1363.test.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/token/ERC20/extensions/ERC1363.test.js b/test/token/ERC20/extensions/ERC1363.test.js index d9fa7f44d3b..3d1f4e58f8e 100644 --- a/test/token/ERC20/extensions/ERC1363.test.js +++ b/test/token/ERC20/extensions/ERC1363.test.js @@ -59,7 +59,7 @@ describe('ERC1363', function () { shouldBehaveLikeERC20Transfer(value); }); - it('reverts transfering to an EOA', async function () { + it('reverts transferring to an EOA', async function () { await expect(this.token.connect(this.holder).getFunction('transferAndCall(address,uint256)')(this.other, value)) .to.be.revertedWithCustomError(this.token, 'ERC1363InvalidReceiver') .withArgs(this.other.address); @@ -171,7 +171,7 @@ describe('ERC1363', function () { shouldBehaveLikeERC20Transfer(value); }); - it('reverts transfering to an EOA', async function () { + it('reverts transferring to an EOA', async function () { await expect( this.token.connect(this.other).getFunction('transferFromAndCall(address,address,uint256)')( this.holder, From 2c854746cd21c601f09e770c988a2d46e8a2ac82 Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Wed, 24 Jan 2024 09:31:33 +0100 Subject: [PATCH 82/82] final cleanup (minimize changes) --- contracts/mocks/token/ERC1363ReceiverMock.sol | 4 ++-- contracts/token/ERC20/extensions/ERC1363.sol | 6 +++--- lib/forge-std | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/contracts/mocks/token/ERC1363ReceiverMock.sol b/contracts/mocks/token/ERC1363ReceiverMock.sol index 8be21dab60b..d33e05e42f7 100644 --- a/contracts/mocks/token/ERC1363ReceiverMock.sol +++ b/contracts/mocks/token/ERC1363ReceiverMock.sol @@ -24,9 +24,9 @@ contract ERC1363ReceiverMock is IERC1363Receiver { _error = RevertType.None; } - function setUp(bytes4 retval, RevertType err) public { + function setUp(bytes4 retval, RevertType error) public { _retval = retval; - _error = err; + _error = error; } function onTransferReceived( diff --git a/contracts/token/ERC20/extensions/ERC1363.sol b/contracts/token/ERC20/extensions/ERC1363.sol index bcc4d807f8c..1d9bbddcd11 100644 --- a/contracts/token/ERC20/extensions/ERC1363.sol +++ b/contracts/token/ERC20/extensions/ERC1363.sol @@ -59,7 +59,7 @@ abstract contract ERC1363 is ERC20, ERC165, IERC1363 { * - The target has code (i.e. is a contract). * - The target `to` must implement the {IERC1363Receiver} interface. * - The target should return the {IERC1363Receiver} interface id. - * - The internal {transfer} must have succeeded (returned `true`). + * - The internal {transfer} must succeed (returned `true`). */ function transferAndCall(address to, uint256 value) public returns (bool) { return transferAndCall(to, value, ""); @@ -86,7 +86,7 @@ abstract contract ERC1363 is ERC20, ERC165, IERC1363 { * - The target has code (i.e. is a contract). * - The target `to` must implement the {IERC1363Receiver} interface. * - The target should return the {IERC1363Receiver} interface id. - * - The internal {transferFrom} must have succeeded (returned `true`). + * - The internal {transferFrom} must succeed (returned `true`). */ function transferFromAndCall(address from, address to, uint256 value) public returns (bool) { return transferFromAndCall(from, to, value, ""); @@ -118,7 +118,7 @@ abstract contract ERC1363 is ERC20, ERC165, IERC1363 { * - The target has code (i.e. is a contract). * - The target `to` must implement the {IERC1363Spender} interface. * - The target should return the {IERC1363Spender} interface id. - * - The internal {approve} must have succeeded (returned `true`). + * - The internal {approve} must succeed (returned `true`). */ function approveAndCall(address spender, uint256 value) public returns (bool) { return approveAndCall(spender, value, ""); diff --git a/lib/forge-std b/lib/forge-std index ae570fec082..c2236853aad 160000 --- a/lib/forge-std +++ b/lib/forge-std @@ -1 +1 @@ -Subproject commit ae570fec082bfe1c1f45b0acca4a2b4f84d345ce +Subproject commit c2236853aadb8e2d9909bbecdc490099519b70a4