Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ERC1363 #3017

Closed
wants to merge 18 commits into from
13 changes: 4 additions & 9 deletions contracts/interfaces/IERC1363.sol
Original file line number Diff line number Diff line change
Expand Up @@ -6,19 +6,14 @@ pragma solidity ^0.8.0;
import "./IERC20.sol";
import "./IERC165.sol";

interface IERC1363 is IERC165, IERC20 {
interface IERC1363 is IERC20, IERC165 {
/*
* Note: the ERC-165 identifier for this interface is 0x4bbee2df.
* 0x4bbee2df ===
* 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)'))
*/

/*
* Note: the ERC-165 identifier for this interface is 0xfb9ec8ce.
* 0xfb9ec8ce ===
* bytes4(keccak256('transferFromAndCall(address,address,uint256,bytes)')) ^
* bytes4(keccak256('approveAndCall(address,uint256)')) ^
* bytes4(keccak256('approveAndCall(address,uint256,bytes)'))
*/
Expand Down
58 changes: 58 additions & 0 deletions contracts/mocks/ERC1363Mock.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
// SPDX-License-Identifier: MIT

pragma solidity ^0.8.0;

import "../token/ERC20/extensions/ERC1363.sol";

contract ERC1363Mock is ERC1363 {
constructor(
string memory name,
string memory symbol,
address initialAccount,
uint256 initialBalance
) payable ERC20(name, symbol) {
_mint(initialAccount, initialBalance);
}

function mint(address account, uint256 amount) public {
_mint(account, amount);
}

function burn(address account, uint256 amount) public {
_burn(account, amount);
}
}

contract ERC1363ReceiverMock is IERC1363Receiver, IERC1363Spender {
event TransferReceived(address operator, address from, uint256 value, bytes data);
event ApprovalReceived(address owner, uint256 value, bytes data);

function onTransferReceived(
address operator,
address from,
uint256 value,
bytes memory data
) external override returns (bytes4) {
if (data.length == 1) {
if (data[0] == 0x00) return bytes4(0);
if (data[0] == 0x01) revert("onTransferReceived revert");
if (data[0] == 0x02) assert(false);
}
emit TransferReceived(operator, from, value, data);
return this.onTransferReceived.selector;
}

function onApprovalReceived(
address owner,
uint256 value,
bytes memory data
) external override returns (bytes4) {
if (data.length == 1) {
if (data[0] == 0x00) return bytes4(0);
if (data[0] == 0x01) revert("onApprovalReceived revert");
if (data[0] == 0x02) assert(false);
}
emit ApprovalReceived(owner, value, data);
return this.onApprovalReceived.selector;
}
}
88 changes: 88 additions & 0 deletions contracts/token/ERC20/extensions/ERC1363.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import "../ERC20.sol";
import "../../../interfaces/IERC1363.sol";
import "../../../interfaces/IERC1363Receiver.sol";
import "../../../interfaces/IERC1363Spender.sol";
import "../../../utils/introspection/ERC165.sol";

abstract contract ERC1363 is IERC1363, ERC20, ERC165 {
function supportsInterface(bytes4 interfaceId) public view virtual override(IERC165, ERC165) returns (bool) {
return interfaceId == type(IERC1363).interfaceId || super.supportsInterface(interfaceId);
Amxx marked this conversation as resolved.
Show resolved Hide resolved
}

function transferAndCall(address to, uint256 value) public override returns (bool) {
return transferAndCall(to, value, bytes(""));
}

function transferAndCall(
address to,
uint256 value,
bytes memory data
) public override returns (bool) {
require(transfer(to, value));
try IERC1363Receiver(to).onTransferReceived(_msgSender(), _msgSender(), value, data) returns (bytes4 selector) {
require(
selector == IERC1363Receiver(to).onTransferReceived.selector,
"ERC1363: onTransferReceived invalid result"
);
} catch Error(string memory reason) {
revert(reason);
} catch {
revert("ERC1363: onTransferReceived reverted without reason");
}
return true;
}

function transferFromAndCall(
address from,
address to,
uint256 value
) public override returns (bool) {
return transferFromAndCall(from, to, value, bytes(""));
}

function transferFromAndCall(
address from,
address to,
uint256 value,
bytes memory data
) public override returns (bool) {
require(transferFrom(from, to, value));
try IERC1363Receiver(to).onTransferReceived(_msgSender(), from, value, data) returns (bytes4 selector) {
require(
selector == IERC1363Receiver(to).onTransferReceived.selector,
"ERC1363: onTransferReceived invalid result"
);
} catch Error(string memory reason) {
revert(reason);
} catch {
revert("ERC1363: onTransferReceived reverted without reason");
}
return true;
}

function approveAndCall(address spender, uint256 value) public override returns (bool) {
return approveAndCall(spender, value, bytes(""));
}

function approveAndCall(
address spender,
uint256 value,
bytes memory data
) public override returns (bool) {
require(approve(spender, value));
try IERC1363Spender(spender).onApprovalReceived(_msgSender(), value, data) returns (bytes4 selector) {
require(
selector == IERC1363Spender(spender).onApprovalReceived.selector,
"ERC1363: onApprovalReceived invalid result"
);
} catch Error(string memory reason) {
revert(reason);
} catch {
revert("ERC1363: onApprovalReceived reverted without reason");
}
return true;
}
}
5 changes: 5 additions & 0 deletions scripts/inheritanceOrdering.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,11 @@ for (const artifact of artifacts) {

for (const source in solcOutput.contracts) {
for (const contractDef of findAll('ContractDefinition', solcOutput.sources[source].ast)) {
// Skip mocks and presets
if ([ 'Mock', 'Preset' ].some(e => contractDef.name.includes(e))) {
continue;
}

names[contractDef.id] = contractDef.name;
linearized.push(contractDef.linearizedBaseContracts);

Expand Down
215 changes: 215 additions & 0 deletions test/token/ERC20/extensions/ERC1363.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,215 @@
const { BN, expectEvent, expectRevert } = require('@openzeppelin/test-helpers');
const { shouldSupportInterfaces } = require('../../../utils/introspection/SupportsInterface.behavior');

const ERC1363Mock = artifacts.require('ERC1363Mock');
const ERC1363ReceiverMock = artifacts.require('ERC1363ReceiverMock');

contract('ERC1363', function (accounts) {
const [ holder, operator ] = accounts;

const initialSupply = new BN(100);
const value = new BN(10);

const name = 'My Token';
const symbol = 'MTKN';

beforeEach(async function () {
this.token = await ERC1363Mock.new(name, symbol, holder, initialSupply);
this.receiver = await ERC1363ReceiverMock.new();
});

shouldSupportInterfaces([
'ERC165',
'ERC1363',
]);

describe('transferAndCall', function () {
it('without data', async function () {
this.function = 'transferAndCall(address,uint256)';
this.operator = holder;
});

it('with data', async function () {
this.function = 'transferAndCall(address,uint256,bytes)';
this.data = '0x123456';
this.operator = holder;
});

it('invalid return value', async function () {
this.function = 'transferAndCall(address,uint256,bytes)';
this.data = '0x00';
this.operator = holder;
this.revert = 'ERC1363: onTransferReceived invalid result';
});

it('hook reverts with message', async function () {
this.function = 'transferAndCall(address,uint256,bytes)';
this.data = '0x01';
this.operator = holder;
this.revert = 'onTransferReceived revert';
});

it('hook reverts with error', async function () {
this.function = 'transferAndCall(address,uint256,bytes)';
this.data = '0x02';
this.operator = holder;
this.revert = 'ERC1363: onTransferReceived reverted without reason';
});

afterEach(async function () {
const txPromise = this.token.methods[this.function](...[
this.receiver.address,
value,
this.data,
{ from: this.operator },
].filter(Boolean));

if (this.revert === undefined) {
const { tx } = await txPromise;
await expectEvent.inTransaction(tx, this.token, 'Transfer', {
from: this.from || this.operator,
to: this.receiver.address,
value,
});
await expectEvent.inTransaction(tx, this.receiver, 'TransferReceived', {
operator: this.operator,
from: this.from || this.operator,
value,
data: this.data || null,
});
} else {
await expectRevert(txPromise, this.revert);
}
});
});

describe('transferFromAndCall', function () {
beforeEach(async function () {
await this.token.approve(operator, initialSupply, { from: holder });
});

it('without data', async function () {
this.function = 'transferFromAndCall(address,address,uint256)';
this.from = holder;
this.operator = operator;
});

it('with data', async function () {
this.function = 'transferFromAndCall(address,address,uint256,bytes)';
this.data = '0x123456';
this.from = holder;
this.operator = operator;
});

it('invalid return value', async function () {
this.function = 'transferFromAndCall(address,address,uint256,bytes)';
this.data = '0x00';
this.from = holder;
this.operator = operator;
this.revert = 'ERC1363: onTransferReceived invalid result';
});

it('hook reverts with message', async function () {
this.function = 'transferFromAndCall(address,address,uint256,bytes)';
this.data = '0x01';
this.from = holder;
this.operator = operator;
this.revert = 'onTransferReceived revert';
});

it('hook reverts with error', async function () {
this.function = 'transferFromAndCall(address,address,uint256,bytes)';
this.data = '0x02';
this.from = holder;
this.operator = operator;
this.revert = 'ERC1363: onTransferReceived reverted without reason';
});

afterEach(async function () {
const txPromise = this.token.methods[this.function](...[
this.from,
this.receiver.address,
value,
this.data,
{ from: this.operator },
].filter(Boolean));

if (this.revert === undefined) {
const { tx } = await txPromise;
await expectEvent.inTransaction(tx, this.token, 'Transfer', {
from: this.from || this.operator,
to: this.receiver.address,
value,
});
await expectEvent.inTransaction(tx, this.receiver, 'TransferReceived', {
operator: this.operator,
from: this.from || this.operator,
value,
data: this.data || null,
});
} else {
await expectRevert(txPromise, this.revert);
}
});
});

describe('approveAndCall', function () {
it('without data', async function () {
this.function = 'approveAndCall(address,uint256)';
this.owner = holder;
});

it('with data', async function () {
this.function = 'approveAndCall(address,uint256,bytes)';
this.data = '0x123456';
this.owner = holder;
});

it('invalid return value', async function () {
this.function = 'approveAndCall(address,uint256,bytes)';
this.data = '0x00';
this.owner = holder;
this.revert = 'ERC1363: onApprovalReceived invalid result';
});

it('hook reverts with message', async function () {
this.function = 'approveAndCall(address,uint256,bytes)';
this.data = '0x01';
this.owner = holder;
this.revert = 'onApprovalReceived revert';
});

it('hook reverts with error', async function () {
this.function = 'approveAndCall(address,uint256,bytes)';
this.data = '0x02';
this.owner = holder;
this.revert = 'ERC1363: onApprovalReceived reverted without reason';
});

afterEach(async function () {
const txPromise = this.token.methods[this.function](...[
this.receiver.address,
value,
this.data,
{ from: this.owner },
].filter(Boolean));

if (this.revert === undefined) {
const { tx } = await txPromise;

await expectEvent.inTransaction(tx, this.token, 'Approval', {
owner: this.owner,
spender: this.receiver.address,
value,
});
await expectEvent.inTransaction(tx, this.receiver, 'ApprovalReceived', {
owner: this.owner,
value,
data: this.data || null,
});
} else {
await expectRevert(txPromise, this.revert);
}
});
});
});
Loading