diff --git a/contracts/interfaces/IERC1363.sol b/contracts/interfaces/IERC1363.sol index 5fad104c223..0b89fc0726f 100644 --- a/contracts/interfaces/IERC1363.sol +++ b/contracts/interfaces/IERC1363.sol @@ -3,93 +3,4 @@ pragma solidity ^0.8.0; -import "./IERC20.sol"; -import "./IERC165.sol"; - -interface IERC1363 is IERC165, IERC20 { - /* - * Note: the ERC-165 identifier for this interface is 0x4bbee2df. - * 0x4bbee2df === - * bytes4(keccak256('transferAndCall(address,uint256)')) ^ - * bytes4(keccak256('transferAndCall(address,uint256,bytes)')) ^ - * bytes4(keccak256('transferFromAndCall(address,address,uint256)')) ^ - * bytes4(keccak256('transferFromAndCall(address,address,uint256,bytes)')) - */ - - /* - * Note: the ERC-165 identifier for this interface is 0xfb9ec8ce. - * 0xfb9ec8ce === - * bytes4(keccak256('approveAndCall(address,uint256)')) ^ - * bytes4(keccak256('approveAndCall(address,uint256,bytes)')) - */ - - /** - * @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 value uint256 The amount of tokens to be transferred - * @return true unless throwing - */ - 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 value 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 - */ - function transferAndCall( - address to, - uint256 value, - bytes memory 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 value uint256 The amount of tokens to be transferred - * @return true unless throwing - */ - 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 value 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 - */ - function transferFromAndCall( - address from, - address to, - uint256 value, - bytes memory 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 value uint256 The amount of tokens to be spent - */ - 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 value uint256 The amount of tokens to be spent - * @param data bytes Additional data with no specified format, sent in call to `spender` - */ - function approveAndCall( - address spender, - uint256 value, - bytes memory data - ) external returns (bool); -} +import "../token/ERC1363/IERC1363.sol"; diff --git a/contracts/interfaces/IERC1363Receiver.sol b/contracts/interfaces/IERC1363Receiver.sol index bc5eaddb042..f09d84878aa 100644 --- a/contracts/interfaces/IERC1363Receiver.sol +++ b/contracts/interfaces/IERC1363Receiver.sol @@ -3,30 +3,4 @@ pragma solidity ^0.8.0; -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 value 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 - */ - function onTransferReceived( - address operator, - address from, - uint256 value, - bytes memory data - ) external returns (bytes4); -} +import "../token/ERC1363/IERC1363Receiver.sol"; diff --git a/contracts/interfaces/IERC1363Spender.sol b/contracts/interfaces/IERC1363Spender.sol index 48f6fd56d6d..071fcf67e5e 100644 --- a/contracts/interfaces/IERC1363Spender.sol +++ b/contracts/interfaces/IERC1363Spender.sol @@ -3,28 +3,4 @@ pragma solidity ^0.8.0; -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 value 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 - */ - function onApprovalReceived( - address owner, - uint256 value, - bytes memory data - ) external returns (bytes4); -} +import "../token/ERC1363/IERC1363Spender.sol"; diff --git a/contracts/interfaces/README.adoc b/contracts/interfaces/README.adoc index 5b4cedf9543..67b7faceb9a 100644 --- a/contracts/interfaces/README.adoc +++ b/contracts/interfaces/README.adoc @@ -22,6 +22,8 @@ are useful to interact with third party contracts that implement them. - {IERC1155MetadataURI} - {IERC1271} - {IERC1363} +- {IERC1363Receiver} +- {IERC1363Spender} - {IERC1820Implementer} - {IERC1820Registry} - {IERC1822Proxiable} @@ -39,6 +41,8 @@ are useful to interact with third party contracts that implement them. {{IERC1363Receiver}} +{{IERC1363Spender}} + {{IERC1820Implementer}} {{IERC1820Registry}} diff --git a/contracts/mocks/ERC1363Mock.sol b/contracts/mocks/ERC1363Mock.sol new file mode 100644 index 00000000000..f87a64a9728 --- /dev/null +++ b/contracts/mocks/ERC1363Mock.sol @@ -0,0 +1,16 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.0; + +import "../token/ERC1363/ERC1363.sol"; + +contract ERC1363Mock is ERC1363 { + constructor( + string memory name, + string memory symbol, + address initialAccount, + uint256 initialBalance + ) ERC20(name, symbol) { + _mint(initialAccount, initialBalance); + } +} diff --git a/contracts/mocks/ERC1363ReceiverMock.sol b/contracts/mocks/ERC1363ReceiverMock.sol new file mode 100644 index 00000000000..7714a9ba6eb --- /dev/null +++ b/contracts/mocks/ERC1363ReceiverMock.sol @@ -0,0 +1,28 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.0; + +import "../token/ERC1363/IERC1363Receiver.sol"; + +contract ERC1363ReceiverMock is IERC1363Receiver { + bytes4 private _retval; + bool private _reverts; + + event Received(address operator, address sender, uint256 amount, bytes data, uint256 gas); + + constructor(bytes4 retval, bool reverts) { + _retval = retval; + _reverts = reverts; + } + + function onTransferReceived( + address spender, + address sender, + uint256 amount, + bytes memory data + ) public override returns (bytes4) { + require(!_reverts, "ERC1363ReceiverMock: throwing"); + emit Received(spender, sender, amount, data, gasleft()); + return _retval; + } +} diff --git a/contracts/mocks/ERC1363SpenderMock.sol b/contracts/mocks/ERC1363SpenderMock.sol new file mode 100644 index 00000000000..3690d8953a2 --- /dev/null +++ b/contracts/mocks/ERC1363SpenderMock.sol @@ -0,0 +1,27 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.0; + +import "../token/ERC1363/IERC1363Spender.sol"; + +contract ERC1363SpenderMock is IERC1363Spender { + bytes4 private _retval; + bool private _reverts; + + event Approved(address sender, uint256 amount, bytes data, uint256 gas); + + constructor(bytes4 retval, bool reverts) { + _retval = retval; + _reverts = reverts; + } + + function onApprovalReceived( + address sender, + uint256 amount, + bytes memory data + ) public override returns (bytes4) { + require(!_reverts, "ERC1363SpenderMock: throwing"); + emit Approved(sender, amount, data, gasleft()); + return _retval; + } +} diff --git a/contracts/token/ERC1363/ERC1363.sol b/contracts/token/ERC1363/ERC1363.sol new file mode 100644 index 00000000000..39a6264aed9 --- /dev/null +++ b/contracts/token/ERC1363/ERC1363.sol @@ -0,0 +1,154 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.0; + +import "../ERC20/ERC20.sol"; +import "../../utils/Address.sol"; +import "../../utils/introspection/ERC165.sol"; + +import "./IERC1363.sol"; +import "./IERC1363Receiver.sol"; +import "./IERC1363Spender.sol"; + +/** + * @dev Implementation of the {IERC1363} interface. + */ +abstract contract ERC1363 is IERC1363, ERC20, ERC165 { + using Address for address; + + /** + * @dev See {IERC165-supportsInterface}. + */ + function supportsInterface(bytes4 interfaceId) public view virtual override(ERC165, IERC165) returns (bool) { + return interfaceId == type(IERC1363).interfaceId || super.supportsInterface(interfaceId); + } + + /** + * @dev See {IERC1363-transferAndCall}. + */ + function transferAndCall(address to, uint256 amount) public virtual override returns (bool) { + return transferAndCall(to, amount, ""); + } + + /** + * @dev See {IERC1363-transferAndCall}. + */ + function transferAndCall( + address to, + uint256 amount, + bytes memory data + ) public virtual override returns (bool) { + transfer(to, amount); + require(_checkOnTransferReceived(_msgSender(), to, amount, data), "ERC1363: receiver returned wrong data"); + return true; + } + + /** + * @dev See {IERC1363-transferFromAndCall}. + */ + function transferFromAndCall( + address from, + address to, + uint256 amount + ) public virtual override returns (bool) { + return transferFromAndCall(from, to, amount, ""); + } + + /** + * @dev See {IERC1363-transferFromAndCall}. + */ + function transferFromAndCall( + address from, + address to, + uint256 amount, + bytes memory data + ) public virtual override returns (bool) { + transferFrom(from, to, amount); + require(_checkOnTransferReceived(from, to, amount, data), "ERC1363: receiver returned wrong data"); + return true; + } + + /** + * @dev See {IERC1363-approveAndCall}. + */ + function approveAndCall(address spender, uint256 amount) public virtual override returns (bool) { + return approveAndCall(spender, amount, ""); + } + + /** + * @dev See {IERC1363-approveAndCall}. + */ + function approveAndCall( + address spender, + uint256 amount, + bytes memory data + ) public virtual override returns (bool) { + approve(spender, amount); + require(_checkOnApprovalReceived(spender, amount, data), "ERC1363: spender returned wrong data"); + return true; + } + + /** + * @dev Internal function to invoke {IERC1363Receiver-onTransferReceived} on a target address + * The call is not executed if the target address is not a contract + * @param sender address Representing the previous owner of the given token amount + * @param recipient address Target address that will receive the tokens + * @param amount uint256 The amount mount of tokens to be transferred + * @param data bytes Optional data to send along with the call + * @return whether the call correctly returned the expected magic value + */ + function _checkOnTransferReceived( + address sender, + address recipient, + uint256 amount, + bytes memory data + ) internal virtual returns (bool) { + if (!recipient.isContract()) { + revert("ERC1363: transfer to non contract address"); + } + + try IERC1363Receiver(recipient).onTransferReceived(_msgSender(), sender, amount, data) returns (bytes4 retval) { + return retval == IERC1363Receiver.onTransferReceived.selector; + } catch (bytes memory reason) { + if (reason.length == 0) { + revert("ERC1363: transfer to non ERC1363Receiver implementer"); + } else { + /// @solidity memory-safe-assembly + assembly { + revert(add(32, reason), mload(reason)) + } + } + } + } + + /** + * @dev Internal function to invoke {IERC1363Receiver-onApprovalReceived} on a target address + * The call is not executed if the target address is not a contract + * @param spender address The address which will spend the funds + * @param amount uint256 The amount of tokens to be spent + * @param data bytes Optional data to send along with the call + * @return whether the call correctly returned the expected magic value + */ + function _checkOnApprovalReceived( + address spender, + uint256 amount, + bytes memory data + ) internal virtual returns (bool) { + if (!spender.isContract()) { + revert("ERC1363: approve a non contract address"); + } + + try IERC1363Spender(spender).onApprovalReceived(_msgSender(), amount, data) returns (bytes4 retval) { + return retval == IERC1363Spender.onApprovalReceived.selector; + } catch (bytes memory reason) { + if (reason.length == 0) { + revert("ERC1363: approve a non ERC1363Spender implementer"); + } else { + /// @solidity memory-safe-assembly + assembly { + revert(add(32, reason), mload(reason)) + } + } + } + } +} diff --git a/contracts/token/ERC1363/IERC1363.sol b/contracts/token/ERC1363/IERC1363.sol new file mode 100644 index 00000000000..0904202b784 --- /dev/null +++ b/contracts/token/ERC1363/IERC1363.sol @@ -0,0 +1,90 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts v4.4.1 (token/ERC1363/IERC1363.sol) + +pragma solidity ^0.8.0; + +import "../ERC20/IERC20.sol"; +import "../../utils/introspection/IERC165.sol"; + +interface IERC1363 is IERC165, IERC20 { + /* + * Note: the ERC-165 identifier for this interface is 0xb0202a11. + * 0xb0202a11 === + * bytes4(keccak256('transferAndCall(address,uint256)')) ^ + * bytes4(keccak256('transferAndCall(address,uint256,bytes)')) ^ + * bytes4(keccak256('transferFromAndCall(address,address,uint256)')) ^ + * bytes4(keccak256('transferFromAndCall(address,address,uint256,bytes)')) ^ + * bytes4(keccak256('approveAndCall(address,uint256)')) ^ + * bytes4(keccak256('approveAndCall(address,uint256,bytes)')) + */ + + /** + * @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 + */ + function transferAndCall(address to, uint256 amount) 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 + */ + function transferAndCall( + address to, + uint256 amount, + bytes memory 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 + */ + function transferFromAndCall( + address from, + address to, + uint256 amount + ) 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 + */ + function transferFromAndCall( + address from, + address to, + uint256 amount, + bytes memory 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 + */ + function approveAndCall(address spender, uint256 amount) 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` + */ + function approveAndCall( + address spender, + uint256 amount, + bytes memory data + ) external returns (bool); +} diff --git a/contracts/token/ERC1363/IERC1363Receiver.sol b/contracts/token/ERC1363/IERC1363Receiver.sol new file mode 100644 index 00000000000..657fccbde99 --- /dev/null +++ b/contracts/token/ERC1363/IERC1363Receiver.sol @@ -0,0 +1,32 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts v4.4.1 (token/ERC136/IERC1363Receiver.sol) + +pragma solidity ^0.8.0; + +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 spender 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 + */ + function onTransferReceived( + address spender, + address from, + uint256 amount, + bytes memory data + ) external returns (bytes4); +} diff --git a/contracts/token/ERC1363/IERC1363Spender.sol b/contracts/token/ERC1363/IERC1363Spender.sol new file mode 100644 index 00000000000..2e05410d759 --- /dev/null +++ b/contracts/token/ERC1363/IERC1363Spender.sol @@ -0,0 +1,30 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts v4.4.1 (token/ERC136/IERC1363Spender.sol) + +pragma solidity ^0.8.0; + +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 sender 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 + */ + function onApprovalReceived( + address sender, + uint256 amount, + bytes memory data + ) external returns (bytes4); +} diff --git a/contracts/token/ERC1363/README.adoc b/contracts/token/ERC1363/README.adoc new file mode 100644 index 00000000000..51d2d2ce55d --- /dev/null +++ b/contracts/token/ERC1363/README.adoc @@ -0,0 +1,37 @@ += ERC 1363 + +[.readme-notice] +NOTE: This document is better viewed at https://docs.openzeppelin.com/contracts/api/token/erc1363 + +This set of interfaces, contracts, and utilities are all related to the https://eips.ethereum.org/EIPS/eip-1363[ERC1363 Payable Token]. + +The EIP specifies three interfaces: + +* {IERC1363}: Core functionality required in all compliant implementation. +* {IERC1363Receiver}: An interface that must be implemented by contracts if they want to accept tokens through `transferAndCall` or `transferFromAndCall`. +* {IERC1363Spender}: An interface that must be implemented by contracts if they want to accept tokens through `approveAndCall`. + +OpenZeppelin Contracts provides implementations of the following contracts: + +* {ERC1363} implements the mandatory {IERC1363} interface. +* {ERC1363Holder} is a bare bones implementation of the receiver and spender interfaces. + +== Core + +{{IERC1363}} + +{{IERC1363Receiver}} + +{{IERC1363Spender}} + +{{ERC1363}} + +== Presets + +These contracts are preconfigured combinations of features. They can be used through inheritance or as models to copy and paste their source code. + +{{ERC1363PresetFixedSupply}} + +== Utilities + +{{ERC1363Holder}} diff --git a/contracts/token/ERC1363/presets/ERC1363PresetFixedSupply.sol b/contracts/token/ERC1363/presets/ERC1363PresetFixedSupply.sol new file mode 100644 index 00000000000..cbe7f46e8b2 --- /dev/null +++ b/contracts/token/ERC1363/presets/ERC1363PresetFixedSupply.sol @@ -0,0 +1,25 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.0; + +import "../ERC1363.sol"; + +/** + * @dev {ERC1363} token, including: + * + * - Preminted initial supply + * + */ +contract ERC1363PresetFixedSupply is ERC1363 { + /** + * @dev Mints `initialSupply` amount of token and transfers them to `owner`. + */ + constructor( + string memory name, + string memory symbol, + uint256 initialSupply, + address owner + ) ERC20(name, symbol) { + _mint(owner, initialSupply); + } +} diff --git a/contracts/token/ERC1363/utils/ERC1363Holder.sol b/contracts/token/ERC1363/utils/ERC1363Holder.sol new file mode 100644 index 00000000000..ae4a3ad1aeb --- /dev/null +++ b/contracts/token/ERC1363/utils/ERC1363Holder.sol @@ -0,0 +1,42 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.0; + +import "../IERC1363Receiver.sol"; +import "../IERC1363Spender.sol"; + +/** + * @dev Simple implementation of the {IERC1363Receiver} and {IERC1363Spender} interfaces that will allow a contract + * to hold ERC1363 tokens. + * + * IMPORTANT: When inheriting this contract, you must include a way to use the received tokens, otherwise they will be + * stuck. + */ +contract ERC1363Holder is IERC1363Receiver, IERC1363Spender { + /* + * @dev See {IERC1363Receiver-onTransferReceived}. + * + * Always returns `IERC1363Receiver.onTransferReceived.selector`. + */ + function onTransferReceived( + address, + address, + uint256, + bytes memory + ) external virtual override returns (bytes4) { + return IERC1363Receiver.onTransferReceived.selector; + } + + /* + * @dev See {IERC1363Spender-onApprovalReceived}. + * + * Always returns `IERC1363Spender.onApprovalReceived.selector`. + */ + function onApprovalReceived( + address, + uint256, + bytes memory + ) external virtual override returns (bytes4) { + return IERC1363Spender.onApprovalReceived.selector; + } +} diff --git a/docs/modules/ROOT/nav.adoc b/docs/modules/ROOT/nav.adoc index 6604c2de58f..2eb8a96c5c1 100644 --- a/docs/modules/ROOT/nav.adoc +++ b/docs/modules/ROOT/nav.adoc @@ -13,6 +13,7 @@ ** xref:erc721.adoc[ERC721] ** xref:erc777.adoc[ERC777] ** xref:erc1155.adoc[ERC1155] +** xref:erc1363.adoc[ERC1363] * xref:governance.adoc[Governance] diff --git a/docs/modules/ROOT/pages/erc1363.adoc b/docs/modules/ROOT/pages/erc1363.adoc new file mode 100644 index 00000000000..c47f654f682 --- /dev/null +++ b/docs/modules/ROOT/pages/erc1363.adoc @@ -0,0 +1,57 @@ += ERC1363 + +ERC1363 is an xref:erc20.adoc[ERC20] compatible token that can perform a callback on a receiver or spender contract, after a transfer or an approval, in a single transaction. + +== Why use ERC1363? + +There is no way to execute any code on a receiver or spender contract after an xref:erc20.adoc[ERC20] `transfer`, `transferFrom` or `approve` so, to make an action, it is required to send another transaction. +This introduces complexity on UI development and friction on adoption because users must wait for the first transaction to be executed and then send the second one. They must also pay GAS twice. + +ERC1363 makes xref:erc20.adoc[ERC20] tokens capable of performing actions more easily and working without the use of any other listener. It allows to make a callback on a receiver or spender contract, after a transfer or an approval, in a single transaction. + +There are many proposed uses of Ethereum smart contracts that can accept EIP-20 callbacks. + +Examples could be: + +* to create a token payable crowdsale +* selling services for tokens +* paying invoices +* making subscriptions + +For these reasons it was originally named "Payable Token". + +Anyway you can use it for specific utilities or for any other purposes who require the execution of a callback after a transfer or approval received. + +ERC1363 adds the following methods: + +* xref:api:token/ERC1363.adoc#IERC1363-transferAndCall-address-uint256-[`transferAndCall`] and xref:api:token/ERC1363.adoc#IERC1363-transferFromAndCall-address-address-uint256-[`transferFromAndCall`] will call xref:api:token/ERC1363.adoc#IERC1363Receiver-onTransferReceived-address-address-uint256-bytes-[`onTransferReceived`] on a xref:api:token/ERC1363.adoc#IERC1363Receiver[`IERC1363Receiver`] contract. + +* xref:api:token/ERC1363.adoc#IERC1363-approveAndCall-address-uint256-[`approveAndCall`] will call xref:api:token/ERC1363.adoc#IERC1363Spender-onApprovalReceived-address-uint256-bytes-[`onApprovalReceived`] on a xref:api:token/ERC1363.adoc#IERC1363Spender[`IERC1363Spender`] contract. + +For more information about the `ERC1363` standard, check out the https://eips.ethereum.org/EIPS/eip-1363[EIP-1363 specification]. + +== Constructing an ERC1363 Token Contract + +We will replicate the `GLD` example of the xref:erc20.adoc#constructing-an-erc20-token-contract[ERC20 guide], this time using ERC1363. As always, check out the xref:api:token/ERC1363.adoc#IERC1363[`API reference`] to learn more about the details of each function. + +[source,solidity] +---- +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import "@openzeppelin/contracts/token/ERC1363/ERC1363.sol"; + +contract GLDToken is ERC1363 { + constructor(uint256 initialSupply) ERC20("Gold", "GLD") { + _mint(msg.sender, initialSupply); + } +} +---- + +We’re reusing xref:erc20.adoc[ERC20] for both the basic standard implementation and the `name` and `symbol`. Additionally, we’re creating an `initialSupply` of tokens, which will be assigned to the address that deploys the contract. + +[[Presets]] +== Preset ERC1363 contract +A preset ERC1363 is available, xref:api:token/ERC1363.adoc#ERC1363PresetFixedSupply[`ERC1363PresetFixedSupply`]. It is preset to create an ERC1363, mint the `initialSupply` amount of tokens and transfer them to `owner`. + +This contract is ready to deploy without having to write any Solidity code. It can be used as-is for quick prototyping and testing, but is also suitable for production environments. diff --git a/docs/modules/ROOT/pages/tokens.adoc b/docs/modules/ROOT/pages/tokens.adoc index b168756df42..e2049078ade 100644 --- a/docs/modules/ROOT/pages/tokens.adoc +++ b/docs/modules/ROOT/pages/tokens.adoc @@ -30,3 +30,4 @@ You've probably heard of the ERC20 or ERC721 token standards, and that's why you * xref:erc721.adoc[ERC721]: the de-facto solution for non-fungible tokens, often used for collectibles and games. * xref:erc777.adoc[ERC777]: a richer standard for fungible tokens, enabling new use cases and building on past learnings. Backwards compatible with ERC20. * xref:erc1155.adoc[ERC1155]: a novel standard for multi-tokens, allowing for a single contract to represent multiple fungible and non-fungible tokens, along with batched operations for increased gas efficiency. +* xref:erc1363.adoc[ERC1363]: ERC20 compatible tokens that can perform a callback on a receiver or spender contract, after a transfer or an approval, in a single transaction. diff --git a/scripts/checks/inheritance-ordering.js b/scripts/checks/inheritance-ordering.js index 3ade7409a7a..84366c40249 100755 --- a/scripts/checks/inheritance-ordering.js +++ b/scripts/checks/inheritance-ordering.js @@ -13,7 +13,7 @@ for (const artifact of artifacts) { const linearized = []; for (const source in solcOutput.contracts) { - if (source.includes('/mocks/')) { + if ([ '/mocks/', '/presets/' ].some(skip => source.includes(skip))) { continue; } diff --git a/test/token/ERC1363/ERC1363.behavior.js b/test/token/ERC1363/ERC1363.behavior.js new file mode 100644 index 00000000000..4a932724e2b --- /dev/null +++ b/test/token/ERC1363/ERC1363.behavior.js @@ -0,0 +1,466 @@ +const { BN, expectRevert, expectEvent } = require('@openzeppelin/test-helpers'); +const { expect } = require('chai'); +const { shouldSupportInterfaces } = require('../../utils/introspection/SupportsInterface.behavior'); + +const ERC1363Receiver = artifacts.require('ERC1363ReceiverMock'); +const ERC1363Spender = artifacts.require('ERC1363SpenderMock'); + +function shouldBehaveLikeERC1363 ([owner, spender, recipient], balance) { + const value = balance; + const data = '0x42'; + + const RECEIVER_MAGIC_VALUE = '0x88a7ca5c'; + const SPENDER_MAGIC_VALUE = '0x7b04a2d0'; + + shouldSupportInterfaces([ + 'ERC165', + 'ERC1363', + ]); + + describe('via transferFromAndCall', function () { + beforeEach(async function () { + await this.token.approve(spender, value, { 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 (transferFun, data) { + describe('to a valid receiver contract', function () { + beforeEach(async function () { + this.receiver = await ERC1363Receiver.new(RECEIVER_MAGIC_VALUE, false); + this.to = this.receiver.address; + }); + + it('should call onTransferReceived', async function () { + const receipt = await transferFun.call(this, owner, this.to, value, { from: spender }); + + await expectEvent.inTransaction(receipt.tx, ERC1363Receiver, 'Received', { + operator: spender, + sender: owner, + amount: value, + data: data, + }); + }); + }); + }; + + const transferFromWasSuccessful = function (sender, spender, balance) { + let receiver; + + beforeEach(async function () { + const receiverContract = await ERC1363Receiver.new(RECEIVER_MAGIC_VALUE, false); + receiver = receiverContract.address; + }); + + describe('when the sender does not have enough balance', function () { + const amount = balance + 1; + + describe('with data', function () { + it('reverts', async function () { + await expectRevert( + transferFromAndCallWithData.call(this, sender, receiver, amount, { from: spender }), + 'ERC20: insufficient allowance', + ); + }); + }); + + describe('without data', function () { + it('reverts', async function () { + await expectRevert( + transferFromAndCallWithoutData.call(this, sender, receiver, amount, { from: spender }), + 'ERC20: insufficient allowance', + ); + }); + }); + }); + + describe('when the sender has enough balance', function () { + const amount = balance; + describe('with data', function () { + it('transfers the requested amount', async function () { + await transferFromAndCallWithData.call(this, sender, receiver, amount, { from: spender }); + + expect(await this.token.balanceOf(sender)).to.be.bignumber.equal(new BN(0)); + + expect(await this.token.balanceOf(receiver)).to.be.bignumber.equal(amount); + }); + + it('emits a transfer event', async function () { + const { logs } = await transferFromAndCallWithData.call(this, sender, receiver, amount, { from: spender }); + + expectEvent.inLogs(logs, 'Transfer', { + from: sender, + to: receiver, + value: amount, + }); + }); + }); + + describe('without data', function () { + it('transfers the requested amount', async function () { + await transferFromAndCallWithoutData.call(this, sender, receiver, amount, { from: spender }); + + expect(await this.token.balanceOf(sender)).to.be.bignumber.equal(new BN(0)); + + expect(await this.token.balanceOf(receiver)).to.be.bignumber.equal(amount); + }); + + it('emits a transfer event', async function () { + const { logs } = await transferFromAndCallWithoutData.call( + this, sender, receiver, amount, { from: spender }, + ); + + expectEvent.inLogs(logs, 'Transfer', { + from: sender, + to: receiver, + value: amount, + }); + }); + }); + }); + }; + + describe('with data', function () { + shouldTransferFromSafely(transferFromAndCallWithData, data); + }); + + describe('without data', function () { + shouldTransferFromSafely(transferFromAndCallWithoutData, null); + }); + + describe('testing ERC20 behaviours', function () { + transferFromWasSuccessful(owner, spender, value); + }); + + describe('to a receiver that is not a contract', function () { + it('reverts', async function () { + await expectRevert( + transferFromAndCallWithoutData.call(this, owner, recipient, value, { from: spender }), + 'ERC1363: transfer to non contract address', + ); + }); + }); + + describe('to a receiver contract returning unexpected value', function () { + it('reverts', async function () { + const invalidReceiver = await ERC1363Receiver.new(data, false); + await expectRevert( + transferFromAndCallWithoutData.call(this, owner, invalidReceiver.address, value, { from: spender }), + 'ERC1363: receiver returned wrong data', + ); + }); + }); + + describe('to a receiver contract that throws', function () { + it('reverts', async function () { + const invalidReceiver = await ERC1363Receiver.new(RECEIVER_MAGIC_VALUE, true); + await expectRevert( + transferFromAndCallWithoutData.call(this, owner, invalidReceiver.address, value, { from: spender }), + 'ERC1363ReceiverMock: throwing', + ); + }); + }); + + describe('to a contract that does not implement the required function', function () { + it('reverts', async function () { + const invalidReceiver = this.token; + await expectRevert( + transferFromAndCallWithoutData.call(this, owner, invalidReceiver.address, value, { from: spender }), + 'ERC1363: transfer to non ERC1363Receiver implementer', + ); + }); + }); + }); + + 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 (transferFun, data) { + describe('to a valid receiver contract', function () { + beforeEach(async function () { + this.receiver = await ERC1363Receiver.new(RECEIVER_MAGIC_VALUE, false); + this.to = this.receiver.address; + }); + + it('should call onTransferReceived', async function () { + const receipt = await transferFun.call(this, this.to, value, { from: owner }); + + await expectEvent.inTransaction(receipt.tx, ERC1363Receiver, 'Received', { + operator: owner, + sender: owner, + amount: value, + data: data, + }); + }); + }); + }; + + const transferWasSuccessful = function (sender, balance) { + let receiver; + + beforeEach(async function () { + const receiverContract = await ERC1363Receiver.new(RECEIVER_MAGIC_VALUE, false); + receiver = receiverContract.address; + }); + + describe('when the sender does not have enough balance', function () { + const amount = balance + 1; + + describe('with data', function () { + it('reverts', async function () { + await expectRevert( + transferAndCallWithData.call(this, receiver, amount, { from: sender }), + 'ERC20: transfer amount exceeds balance', + ); + }); + }); + + describe('without data', function () { + it('reverts', async function () { + await expectRevert( + transferAndCallWithoutData.call(this, receiver, amount, { from: sender }), + 'ERC20: transfer amount exceeds balance', + ); + }); + }); + }); + + describe('when the sender has enough balance', function () { + const amount = balance; + describe('with data', function () { + it('transfers the requested amount', async function () { + await transferAndCallWithData.call(this, receiver, amount, { from: sender }); + + expect(await this.token.balanceOf(sender)).to.be.bignumber.equal(new BN(0)); + + expect(await this.token.balanceOf(receiver)).to.be.bignumber.equal(amount); + }); + + it('emits a transfer event', async function () { + const { logs } = await transferAndCallWithData.call(this, receiver, amount, { from: sender }); + + expectEvent.inLogs(logs, 'Transfer', { + from: sender, + to: receiver, + value: amount, + }); + }); + }); + + describe('without data', function () { + it('transfers the requested amount', async function () { + await transferAndCallWithoutData.call(this, receiver, amount, { from: sender }); + + expect(await this.token.balanceOf(sender)).to.be.bignumber.equal(new BN(0)); + + expect(await this.token.balanceOf(receiver)).to.be.bignumber.equal(amount); + }); + + it('emits a transfer event', async function () { + const { logs } = await transferAndCallWithoutData.call(this, receiver, amount, { from: sender }); + + expectEvent.inLogs(logs, 'Transfer', { + from: sender, + to: receiver, + value: amount, + }); + }); + }); + }); + }; + + describe('with data', function () { + shouldTransferSafely(transferAndCallWithData, data); + }); + + describe('without data', function () { + shouldTransferSafely(transferAndCallWithoutData, null); + }); + + describe('testing ERC20 behaviours', function () { + transferWasSuccessful(owner, value); + }); + + describe('to a receiver that is not a contract', function () { + it('reverts', async function () { + await expectRevert( + transferAndCallWithoutData.call(this, recipient, value, { from: owner }), + 'ERC1363: transfer to non contract address', + ); + }); + }); + + describe('to a receiver contract returning unexpected value', function () { + it('reverts', async function () { + const invalidReceiver = await ERC1363Receiver.new(data, false); + await expectRevert( + transferAndCallWithoutData.call(this, invalidReceiver.address, value, { from: owner }), + 'ERC1363: receiver returned wrong data', + ); + }); + }); + + describe('to a receiver contract that throws', function () { + it('reverts', async function () { + const invalidReceiver = await ERC1363Receiver.new(RECEIVER_MAGIC_VALUE, true); + await expectRevert( + transferAndCallWithoutData.call(this, invalidReceiver.address, value, { from: owner }), + 'ERC1363ReceiverMock: throwing', + ); + }); + }); + + describe('to a contract that does not implement the required function', function () { + it('reverts', async function () { + const invalidReceiver = this.token; + await expectRevert( + transferAndCallWithoutData.call(this, invalidReceiver.address, value, { from: owner }), + 'ERC1363: transfer to non ERC1363Receiver implementer', + ); + }); + }); + }); + + 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 (approveFun, data) { + describe('to a valid receiver contract', function () { + beforeEach(async function () { + this.spender = await ERC1363Spender.new(SPENDER_MAGIC_VALUE, false); + this.to = this.spender.address; + }); + + it('should call onApprovalReceived', async function () { + const receipt = await approveFun.call(this, this.to, value, { from: owner }); + + await expectEvent.inTransaction(receipt.tx, ERC1363Spender, 'Approved', { + sender: owner, + amount: value, + data: data, + }); + }); + }); + }; + + const approveWasSuccessful = function (sender, amount) { + let spender; + + beforeEach(async function () { + const spenderContract = await ERC1363Spender.new(SPENDER_MAGIC_VALUE, false); + spender = spenderContract.address; + }); + + describe('with data', function () { + it('approves the requested amount', async function () { + await approveAndCallWithData.call(this, spender, amount, { from: sender }); + + expect(await this.token.allowance(sender, spender)).to.be.bignumber.equal(amount); + }); + + it('emits an approval event', async function () { + const { logs } = await approveAndCallWithData.call(this, spender, amount, { from: sender }); + + expectEvent.inLogs(logs, 'Approval', { + owner: sender, + spender: spender, + value: amount, + }); + }); + }); + + describe('without data', function () { + it('approves the requested amount', async function () { + await approveAndCallWithoutData.call(this, spender, amount, { from: sender }); + + expect(await this.token.allowance(sender, spender)).to.be.bignumber.equal(amount); + }); + + it('emits an approval event', async function () { + const { logs } = await approveAndCallWithoutData.call(this, spender, amount, { from: sender }); + + expectEvent.inLogs(logs, 'Approval', { + owner: sender, + spender: spender, + value: amount, + }); + }); + }); + }; + + 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 expectRevert( + approveAndCallWithoutData.call(this, recipient, value, { from: owner }), + 'ERC1363: approve a non contract address', + ); + }); + }); + + describe('to a spender contract returning unexpected value', function () { + it('reverts', async function () { + const invalidSpender = await ERC1363Spender.new(data, false); + await expectRevert( + approveAndCallWithoutData.call(this, invalidSpender.address, value, { from: owner }), + 'ERC1363: spender returned wrong data', + ); + }); + }); + + describe('to a spender contract that throws', function () { + it('reverts', async function () { + const invalidSpender = await ERC1363Spender.new(SPENDER_MAGIC_VALUE, true); + await expectRevert( + approveAndCallWithoutData.call(this, invalidSpender.address, value, { from: owner }), + 'ERC1363SpenderMock: throwing', + ); + }); + }); + + describe('to a contract that does not implement the required function', function () { + it('reverts', async function () { + const invalidSpender = this.token; + await expectRevert( + approveAndCallWithoutData.call(this, invalidSpender.address, value, { from: owner }), + 'ERC1363: approve a non ERC1363Spender implementer', + ); + }); + }); + }); +} + +module.exports = { + shouldBehaveLikeERC1363, +}; diff --git a/test/token/ERC1363/ERC1363.test.js b/test/token/ERC1363/ERC1363.test.js new file mode 100644 index 00000000000..8affd14d8f8 --- /dev/null +++ b/test/token/ERC1363/ERC1363.test.js @@ -0,0 +1,18 @@ +const { BN } = require('@openzeppelin/test-helpers'); + +const { shouldBehaveLikeERC1363 } = require('./ERC1363.behavior'); + +const ERC1363 = artifacts.require('ERC1363Mock'); + +contract('ERC1363', function ([owner, spender, recipient]) { + const name = 'ERC1363 TEST'; + const symbol = '1363T'; + + const balance = new BN(100); + + beforeEach(async function () { + this.token = await ERC1363.new(name, symbol, owner, balance); + }); + + shouldBehaveLikeERC1363([owner, spender, recipient], balance); +}); diff --git a/test/token/ERC1363/presets/ERC1363PresetFixedSupply.test.js b/test/token/ERC1363/presets/ERC1363PresetFixedSupply.test.js new file mode 100644 index 00000000000..a7bdb15b600 --- /dev/null +++ b/test/token/ERC1363/presets/ERC1363PresetFixedSupply.test.js @@ -0,0 +1,34 @@ +const { BN } = require('@openzeppelin/test-helpers'); + +const { expect } = require('chai'); + +const ERC1363PresetFixedSupply = artifacts.require('ERC1363PresetFixedSupply'); + +contract('ERC1363PresetFixedSupply', function (accounts) { + const [deployer, owner] = accounts; + + const name = 'ERC1363 Preset'; + const symbol = '1363P'; + + const initialSupply = new BN('50000'); + + before(async function () { + this.token = await ERC1363PresetFixedSupply.new(name, symbol, initialSupply, owner, { from: deployer }); + }); + + it('returns the name', async function () { + expect(await this.token.name()).to.equal(name); + }); + + it('returns the symbol', async function () { + expect(await this.token.symbol()).to.equal(symbol); + }); + + it('deployer has the balance equal to initial supply', async function () { + expect(await this.token.balanceOf(owner)).to.be.bignumber.equal(initialSupply); + }); + + it('total supply is equal to initial supply', async function () { + expect(await this.token.totalSupply()).to.be.bignumber.equal(initialSupply); + }); +}); diff --git a/test/token/ERC1363/utils/ERC1363Holder.test.js b/test/token/ERC1363/utils/ERC1363Holder.test.js new file mode 100644 index 00000000000..3fdf91adf4f --- /dev/null +++ b/test/token/ERC1363/utils/ERC1363Holder.test.js @@ -0,0 +1,112 @@ +const { BN } = require('@openzeppelin/test-helpers'); + +const { expect } = require('chai'); + +const ERC1363Holder = artifacts.require('ERC1363Holder'); +const ERC1363 = artifacts.require('ERC1363Mock'); + +contract('ERC1363Holder', function (accounts) { + const [ owner, spender ] = accounts; + + const data = '0x42'; + + const name = 'ERC1363 TEST'; + const symbol = '1363T'; + + const balance = new BN(100); + + beforeEach(async function () { + this.token = await ERC1363.new(name, symbol, owner, balance); + this.holder = await ERC1363Holder.new(); + }); + + describe('via transferFromAndCall', function () { + const amount = new BN(1); + + beforeEach(async function () { + await this.token.approve(spender, amount, { 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 (transferFun, data) { + it('receives ERC1363 tokens', async function () { + await transferFun.call(this, owner, this.holder.address, amount, { from: spender }); + + expect(await this.token.balanceOf(this.holder.address)).to.be.bignumber.equal(amount); + }); + }; + + describe('with data', function () { + shouldTransferFromSafely(transferFromAndCallWithData, data); + }); + + describe('without data', function () { + shouldTransferFromSafely(transferFromAndCallWithoutData, null); + }); + }); + + describe('via transferAndCall', function () { + const amount = new BN(1); + + 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 (transferFun, data) { + it('receives ERC1363 tokens', async function () { + await transferFun.call(this, this.holder.address, amount, { from: owner }); + + expect(await this.token.balanceOf(this.holder.address)).to.be.bignumber.equal(amount); + }); + }; + + describe('with data', function () { + shouldTransferSafely(transferAndCallWithData, data); + }); + + describe('without data', function () { + shouldTransferSafely(transferAndCallWithoutData, null); + }); + }); + + describe('via approveAndCall', function () { + const amount = new BN(1); + + 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 (approveFun, data) { + it('has allowance for ERC1363 tokens', async function () { + await approveFun.call(this, this.holder.address, amount, { from: owner }); + + expect(await this.token.allowance(owner, this.holder.address)).to.be.bignumber.equal(amount); + }); + }; + + describe('with data', function () { + shouldApproveSafely(approveAndCallWithData, data); + }); + + describe('without data', function () { + shouldApproveSafely(approveAndCallWithoutData, null); + }); + }); +}); diff --git a/test/utils/introspection/SupportsInterface.behavior.js b/test/utils/introspection/SupportsInterface.behavior.js index 78e32724823..fc5e0815a45 100644 --- a/test/utils/introspection/SupportsInterface.behavior.js +++ b/test/utils/introspection/SupportsInterface.behavior.js @@ -39,6 +39,14 @@ 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)', + ], AccessControl: [ 'hasRole(bytes32,address)', 'getRoleAdmin(bytes32)',