diff --git a/contracts/examples/ExampleTokensRecipient.sol b/contracts/examples/ExampleTokensRecipient.sol index c3e8fab..ed4e2e1 100644 --- a/contracts/examples/ExampleTokensRecipient.sol +++ b/contracts/examples/ExampleTokensRecipient.sol @@ -11,17 +11,25 @@ import { ERC820Implementer } from "eip820/contracts/ERC820Implementer.sol"; import { ERC820ImplementerInterface } from "eip820/contracts/ERC820ImplementerInterface.sol"; import { Ownable } from "openzeppelin-solidity/contracts/ownership/Ownable.sol"; import { ERC777TokensRecipient } from "../ERC777TokensRecipient.sol"; +import { ERC777Token } from "../ERC777Token.sol"; contract ExampleTokensRecipient is ERC820Implementer, ERC820ImplementerInterface, ERC777TokensRecipient, Ownable { bool private allowTokensReceived; - bool public notified; + + mapping(address => address) public token; + mapping(address => address) public operator; + mapping(address => address) public from; + mapping(address => address) public to; + mapping(address => uint256) public amount; + mapping(address => bytes) public holderData; + mapping(address => bytes) public operatorData; + mapping(address => uint256) public balanceOf; constructor(bool _setInterface) public { if (_setInterface) { setInterfaceImplementation("ERC777TokensRecipient", this); } allowTokensReceived = true; - notified = false; } function tokensReceived( @@ -35,7 +43,15 @@ contract ExampleTokensRecipient is ERC820Implementer, ERC820ImplementerInterface public { require(allowTokensReceived, "Receive not allowed"); - notified = true; + token[_to] = msg.sender; + operator[_to] = _operator; + from[_to] = _from; + to[_to] = _to; + amount[_to] = _amount; + holderData[_to] = _holderData; + operatorData[_to] = _operatorData; + balanceOf[_from] = ERC777Token(msg.sender).balanceOf(_from); + balanceOf[_to] = ERC777Token(msg.sender).balanceOf(_to); } function acceptTokens() public onlyOwner { allowTokensReceived = true; } diff --git a/contracts/examples/ExampleTokensSender.sol b/contracts/examples/ExampleTokensSender.sol index ce418d0..d4b292e 100644 --- a/contracts/examples/ExampleTokensSender.sol +++ b/contracts/examples/ExampleTokensSender.sol @@ -11,31 +11,47 @@ import { ERC820Implementer } from "eip820/contracts/ERC820Implementer.sol"; import { ERC820ImplementerInterface } from "eip820/contracts/ERC820ImplementerInterface.sol"; import { Ownable } from "openzeppelin-solidity/contracts/ownership/Ownable.sol"; import { ERC777TokensSender } from "../ERC777TokensSender.sol"; +import { ERC777Token } from "../ERC777Token.sol"; contract ExampleTokensSender is ERC820Implementer, ERC820ImplementerInterface, ERC777TokensSender, Ownable { bool private allowTokensToSend; - bool public notified; + + mapping(address => address) public token; + mapping(address => address) public operator; + mapping(address => address) public from; + mapping(address => address) public to; + mapping(address => uint256) public amount; + mapping(address => bytes) public holderData; + mapping(address => bytes) public operatorData; + mapping(address => uint256) public balanceOf; constructor(bool _setInterface) public { if (_setInterface) { setInterfaceImplementation("ERC777TokensSender", this); } allowTokensToSend = true; - notified = false; } function tokensToSend( - address operator, // solhint-disable no-unused-vars - address from, - address to, - uint amount, - bytes holderData, - bytes operatorData - ) // solhint-enable no-unused-vars + address _operator, + address _from, + address _to, + uint _amount, + bytes _holderData, + bytes _operatorData + ) public { require(allowTokensToSend, "Send not allowed"); - notified = true; + token[_to] = msg.sender; + operator[_to] = _operator; + from[_to] = _from; + to[_to] = _to; + amount[_to] = _amount; + holderData[_to] = _holderData; + operatorData[_to] = _operatorData; + balanceOf[_from] = ERC777Token(msg.sender).balanceOf(_from); + balanceOf[_to] = ERC777Token(msg.sender).balanceOf(_to); } function acceptTokensToSend() public onlyOwner { allowTokensToSend = true; } diff --git a/test/ReferenceToken.test.js b/test/ReferenceToken.test.js index a45de02..af4d332 100644 --- a/test/ReferenceToken.test.js +++ b/test/ReferenceToken.test.js @@ -41,8 +41,6 @@ contract('ReferenceToken', function(accounts) { token.burnOperator, ] }); - after(async function() { await web3.currentProvider.connection.close(); }); - beforeEach(async function() { let erc820Registry = await EIP820Registry.deploy(web3, accounts[0]); assert.ok(erc820Registry.$address); @@ -59,13 +57,16 @@ contract('ReferenceToken', function(accounts) { .send({ gas: 300000, from: accounts[0] }); }; - token.genMintTxForAccount = function(account, amount, operator, gas) { - return token.contract.methods - .mint(account, web3.utils.toWei(amount), '0xcafe') - .send.request({ gas: gas, from: operator }); + token.mintForAccount = async function(account, amount, operator) { + const mintTx = token.contract.methods + .mint(account, web3.utils.toWei(amount), '0xcafe'); + const gas = await mintTx.estimateGas(); + await mintTx.send({ gas: gas, from: operator }); }; }); + after(async function() { await web3.currentProvider.connection.close(); }); + describe('Creation', function() { it('should not deploy the token with a granularity of 0', async function() { const estimateGas = await deployContract.estimateGas(); @@ -110,8 +111,7 @@ contract('ReferenceToken', function(accounts) { .getInterfaceImplementer(token.contract.options.address, erc20Hash) .call(); - assert.strictEqual( - erc20Addr, '0x0000000000000000000000000000000000000000'); + assert.strictEqual(erc20Addr, utils.zeroAddress); }); }); diff --git a/test/utils/erc20Disabled.js b/test/utils/erc20Disabled.js index 1d65172..f5c1c87 100644 --- a/test/utils/erc20Disabled.js +++ b/test/utils/erc20Disabled.js @@ -82,8 +82,7 @@ exports.test = function(web3, accounts, token) { .getInterfaceImplementer(token.contract.options.address, erc20Hash) .call(); - assert.strictEqual( - erc20Addr, '0x0000000000000000000000000000000000000000'); + assert.strictEqual(erc20Addr, utils.zeroAddress); await token.contract.methods .enableERC20() diff --git a/test/utils/index.js b/test/utils/index.js index f91e19d..0ff0baa 100644 --- a/test/utils/index.js +++ b/test/utils/index.js @@ -19,9 +19,11 @@ const testAccounts = [ const blocks = []; let blockIdx = 0; -let log = (msg) => process.env.MOCHA_VERBOSE && console.log(msg); +const log = (msg) => process.env.MOCHA_VERBOSE && console.log(msg); +const zeroAddress = '0x0000000000000000000000000000000000000000'; module.exports = { + zeroAddress, log, formatAccount(account) { @@ -56,13 +58,70 @@ module.exports = { ); }, - async mintForAllAccounts(web3, accounts, token, operator, amount, gas) { - const mintBatch = new web3.BatchRequest(); + async mintForAllAccounts(web3, accounts, token, operator, amount) { + let erc820Registry = new web3.eth.Contract( + ERC820Registry.abi, + '0x991a1bcb077599290d7305493c9a630c20f8b798' + ); + let hook; for (let account of accounts) { - mintBatch.add( - token.genMintTxForAccount(account, amount, accounts[0], gas) - ); + hook = await erc820Registry.methods.getInterfaceImplementer( + account, web3.utils.keccak256('ERC777TokensRecipient')).call(); + if (hook === zeroAddress) { hook = '0x0'; } + log(`mint ${amount} for ${account} by ${operator} (hook: ${hook})`); + await token.mintForAccount(account, amount, operator); } - await mintBatch.execute(); + }, + + async assertHookCalled( + web3, + hook, + token, + operator, + from, + to, + holderData, + operatorData, + balance_from, + balance_to + ) { + assert.strictEqual( + await hook.methods.token(to).call(), + web3.utils.toChecksumAddress(token) + ); + assert.strictEqual( + await hook.methods.operator(to).call(), + web3.utils.toChecksumAddress(operator) + ); + assert.strictEqual( + await hook.methods.from(to).call(), + web3.utils.toChecksumAddress(from) + ); + assert.strictEqual( + await hook.methods.to(to).call(), + web3.utils.toChecksumAddress(to) + ); + assert.strictEqual(await hook.methods.holderData(to).call(), holderData); + assert.strictEqual( + await hook.methods.operatorData(to).call(), operatorData); + + assert.equal( + web3.utils.fromWei(await hook.methods.balanceOf(from).call()), + balance_from + ); + + assert.equal( + web3.utils.fromWei(await hook.methods.balanceOf(to).call()), + balance_to + ); + }, + + async assertHookNotCalled(hook, to) { + assert.strictEqual(await hook.methods.token(to).call(), zeroAddress); + assert.strictEqual(await hook.methods.operator(to).call(), zeroAddress); + assert.strictEqual(await hook.methods.from(to).call(), zeroAddress); + assert.strictEqual(await hook.methods.to(to).call(), zeroAddress); + assert.strictEqual(await hook.methods.holderData(to).call(), null); + assert.strictEqual(await hook.methods.operatorData(to).call(), null); }, }; diff --git a/test/utils/send.js b/test/utils/send.js index 7c850e1..1b286d8 100644 --- a/test/utils/send.js +++ b/test/utils/send.js @@ -125,7 +125,7 @@ exports.test = function(web3, accounts, token) { await utils.assertBalance(web3, token, accounts[2], 10); await token.contract.methods - .send('0x0000000000000000000000000000000000000000', + .send(utils.zeroAddress, web3.utils.toWei('1'), '0x') .send({ gas: 300000, from: accounts[1] }) .should.be.rejectedWith('revert'); diff --git a/test/utils/tokensRecipient.js b/test/utils/tokensRecipient.js index 888ec15..a842006 100644 --- a/test/utils/tokensRecipient.js +++ b/test/utils/tokensRecipient.js @@ -6,65 +6,106 @@ const assert = chai.assert; chai.use(require('chai-as-promised')).should(); const utils = require('./index'); const OldExampleTokensRecipient = artifacts.require('ExampleTokensRecipient'); +const empty = '0x'; + +let deployTokensRecipient; +let erc820Registry; exports.test = function(web3, accounts, token) { - const ExampleTokensRecipient = new web3.eth.Contract( - OldExampleTokensRecipient.abi, - { data: OldExampleTokensRecipient.bytecode } - ); - let recipient; describe('TokensRecipient', async function() { + before(function() { + let ExampleTokensRecipient = new web3.eth.Contract( + OldExampleTokensRecipient.abi, + { data: OldExampleTokensRecipient.bytecode } + ); + + erc820Registry = utils.getERC820Registry(web3); + + deployTokensRecipient = async function(setInterface, from) { + const deployRecipient = ExampleTokensRecipient + .deploy({ arguments: [setInterface] }); + const gas = await deployRecipient.estimateGas(); + const recipient = await deployRecipient + .send({ from: from, gas: gas }); + assert.ok(recipient.options.address); + return recipient; + }; + }); + beforeEach(async function() { await utils .mintForAllAccounts(web3, accounts, token, accounts[0], '10', 100000); + }); - recipient = await ExampleTokensRecipient - .deploy({ arguments: [true] }) - .send({ from: accounts[4], gasLimit: 4712388 }); - assert.ok(recipient.options.address); + // truffle clean-room is not able to revert the ERC820Registry + // manually unset any TokensRecipient that may have been set during testing. + afterEach(async function() { + for (let account of accounts) { + await erc820Registry.methods + .setInterfaceImplementer( + account, + web3.utils.keccak256('ERC777TokensRecipient'), + utils.zeroAddress + ).send({ from: account }); + } }); it('should notify the recipient upon receiving tokens', async function() { + const recipient = await deployTokensRecipient(true, accounts[4]); + await utils.assertTotalSupply(web3, token, 10 * accounts.length); await utils.assertBalance(web3, token, accounts[5], 10); await utils.assertBalance(web3, token, recipient.options.address, 0); - assert.isFalse(await recipient.methods.notified().call()); await recipient.methods .acceptTokens() .send({ gas: 300000, from: accounts[4] }); - await token.contract.methods - .send(recipient.options.address, web3.utils.toWei('1.22'), '0x') - .send({ gas: 300000, from: accounts[5] }); + const send = token.contract.methods + .send(recipient.options.address, web3.utils.toWei('1.22'), '0xcafe'); + + const sendGas = await send.estimateGas(); + await send.send({ gas: sendGas, from: accounts[5] }); await utils.getBlock(web3); - assert.isTrue(await recipient.methods.notified().call()); + await utils.assertHookCalled( + web3, + recipient, + token.contract.options.address, + accounts[5], + accounts[5], + recipient.options.address, + '0xcafe', + null, + 8.78, + 1.22 + ); await utils.assertTotalSupply(web3, token, 10 * accounts.length); await utils.assertBalance(web3, token, accounts[5], 8.78); await utils.assertBalance(web3, token, recipient.options.address, 1.22); }); it('should let the recipient reject the tokens', async function() { + const recipient = await deployTokensRecipient(true, accounts[4]); + await utils.assertTotalSupply(web3, token, 10 * accounts.length); await utils.assertBalance(web3, token, accounts[5], 10); await utils.assertBalance(web3, token, recipient.options.address, 0); - assert.isFalse(await recipient.methods.notified().call()); await recipient.methods .rejectTokens() .send({ gas: 300000, from: accounts[4] }); await token.contract.methods - .send(recipient.options.address, web3.utils.toWei('1.22'), '0x') + .send(recipient.options.address, web3.utils.toWei('1.22'), empty) .send({ gas: 300000, from: accounts[5] }) .should.be.rejectedWith('revert'); await utils.getBlock(web3); - // revert will prevent setting notified to true - assert.isFalse(await recipient.methods.notified().call()); + // revert will revert setting data in the hook + utils.assertHookNotCalled(recipient, recipient.options.address); await utils.assertTotalSupply(web3, token, 10 * accounts.length); await utils.assertBalance(web3, token, accounts[5], 10); await utils.assertBalance(web3, token, recipient.options.address, 0); @@ -72,12 +113,8 @@ exports.test = function(web3, accounts, token) { it('should call "TokensRecipient" for ' + `${utils.formatAccount(accounts[4])}`, async function() { - recipient = await ExampleTokensRecipient - .deploy({ arguments: [false] }) - .send({ from: accounts[4], gasLimit: 4712388 }); - assert.ok(recipient.options.address); + const recipient = await deployTokensRecipient(false, accounts[4]); - let erc820Registry = utils.getERC820Registry(web3); await erc820Registry.methods .setInterfaceImplementer( accounts[4], @@ -89,19 +126,29 @@ exports.test = function(web3, accounts, token) { await utils.assertBalance(web3, token, accounts[4], 10); await utils.assertBalance(web3, token, accounts[5], 10); await utils.assertBalance(web3, token, recipient.options.address, 0); - assert.isFalse(await recipient.methods.notified().call()); await recipient.methods .acceptTokens() .send({ gas: 300000, from: accounts[4] }); await token.contract.methods - .send(accounts[4], web3.utils.toWei('1.22'), '0x') + .send(accounts[4], web3.utils.toWei('1.22'), '0xbeef') .send({ gas: 300000, from: accounts[5] }); await utils.getBlock(web3); - assert.isTrue(await recipient.methods.notified().call()); + await utils.assertHookCalled( + web3, + recipient, + token.contract.options.address, + accounts[5], + accounts[5], + accounts[4], + '0xbeef', + null, + 8.78, + 11.22, + ); await utils.assertTotalSupply(web3, token, 10 * accounts.length); await utils.assertBalance(web3, token, accounts[4], 11.22); await utils.assertBalance(web3, token, accounts[5], 8.78); @@ -110,26 +157,21 @@ exports.test = function(web3, accounts, token) { it('should not send tokens to a contract ' + 'without TokensRecipient', async function() { - // must be redeployed without registering with EIP820 for this test - recipient = await ExampleTokensRecipient - .deploy({ arguments: [false] }) - .send({ from: accounts[4], gasLimit: 4712388 }); - assert.ok(recipient.options.address); + const recipient = await deployTokensRecipient(false, accounts[4]); await utils.assertTotalSupply(web3, token, 10 * accounts.length); await utils.assertBalance(web3, token, accounts[5], 10); await utils.assertBalance(web3, token, recipient.options.address, 0); - assert.isFalse(await recipient.methods.notified().call()); await token.contract.methods - .send(recipient.options.address, web3.utils.toWei('1.22'), '0x') + .send(recipient.options.address, web3.utils.toWei('1.22'), empty) .send({ gas: 300000, from: accounts[5] }) .should.be.rejectedWith('revert'); await utils.getBlock(web3); - // revert will prevent setting notified to true - assert.isFalse(await recipient.methods.notified().call()); + // revert will revert setting data in the hook + utils.assertHookNotCalled(recipient, recipient.options.address); await utils.assertTotalSupply(web3, token, 10 * accounts.length); await utils.assertBalance(web3, token, accounts[5], 10); await utils.assertBalance(web3, token, recipient.options.address, 0); diff --git a/test/utils/tokensSender.js b/test/utils/tokensSender.js index 7f071f3..e1f6506 100644 --- a/test/utils/tokensSender.js +++ b/test/utils/tokensSender.js @@ -7,29 +7,54 @@ chai.use(require('chai-as-promised')).should(); const utils = require('./index'); const OldExampleTokensSender = artifacts.require('ExampleTokensSender'); +let deployTokensSender; +let erc820Registry; + exports.test = function(web3, accounts, token) { - const ExampleTokensSender = new web3.eth.Contract( - OldExampleTokensSender.abi, - { data: OldExampleTokensSender.bytecode } - ); - let sender; describe('TokensSender', async function() { + before(async function() { + const ExampleTokensSender = new web3.eth.Contract( + OldExampleTokensSender.abi, + { data: OldExampleTokensSender.bytecode } + ); + + erc820Registry = utils.getERC820Registry(web3); + + deployTokensSender = async function(setInterface, from) { + const deploySender = ExampleTokensSender + .deploy({ arguments: [setInterface] }); + const deployGas = await deploySender.estimateGas(); + const sender = await deploySender + .send({ from: from, gas: deployGas }); + assert.ok(sender.options.address); + + await erc820Registry.methods + .setInterfaceImplementer( + from, + web3.utils.keccak256('ERC777TokensSender'), + sender.options.address + ).send({ from: from }); + + return sender; + }; + }); + beforeEach(async function() { await utils .mintForAllAccounts(web3, accounts, token, accounts[0], '10', 100000); + }); - sender = await ExampleTokensSender - .deploy({ arguments: [false] }) - .send({ from: accounts[4], gasLimit: 4712388 }); - - let erc820Registry = utils.getERC820Registry(web3); - await erc820Registry.methods - .setInterfaceImplementer( - accounts[4], - web3.utils.keccak256('ERC777TokensSender'), - sender.options.address - ).send({ from: accounts[4] }); - assert.ok(sender.options.address); + // truffle clean-room is not able to revert the ERC820Registry + // manually unset any TokensSenders that may have been set during testing. + afterEach(async function() { + for (let account of accounts) { + await erc820Registry.methods + .setInterfaceImplementer( + account, + web3.utils.keccak256('ERC777TokensSender'), + utils.zeroAddress + ).send({ from: account }); + } }); it('should notify the token holder before sending tokens', async function() { @@ -39,7 +64,7 @@ exports.test = function(web3, accounts, token) { await utils.assertBalance(web3, token, accounts[4], 10); await utils.assertBalance(web3, token, accounts[5], 10); await utils.assertBalance(web3, token, sender.options.address, 0); - assert.isFalse(await sender.methods.notified().call()); + utils.assertHookNotCalled(sender, sender.options.address); await sender.methods .acceptTokensToSend() @@ -51,19 +76,31 @@ exports.test = function(web3, accounts, token) { await utils.getBlock(web3); - assert.isTrue(await sender.methods.notified().call()); + await utils.assertHookCalled( + web3, + sender, + token.contract.options.address, + accounts[4], + accounts[4], + accounts[5], + null, + null, + 10, + 10 + ); await utils.assertTotalSupply(web3, token, 10 * accounts.length); await utils.assertBalance(web3, token, accounts[4], 8.78); await utils.assertBalance(web3, token, accounts[5], 11.22); await utils.assertBalance(web3, token, sender.options.address, 0); }); - it('should block the sending tokens for the sender', async function() { + it('should block the sending of tokens for the sender', async function() { + const sender = await deployTokensSender(true, accounts[4]); + await utils.assertTotalSupply(web3, token, 10 * accounts.length); await utils.assertBalance(web3, token, accounts[4], 10); await utils.assertBalance(web3, token, accounts[5], 10); await utils.assertBalance(web3, token, sender.options.address, 0); - assert.isFalse(await sender.methods.notified().call()); await sender.methods .rejectTokensToSend() @@ -76,8 +113,8 @@ exports.test = function(web3, accounts, token) { await utils.getBlock(web3); - // revert will prevent setting notified to true - assert.isFalse(await sender.methods.notified().call()); + // revert will revert setting data in the hook + utils.assertHookNotCalled(sender, sender.options.address); await utils.assertTotalSupply(web3, token, 10 * accounts.length); await utils.assertBalance(web3, token, accounts[4], 10); await utils.assertBalance(web3, token, accounts[5], 10);