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 b6b96ffef88..30d025ff638 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} - {IERC2612} @@ -37,6 +39,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..4982a925ccc --- /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 operator, + address sender, + uint256 amount, + bytes memory data + ) public override returns (bytes4) { + require(!_reverts, "ERC1363ReceiverMock: throwing"); + emit Received(operator, 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..b195834066e --- /dev/null +++ b/contracts/token/ERC1363/ERC1363.sol @@ -0,0 +1,132 @@ +// 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 ERC20, IERC1363, 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: _checkOnTransferReceived reverts"); + 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: _checkOnTransferReceived reverts"); + 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: _checkOnApprovalReceived reverts"); + 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()) { + return false; + } + bytes4 retval = IERC1363Receiver(recipient).onTransferReceived(_msgSender(), sender, amount, data); + return retval == IERC1363Receiver.onTransferReceived.selector; + } + + /** + * @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()) { + return false; + } + bytes4 retval = IERC1363Spender(spender).onApprovalReceived(_msgSender(), amount, data); + return retval == IERC1363Spender.onApprovalReceived.selector; + } +} 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..3e9dbe207cb --- /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 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 + */ + function onTransferReceived( + address operator, + 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..1d3ee885b90 --- /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 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 + */ + function onApprovalReceived( + address owner, + uint256 amount, + bytes memory data + ) external returns (bytes4); +} diff --git a/test/token/ERC1363/ERC1363.behavior.js b/test/token/ERC1363/ERC1363.behavior.js new file mode 100644 index 00000000000..7aafa6f843a --- /dev/null +++ b/test/token/ERC1363/ERC1363.behavior.js @@ -0,0 +1,463 @@ +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: _checkOnTransferReceived reverts', + ); + }); + }); + + 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: _checkOnTransferReceived reverts', + ); + }); + }); + + 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.unspecified( + transferFromAndCallWithoutData.call(this, owner, invalidReceiver.address, value, { from: spender }), + ); + }); + }); + }); + + 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: _checkOnTransferReceived reverts', + ); + }); + }); + + 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: _checkOnTransferReceived reverts', + ); + }); + }); + + 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.unspecified( + transferAndCallWithoutData.call(this, invalidReceiver.address, value, { from: owner }), + ); + }); + }); + }); + + 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: _checkOnApprovalReceived reverts', + ); + }); + }); + + 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: _checkOnApprovalReceived reverts', + ); + }); + }); + + 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.unspecified( + approveAndCallWithoutData.call(this, invalidSpender.address, value, { from: owner }), + ); + }); + }); + }); +} + +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/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)',