123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556 |
- const { BN, constants, expectEvent, expectRevert, singletons } = require('@openzeppelin/test-helpers');
- const { ZERO_ADDRESS } = constants;
- const { expect } = require('chai');
- const {
- shouldBehaveLikeERC777DirectSendBurn,
- shouldBehaveLikeERC777OperatorSendBurn,
- shouldBehaveLikeERC777UnauthorizedOperatorSendBurn,
- shouldBehaveLikeERC777InternalMint,
- shouldBehaveLikeERC777SendBurnMintInternalWithReceiveHook,
- shouldBehaveLikeERC777SendBurnWithSendHook,
- } = require('./ERC777.behavior');
- const { shouldBehaveLikeERC20, shouldBehaveLikeERC20Approve } = require('../ERC20/ERC20.behavior');
- const ERC777 = artifacts.require('$ERC777Mock');
- const ERC777SenderRecipientMock = artifacts.require('$ERC777SenderRecipientMock');
- contract('ERC777', function (accounts) {
- const [registryFunder, holder, defaultOperatorA, defaultOperatorB, newOperator, anyone] = accounts;
- const initialSupply = new BN('10000');
- const name = 'ERC777Test';
- const symbol = '777T';
- const data = web3.utils.sha3('OZ777TestData');
- const operatorData = web3.utils.sha3('OZ777TestOperatorData');
- const defaultOperators = [defaultOperatorA, defaultOperatorB];
- beforeEach(async function () {
- this.erc1820 = await singletons.ERC1820Registry(registryFunder);
- });
- context('with default operators', function () {
- beforeEach(async function () {
- this.token = await ERC777.new(name, symbol, defaultOperators);
- await this.token.$_mint(holder, initialSupply, '0x', '0x');
- });
- describe('as an ERC20 token', function () {
- shouldBehaveLikeERC20('ERC777', initialSupply, holder, anyone, defaultOperatorA);
- describe('_approve', function () {
- shouldBehaveLikeERC20Approve('ERC777', holder, anyone, initialSupply, function (owner, spender, amount) {
- return this.token.$_approve(owner, spender, amount);
- });
- describe('when the owner is the zero address', function () {
- it('reverts', async function () {
- await expectRevert(
- this.token.$_approve(ZERO_ADDRESS, anyone, initialSupply),
- 'ERC777: approve from the zero address',
- );
- });
- });
- });
- });
- it('does not emit AuthorizedOperator events for default operators', async function () {
- await expectEvent.notEmitted.inConstruction(this.token, 'AuthorizedOperator');
- });
- describe('basic information', function () {
- 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('returns a granularity of 1', async function () {
- expect(await this.token.granularity()).to.be.bignumber.equal('1');
- });
- it('returns the default operators', async function () {
- expect(await this.token.defaultOperators()).to.deep.equal(defaultOperators);
- });
- it('default operators are operators for all accounts', async function () {
- for (const operator of defaultOperators) {
- expect(await this.token.isOperatorFor(operator, anyone)).to.equal(true);
- }
- });
- it('returns the total supply', async function () {
- expect(await this.token.totalSupply()).to.be.bignumber.equal(initialSupply);
- });
- it('returns 18 when decimals is called', async function () {
- expect(await this.token.decimals()).to.be.bignumber.equal('18');
- });
- it('the ERC777Token interface is registered in the registry', async function () {
- expect(
- await this.erc1820.getInterfaceImplementer(this.token.address, web3.utils.soliditySha3('ERC777Token')),
- ).to.equal(this.token.address);
- });
- it('the ERC20Token interface is registered in the registry', async function () {
- expect(
- await this.erc1820.getInterfaceImplementer(this.token.address, web3.utils.soliditySha3('ERC20Token')),
- ).to.equal(this.token.address);
- });
- });
- describe('balanceOf', function () {
- context('for an account with no tokens', function () {
- it('returns zero', async function () {
- expect(await this.token.balanceOf(anyone)).to.be.bignumber.equal('0');
- });
- });
- context('for an account with tokens', function () {
- it('returns their balance', async function () {
- expect(await this.token.balanceOf(holder)).to.be.bignumber.equal(initialSupply);
- });
- });
- });
- context('with no ERC777TokensSender and no ERC777TokensRecipient implementers', function () {
- describe('send/burn', function () {
- shouldBehaveLikeERC777DirectSendBurn(holder, anyone, data);
- context('with self operator', function () {
- shouldBehaveLikeERC777OperatorSendBurn(holder, anyone, holder, data, operatorData);
- });
- context('with first default operator', function () {
- shouldBehaveLikeERC777OperatorSendBurn(holder, anyone, defaultOperatorA, data, operatorData);
- });
- context('with second default operator', function () {
- shouldBehaveLikeERC777OperatorSendBurn(holder, anyone, defaultOperatorB, data, operatorData);
- });
- context('before authorizing a new operator', function () {
- shouldBehaveLikeERC777UnauthorizedOperatorSendBurn(holder, anyone, newOperator, data, operatorData);
- });
- context('with new authorized operator', function () {
- beforeEach(async function () {
- await this.token.authorizeOperator(newOperator, { from: holder });
- });
- shouldBehaveLikeERC777OperatorSendBurn(holder, anyone, newOperator, data, operatorData);
- context('with revoked operator', function () {
- beforeEach(async function () {
- await this.token.revokeOperator(newOperator, { from: holder });
- });
- shouldBehaveLikeERC777UnauthorizedOperatorSendBurn(holder, anyone, newOperator, data, operatorData);
- });
- });
- });
- describe('mint (internal)', function () {
- const to = anyone;
- const amount = new BN('5');
- context('with default operator', function () {
- const operator = defaultOperatorA;
- shouldBehaveLikeERC777InternalMint(to, operator, amount, data, operatorData);
- });
- context('with non operator', function () {
- const operator = newOperator;
- shouldBehaveLikeERC777InternalMint(to, operator, amount, data, operatorData);
- });
- });
- describe('mint (internal extended)', function () {
- const amount = new BN('5');
- context('to anyone', function () {
- beforeEach(async function () {
- this.recipient = anyone;
- });
- context('with default operator', function () {
- const operator = defaultOperatorA;
- it('without requireReceptionAck', async function () {
- await this.token.$_mint(this.recipient, amount, data, operatorData, false, { from: operator });
- });
- it('with requireReceptionAck', async function () {
- await this.token.$_mint(this.recipient, amount, data, operatorData, true, { from: operator });
- });
- });
- context('with non operator', function () {
- const operator = newOperator;
- it('without requireReceptionAck', async function () {
- await this.token.$_mint(this.recipient, amount, data, operatorData, false, { from: operator });
- });
- it('with requireReceptionAck', async function () {
- await this.token.$_mint(this.recipient, amount, data, operatorData, true, { from: operator });
- });
- });
- });
- context('to non ERC777TokensRecipient implementer', function () {
- beforeEach(async function () {
- this.tokensRecipientImplementer = await ERC777SenderRecipientMock.new();
- this.recipient = this.tokensRecipientImplementer.address;
- });
- context('with default operator', function () {
- const operator = defaultOperatorA;
- it('without requireReceptionAck', async function () {
- await this.token.$_mint(this.recipient, amount, data, operatorData, false, { from: operator });
- });
- it('with requireReceptionAck', async function () {
- await expectRevert(
- this.token.$_mint(this.recipient, amount, data, operatorData, true, { from: operator }),
- 'ERC777: token recipient contract has no implementer for ERC777TokensRecipient',
- );
- });
- });
- context('with non operator', function () {
- const operator = newOperator;
- it('without requireReceptionAck', async function () {
- await this.token.$_mint(this.recipient, amount, data, operatorData, false, { from: operator });
- });
- it('with requireReceptionAck', async function () {
- await expectRevert(
- this.token.$_mint(this.recipient, amount, data, operatorData, true, { from: operator }),
- 'ERC777: token recipient contract has no implementer for ERC777TokensRecipient',
- );
- });
- });
- });
- });
- });
- describe('operator management', function () {
- it('accounts are their own operator', async function () {
- expect(await this.token.isOperatorFor(holder, holder)).to.equal(true);
- });
- it('reverts when self-authorizing', async function () {
- await expectRevert(
- this.token.authorizeOperator(holder, { from: holder }),
- 'ERC777: authorizing self as operator',
- );
- });
- it('reverts when self-revoking', async function () {
- await expectRevert(this.token.revokeOperator(holder, { from: holder }), 'ERC777: revoking self as operator');
- });
- it('non-operators can be revoked', async function () {
- expect(await this.token.isOperatorFor(newOperator, holder)).to.equal(false);
- const receipt = await this.token.revokeOperator(newOperator, { from: holder });
- expectEvent(receipt, 'RevokedOperator', { operator: newOperator, tokenHolder: holder });
- expect(await this.token.isOperatorFor(newOperator, holder)).to.equal(false);
- });
- it('non-operators can be authorized', async function () {
- expect(await this.token.isOperatorFor(newOperator, holder)).to.equal(false);
- const receipt = await this.token.authorizeOperator(newOperator, { from: holder });
- expectEvent(receipt, 'AuthorizedOperator', { operator: newOperator, tokenHolder: holder });
- expect(await this.token.isOperatorFor(newOperator, holder)).to.equal(true);
- });
- describe('new operators', function () {
- beforeEach(async function () {
- await this.token.authorizeOperator(newOperator, { from: holder });
- });
- it('are not added to the default operators list', async function () {
- expect(await this.token.defaultOperators()).to.deep.equal(defaultOperators);
- });
- it('can be re-authorized', async function () {
- const receipt = await this.token.authorizeOperator(newOperator, { from: holder });
- expectEvent(receipt, 'AuthorizedOperator', { operator: newOperator, tokenHolder: holder });
- expect(await this.token.isOperatorFor(newOperator, holder)).to.equal(true);
- });
- it('can be revoked', async function () {
- const receipt = await this.token.revokeOperator(newOperator, { from: holder });
- expectEvent(receipt, 'RevokedOperator', { operator: newOperator, tokenHolder: holder });
- expect(await this.token.isOperatorFor(newOperator, holder)).to.equal(false);
- });
- });
- describe('default operators', function () {
- it('can be re-authorized', async function () {
- const receipt = await this.token.authorizeOperator(defaultOperatorA, { from: holder });
- expectEvent(receipt, 'AuthorizedOperator', { operator: defaultOperatorA, tokenHolder: holder });
- expect(await this.token.isOperatorFor(defaultOperatorA, holder)).to.equal(true);
- });
- it('can be revoked', async function () {
- const receipt = await this.token.revokeOperator(defaultOperatorA, { from: holder });
- expectEvent(receipt, 'RevokedOperator', { operator: defaultOperatorA, tokenHolder: holder });
- expect(await this.token.isOperatorFor(defaultOperatorA, holder)).to.equal(false);
- });
- it('cannot be revoked for themselves', async function () {
- await expectRevert(
- this.token.revokeOperator(defaultOperatorA, { from: defaultOperatorA }),
- 'ERC777: revoking self as operator',
- );
- });
- context('with revoked default operator', function () {
- beforeEach(async function () {
- await this.token.revokeOperator(defaultOperatorA, { from: holder });
- });
- it('default operator is not revoked for other holders', async function () {
- expect(await this.token.isOperatorFor(defaultOperatorA, anyone)).to.equal(true);
- });
- it('other default operators are not revoked', async function () {
- expect(await this.token.isOperatorFor(defaultOperatorB, holder)).to.equal(true);
- });
- it('default operators list is not modified', async function () {
- expect(await this.token.defaultOperators()).to.deep.equal(defaultOperators);
- });
- it('revoked default operator can be re-authorized', async function () {
- const receipt = await this.token.authorizeOperator(defaultOperatorA, { from: holder });
- expectEvent(receipt, 'AuthorizedOperator', { operator: defaultOperatorA, tokenHolder: holder });
- expect(await this.token.isOperatorFor(defaultOperatorA, holder)).to.equal(true);
- });
- });
- });
- });
- describe('send and receive hooks', function () {
- const amount = new BN('1');
- const operator = defaultOperatorA;
- // sender and recipient are stored inside 'this', since in some tests their addresses are determined dynamically
- describe('tokensReceived', function () {
- beforeEach(function () {
- this.sender = holder;
- });
- context('with no ERC777TokensRecipient implementer', function () {
- context('with contract recipient', function () {
- beforeEach(async function () {
- this.tokensRecipientImplementer = await ERC777SenderRecipientMock.new();
- this.recipient = this.tokensRecipientImplementer.address;
- // Note that tokensRecipientImplementer doesn't implement the recipient interface for the recipient
- });
- it('send reverts', async function () {
- await expectRevert(
- this.token.send(this.recipient, amount, data, { from: holder }),
- 'ERC777: token recipient contract has no implementer for ERC777TokensRecipient',
- );
- });
- it('operatorSend reverts', async function () {
- await expectRevert(
- this.token.operatorSend(this.sender, this.recipient, amount, data, operatorData, { from: operator }),
- 'ERC777: token recipient contract has no implementer for ERC777TokensRecipient',
- );
- });
- it('mint (internal) reverts', async function () {
- await expectRevert(
- this.token.$_mint(this.recipient, amount, data, operatorData, true, { from: operator }),
- 'ERC777: token recipient contract has no implementer for ERC777TokensRecipient',
- );
- });
- it('(ERC20) transfer succeeds', async function () {
- await this.token.transfer(this.recipient, amount, { from: holder });
- });
- it('(ERC20) transferFrom succeeds', async function () {
- const approved = anyone;
- await this.token.approve(approved, amount, { from: this.sender });
- await this.token.transferFrom(this.sender, this.recipient, amount, { from: approved });
- });
- });
- });
- context('with ERC777TokensRecipient implementer', function () {
- context('with contract as implementer for an externally owned account', function () {
- beforeEach(async function () {
- this.tokensRecipientImplementer = await ERC777SenderRecipientMock.new();
- this.recipient = anyone;
- await this.tokensRecipientImplementer.recipientFor(this.recipient);
- await this.erc1820.setInterfaceImplementer(
- this.recipient,
- web3.utils.soliditySha3('ERC777TokensRecipient'),
- this.tokensRecipientImplementer.address,
- { from: this.recipient },
- );
- });
- shouldBehaveLikeERC777SendBurnMintInternalWithReceiveHook(operator, amount, data, operatorData);
- });
- context('with contract as implementer for another contract', function () {
- beforeEach(async function () {
- this.recipientContract = await ERC777SenderRecipientMock.new();
- this.recipient = this.recipientContract.address;
- this.tokensRecipientImplementer = await ERC777SenderRecipientMock.new();
- await this.tokensRecipientImplementer.recipientFor(this.recipient);
- await this.recipientContract.registerRecipient(this.tokensRecipientImplementer.address);
- });
- shouldBehaveLikeERC777SendBurnMintInternalWithReceiveHook(operator, amount, data, operatorData);
- });
- context('with contract as implementer for itself', function () {
- beforeEach(async function () {
- this.tokensRecipientImplementer = await ERC777SenderRecipientMock.new();
- this.recipient = this.tokensRecipientImplementer.address;
- await this.tokensRecipientImplementer.recipientFor(this.recipient);
- });
- shouldBehaveLikeERC777SendBurnMintInternalWithReceiveHook(operator, amount, data, operatorData);
- });
- });
- });
- describe('tokensToSend', function () {
- beforeEach(function () {
- this.recipient = anyone;
- });
- context('with a contract as implementer for an externally owned account', function () {
- beforeEach(async function () {
- this.tokensSenderImplementer = await ERC777SenderRecipientMock.new();
- this.sender = holder;
- await this.tokensSenderImplementer.senderFor(this.sender);
- await this.erc1820.setInterfaceImplementer(
- this.sender,
- web3.utils.soliditySha3('ERC777TokensSender'),
- this.tokensSenderImplementer.address,
- { from: this.sender },
- );
- });
- shouldBehaveLikeERC777SendBurnWithSendHook(operator, amount, data, operatorData);
- });
- context('with contract as implementer for another contract', function () {
- beforeEach(async function () {
- this.senderContract = await ERC777SenderRecipientMock.new();
- this.sender = this.senderContract.address;
- this.tokensSenderImplementer = await ERC777SenderRecipientMock.new();
- await this.tokensSenderImplementer.senderFor(this.sender);
- await this.senderContract.registerSender(this.tokensSenderImplementer.address);
- // For the contract to be able to receive tokens (that it can later send), it must also implement the
- // recipient interface.
- await this.senderContract.recipientFor(this.sender);
- await this.token.send(this.sender, amount, data, { from: holder });
- });
- shouldBehaveLikeERC777SendBurnWithSendHook(operator, amount, data, operatorData);
- });
- context('with a contract as implementer for itself', function () {
- beforeEach(async function () {
- this.tokensSenderImplementer = await ERC777SenderRecipientMock.new();
- this.sender = this.tokensSenderImplementer.address;
- await this.tokensSenderImplementer.senderFor(this.sender);
- // For the contract to be able to receive tokens (that it can later send), it must also implement the
- // recipient interface.
- await this.tokensSenderImplementer.recipientFor(this.sender);
- await this.token.send(this.sender, amount, data, { from: holder });
- });
- shouldBehaveLikeERC777SendBurnWithSendHook(operator, amount, data, operatorData);
- });
- });
- });
- });
- context('with no default operators', function () {
- beforeEach(async function () {
- this.token = await ERC777.new(name, symbol, []);
- });
- it('default operators list is empty', async function () {
- expect(await this.token.defaultOperators()).to.deep.equal([]);
- });
- });
- describe('relative order of hooks', function () {
- beforeEach(async function () {
- await singletons.ERC1820Registry(registryFunder);
- this.sender = await ERC777SenderRecipientMock.new();
- await this.sender.registerRecipient(this.sender.address);
- await this.sender.registerSender(this.sender.address);
- this.token = await ERC777.new(name, symbol, []);
- await this.token.$_mint(this.sender.address, 1, '0x', '0x');
- });
- it('send', async function () {
- const { receipt } = await this.sender.send(this.token.address, anyone, 1, '0x');
- const internalBeforeHook = receipt.logs.findIndex(l => l.event === 'BeforeTokenTransfer');
- expect(internalBeforeHook).to.be.gte(0);
- const externalSendHook = receipt.logs.findIndex(l => l.event === 'TokensToSendCalled');
- expect(externalSendHook).to.be.gte(0);
- expect(externalSendHook).to.be.lt(internalBeforeHook);
- });
- it('burn', async function () {
- const { receipt } = await this.sender.burn(this.token.address, 1, '0x');
- const internalBeforeHook = receipt.logs.findIndex(l => l.event === 'BeforeTokenTransfer');
- expect(internalBeforeHook).to.be.gte(0);
- const externalSendHook = receipt.logs.findIndex(l => l.event === 'TokensToSendCalled');
- expect(externalSendHook).to.be.gte(0);
- expect(externalSendHook).to.be.lt(internalBeforeHook);
- });
- });
- });
|