Add ERC1363 implementation (#4631)

Co-authored-by: Hadrien Croubois <hadrien.croubois@gmail.com>
Co-authored-by: ernestognw <ernestognw@gmail.com>
pull/4854/head
Vittorio Minacori 1 year ago committed by GitHub
parent a51f1e1354
commit e5f02bc608
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 5
      .changeset/friendly-nails-push.md
  2. 5
      .changeset/nice-paws-pull.md
  3. 86
      contracts/interfaces/IERC1363.sol
  4. 37
      contracts/interfaces/IERC1363Receiver.sol
  5. 33
      contracts/interfaces/IERC1363Spender.sol
  6. 14
      contracts/mocks/token/ERC1363ForceApproveMock.sol
  7. 34
      contracts/mocks/token/ERC1363NoReturnMock.sol
  8. 52
      contracts/mocks/token/ERC1363ReceiverMock.sol
  9. 34
      contracts/mocks/token/ERC1363ReturnFalseMock.sol
  10. 47
      contracts/mocks/token/ERC1363SpenderMock.sol
  11. 3
      contracts/token/ERC20/README.adoc
  12. 198
      contracts/token/ERC20/extensions/ERC1363.sol
  13. 57
      contracts/token/ERC20/utils/SafeERC20.sol
  14. 112
      test/token/ERC20/ERC20.behavior.js
  15. 40
      test/token/ERC20/ERC20.test.js
  16. 370
      test/token/ERC20/extensions/ERC1363.test.js
  17. 8
      test/token/ERC20/extensions/ERC20FlashMint.test.js
  18. 8
      test/token/ERC20/extensions/ERC20Permit.test.js
  19. 90
      test/token/ERC20/extensions/ERC20Wrapper.test.js
  20. 201
      test/token/ERC20/utils/SafeERC20.test.js
  21. 8
      test/utils/introspection/SupportsInterface.behavior.js

@ -0,0 +1,5 @@
---
'openzeppelin-solidity': minor
---
`ERC1363`: Add implementation of the token payable standard allowing execution of contract code after transfers and approvals.

@ -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.

@ -7,13 +7,13 @@ import {IERC20} from "./IERC20.sol";
import {IERC165} from "./IERC165.sol";
/**
* @dev Interface of an ERC-1363 compliant contract, as defined in the
* https://eips.ethereum.org/EIPS/eip-1363[ERC].
* @title IERC1363
* @dev Interface of the ERC-1363 standard as defined in the https://eips.ethereum.org/EIPS/eip-1363[ERC-1363].
*
* Defines a interface for ERC-20 tokens that supports executing recipient
* code after `transfer` or `transferFrom`, or spender code after `approve`.
* Defines an extension interface for ERC-20 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 IERC165, IERC20 {
interface IERC1363 is IERC20, IERC165 {
/*
* Note: the ERC-165 identifier for this interface is 0xb0202a11.
* 0xb0202a11 ===
@ -26,55 +26,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 {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.
*/
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 {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`.
* @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 {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.
* @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 {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.
* @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 {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.
*/
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 {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`.
* @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);
}

@ -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 ERC-1363 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 ERC-1363 tokens
* @dev Any ERC-1363 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 ERC-1363 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);
}

@ -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 ERC-1363 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 ERC-1363 tokens
* @dev Any ERC-1363 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 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)"))`
* (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);
}

@ -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);
}
}

@ -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)
}
}
}

@ -0,0 +1,52 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import {IERC1363Receiver} from "../../interfaces/IERC1363Receiver.sol";
contract ERC1363ReceiverMock is IERC1363Receiver {
enum RevertType {
None,
RevertWithoutMessage,
RevertWithMessage,
RevertWithCustomError,
Panic
}
bytes4 private _retval;
RevertType private _error;
event Received(address operator, address from, uint256 value, bytes data);
error CustomError(bytes4);
constructor() {
_retval = IERC1363Receiver.onTransferReceived.selector;
_error = RevertType.None;
}
function setUp(bytes4 retval, RevertType error) public {
_retval = retval;
_error = error;
}
function onTransferReceived(
address operator,
address from,
uint256 value,
bytes calldata data
) external 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);
return _retval;
}
}

@ -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 ERC1363ReturnFalseOnERC20Mock 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 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;
}
}

@ -0,0 +1,47 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import {IERC1363Spender} from "../../interfaces/IERC1363Spender.sol";
contract ERC1363SpenderMock is IERC1363Spender {
enum RevertType {
None,
RevertWithoutMessage,
RevertWithMessage,
RevertWithCustomError,
Panic
}
bytes4 private _retval;
RevertType private _error;
event Approved(address owner, uint256 value, bytes data);
error CustomError(bytes4);
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) external 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);
return _retval;
}
}

@ -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 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}: 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:
@ -60,6 +61,8 @@ NOTE: This core set of contracts is designed to be unopinionated, allowing devel
{{ERC20FlashMint}}
{{ERC1363}}
{{ERC4626}}
== Utilities

@ -0,0 +1,198 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import {ERC20} from "../ERC20.sol";
import {IERC165, ERC165} from "../../../utils/introspection/ERC165.sol";
import {IERC1363} from "../../../interfaces/IERC1363.sol";
import {IERC1363Receiver} from "../../../interfaces/IERC1363Receiver.sol";
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}
*/
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);
/**
* @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
*/
function supportsInterface(bytes4 interfaceId) public view virtual override(ERC165, IERC165) returns (bool) {
return interfaceId == type(IERC1363).interfaceId || super.supportsInterface(interfaceId);
}
/**
* @dev Moves a `value` amount of tokens from the caller's account to `to`
* and then calls {IERC1363Receiver-onTransferReceived} on `to`.
*
* 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 succeed (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
* no specified format.
*/
function transferAndCall(address to, uint256 value, bytes memory data) public virtual returns (bool) {
if (!transfer(to, value)) {
revert ERC1363TransferFailed(to, value);
}
_checkOnTransferReceived(_msgSender(), to, value, data);
return true;
}
/**
* @dev Moves a `value` amount of tokens from `from` to `to` using the allowance mechanism
* and then calls {IERC1363Receiver-onTransferReceived} on `to`.
*
* 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 succeed (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
* no specified format.
*/
function transferFromAndCall(
address from,
address to,
uint256 value,
bytes memory data
) public virtual returns (bool) {
if (!transferFrom(from, to, value)) {
revert ERC1363TransferFromFailed(from, to, value);
}
_checkOnTransferReceived(from, to, value, data);
return true;
}
/**
* @dev Sets a `value` amount of tokens as the allowance of `spender` over the
* caller's tokens and then calls {IERC1363Spender-onApprovalReceived} on `spender`.
*
* 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 succeed (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
* no specified format.
*/
function approveAndCall(address spender, uint256 value, bytes memory data) public virtual returns (bool) {
if (!approve(spender, value)) {
revert ERC1363ApproveFailed(spender, value);
}
_checkOnApprovalReceived(spender, value, data);
return true;
}
/**
* @dev Performs a call to {IERC1363Receiver-onTransferReceived} on a target address.
*
* 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) {
revert ERC1363InvalidReceiver(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 Performs a call to {IERC1363Spender-onApprovalReceived} on a target address.
*
* 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) {
revert ERC1363InvalidSpender(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))
}
}
}
}
}

@ -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,61 @@ library SafeERC20 {
}
}
/**
* @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.
*
* 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) {
safeTransfer(token, to, value);
} else if (!token.transferAndCall(to, value, data)) {
revert SafeERC20FailedOperation(address(token));
}
}
/**
* @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.
*
* Reverts if the returned value is other than `true`.
*/
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 if (!token.transferFromAndCall(from, to, value, data)) {
revert SafeERC20FailedOperation(address(token));
}
}
/**
* @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.
*
* 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.
*
* 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) {
forceApprove(token, to, value);
} else if (!token.approveAndCall(to, value, data)) {
revert SafeERC20FailedOperation(address(token));
}
}
/**
* @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).

@ -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,34 +35,26 @@ 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 () {
const value = initialSupply;
beforeEach(async function () {
this.tx = await this.token
.connect(this.recipient)
.transferFrom(this.initialHolder, this.anotherAccount, 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.initialHolder, this.anotherAccount],
[-value, value],
);
await expect(this.tx).to.changeTokenBalances(this.token, [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, this.anotherAccount, value);
await expect(this.tx).to.emit(this.token, 'Transfer').withArgs(this.holder, this.other, value);
});
if (forcedApproval) {
@ -66,9 +62,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 +76,10 @@ 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 expect(
this.token.connect(this.recipient).transferFrom(this.initialHolder, this.anotherAccount, value),
)
await this.token.connect(this.holder).transfer(this.other, 1n);
await expect(this.token.connect(this.recipient).transferFrom(this.holder, this.other, value))
.to.revertedWithCustomError(this.token, 'ERC20InsufficientBalance')
.withArgs(this.initialHolder, value - 1n, value);
.withArgs(this.holder, value - 1n, value);
});
});
@ -93,39 +87,33 @@ 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),
)
await expect(this.token.connect(this.recipient).transferFrom(this.holder, this.other, value))
.to.be.revertedWithCustomError(this.token, 'ERC20InsufficientAllowance')
.withArgs(this.recipient, allowance, value);
});
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 expect(
this.token.connect(this.recipient).transferFrom(this.initialHolder, this.anotherAccount, value),
)
await this.token.connect(this.holder).transfer(this.other, 2);
await expect(this.token.connect(this.recipient).transferFrom(this.holder, this.other, value))
.to.be.revertedWithCustomError(this.token, 'ERC20InsufficientBalance')
.withArgs(this.initialHolder, value - 1n, value);
.withArgs(this.holder, 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);
this.tx = await this.token
.connect(this.recipient)
.transferFrom(this.initialHolder, this.anotherAccount, 1n);
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);
});
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 +124,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,24 +152,24 @@ 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, balance, value);
.withArgs(this.holder, 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, this.recipient, value);
await expect(this.tx).to.emit(this.token, 'Transfer').withArgs(this.holder, this.recipient, value);
});
});
@ -189,21 +177,21 @@ 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, this.recipient, value);
await expect(this.tx).to.emit(this.token, 'Transfer').withArgs(this.holder, this.recipient, 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);
});
@ -215,22 +203,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, this.recipient, value);
.withArgs(this.holder, this.recipient, 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);
});
});
@ -238,28 +226,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, this.recipient, value);
.withArgs(this.holder, this.recipient, 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);
});

@ -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 () {
@ -87,29 +89,27 @@ 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, initialSupply, initialSupply + 1n);
.withArgs(this.holder, 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, ethers.ZeroAddress, value);
await expect(this.tx).to.emit(this.token, 'Transfer').withArgs(this.holder, ethers.ZeroAddress, value);
});
});
};
@ -127,19 +127,19 @@ describe('ERC20', function () {
});
it('from is the zero address', async function () {
const tx = await this.token.$_update(ethers.ZeroAddress, this.initialHolder, value);
await expect(tx).to.emit(this.token, 'Transfer').withArgs(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.holder, 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);
await expect(tx).to.emit(this.token, 'Transfer').withArgs(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.holder, 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 () {
@ -159,9 +159,9 @@ 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);
await expect(tx).to.emit(this.token, 'Transfer').withArgs(this.initialHolder, this.initialHolder, value);
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.holder, this.holder, value);
});
});
});

@ -0,0 +1,370 @@
const { ethers } = require('hardhat');
const { expect } = require('chai');
const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers');
const {
shouldBehaveLikeERC20,
shouldBehaveLikeERC20Transfer,
shouldBehaveLikeERC20Approve,
} = require('../ERC20.behavior.js');
const { shouldSupportInterfaces } = require('../../../utils/introspection/SupportsInterface.behavior');
const { RevertType } = require('../../../helpers/enums.js');
const name = 'My Token';
const symbol = 'MTKN';
const value = 1000n;
const data = '0x123456';
async function fixture() {
// 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 {
accounts,
holder,
other,
token,
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 () {
Object.assign(this, await loadFixture(fixture));
});
shouldSupportInterfaces(['ERC165', 'ERC1363']);
shouldBehaveLikeERC20(value);
describe('transferAndCall', 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 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);
});
it('succeeds 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('succeeds 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('reverts 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('reverts 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('reverts 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('panics 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('reverts 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);
});
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 transferring 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('succeeds 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('succeeds 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('reverts 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('reverts 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('reverts 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('panics 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('reverts 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 () {
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('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)
.to.emit(this.spender, 'Approved')
.withArgs(this.holder.address, value, '0x');
});
it('succeeds 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('reverts 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('reverts 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('panics 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('reverts 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);
});
});
});

@ -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);

@ -10,13 +10,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,
@ -30,7 +30,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 () {

@ -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, this.token, initialSupply)
.withArgs(this.holder, this.token, initialSupply)
.to.emit(this.token, 'Transfer')
.withArgs(ethers.ZeroAddress, this.initialHolder, initialSupply);
.withArgs(ethers.ZeroAddress, this.holder, 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, 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, initialSupply, ethers.MaxUint256);
.withArgs(this.holder, 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, this.token, initialSupply)
.withArgs(this.holder, this.token.target, initialSupply)
.to.emit(this.token, 'Transfer')
.withArgs(ethers.ZeroAddress, this.recipient, 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);
});
@ -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, initialSupply, ethers.MaxInt256);
.withArgs(this.holder, 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, this.initialHolder, value)
.withArgs(this.token.target, this.holder, value)
.to.emit(this.token, 'Transfer')
.withArgs(this.initialHolder, 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, 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, this.initialHolder, initialSupply)
.withArgs(this.token.target, this.holder, initialSupply)
.to.emit(this.token, 'Transfer')
.withArgs(this.initialHolder, ethers.ZeroAddress, initialSupply);
.withArgs(this.holder, 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, this.recipient, initialSupply)
.to.emit(this.token, 'Transfer')
.withArgs(this.initialHolder, ethers.ZeroAddress, initialSupply);
.withArgs(this.holder, 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);
});
@ -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, 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).to.emit(this.token, 'Transfer').withArgs(ethers.ZeroAddress, this.recipient, initialSupply);
@ -192,8 +194,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);

@ -4,26 +4,43 @@ 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] = 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 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');
return {
hasNoCode,
owner,
receiver,
spender,
other,
mock,
erc20ReturnFalseMock,
erc20ReturnTrueMock,
erc20NoReturnMock,
erc20ForceApproveMock,
erc1363Mock,
erc1363ReturnFalseOnErc20Mock,
erc1363ReturnFalseMock,
erc1363NoReturnMock,
erc1363ForceApproveMock,
erc1363Receiver,
erc1363Spender,
};
}
@ -118,7 +135,7 @@ describe('SafeERC20', function () {
shouldOnlyRevertOnErrors();
});
describe('with usdt approval beaviour', function () {
describe('with usdt approval behaviour', function () {
beforeEach(async function () {
this.token = this.erc20ForceApproveMock;
});
@ -144,6 +161,186 @@ describe('SafeERC20', function () {
});
});
});
describe('with standard ERC1363', function () {
beforeEach(async function () {
this.token = this.erc1363Mock;
});
shouldOnlyRevertOnErrors();
describe('transferAndCall', function () {
it('cannot transferAndCall to an EOA directly', async function () {
await this.token.$_mint(this.owner, 100n);
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, 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 () {
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('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);
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 () {
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('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')
.withArgs(this.mock, this.receiver, value);
});
it('can approveAndCall to an ERC1363Spender using helper', async function () {
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);
});
});
});
describe('with ERC1363 that returns false on all ERC20 calls', function () {
beforeEach(async function () {
this.token = this.erc1363ReturnFalseOnErc20Mock;
});
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;
});
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();
});
});
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() {

@ -31,6 +31,14 @@ const SIGNATURES = {
'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)',
],
AccessControl: [
'hasRole(bytes32,address)',
'getRoleAdmin(bytes32)',

Loading…
Cancel
Save