* IERC777 from specs, constants returned, up to defaultOperators. (#1159) * IERC777 oprarator approvals (#1159) * ERC777 oprarator approvals fixes and tests * IERC777 send and receive with ERC820 (#1159) * ERC777 Add burn functions and fix send functions (#1159) * ERC777 Make expectEvent compatible with web3.js 1.0 (#1159) * ERC777 Add ERC820 deploy script (#1159) * ERC777 Complete implementation of ERC777 (#1159) This implementation conforms to the current EIP * ERC777 Update ERC820 Registry contract to final version (#1159) * ERC777 Move contracts to 'drafts' folder (#1159) * ERC777: Update to ERC1820 registry and linter error fix (#1159) * ERC777: implement recent changes of EIP777 (#1159) * ERC777 Fix formatting (#1159) * ERC777 Update to solc 0.5.2 (#1159) * ERC777 Fix travis CI errors (#1159) * ERC777 Fix linter errors again... (#1159) * ERC777 Fix unit test (#1159) * ERC777 Fix unit test again (#1159) * Remove extra newlines. * Rename ERC777Base to ERC777. * Remove 'Token' from contract names. * Replace ops for operators. * Move operator check out of _send. * Remove ERC777Burnable. * Remove ERC1820Client, now using the interface directly. * Minor internal refactors in contracts. * Delete extra test helpers. * Simplified tests. * Add basic 777 tests. * Add granularity send test. * Add first operator send tests. * Add burn tests. * Refactor send and burn tests. * Improve send burn refactor. * Greatly improve test module. * Burn instead of send removed tokens. * Add operator tests. * Improve send tests under changing operators. * Refactor and merge send and burn tests. * Add missing and not-implemented tests. * Make _burn private. * Fix typo. * Greatly improve tokensToSend tests. * Refactor hook tests. * Fix hook tests. * Update openzeppelin-test-helpers and ERC1820 address. * Fix natspec indentation. * Make interface functions external. * Remove redundant private revoke and authorize functions. * Improved readability of if statement. * Remove unnecessary asserts. * Add non-one granularity test. * Fix hook call order in _mint. * Fix _mint not reverting on failure to implement tokensReceived. * Remove special case in operatorFn when from is 0. * Refactor ERC777SenderMock. * Add tokensReceived tests. * switch to updated ganache-cli-coverage fork * Fix linter errors. * Add mint tests. * Fix linter errors. * Fix tests. * Update test/drafts/ERC777/ERC777.test.js Co-Authored-By: nventuro <nicolas.venturo@gmail.com> * Add changelog entry.
This commit is contained in:
committed by
Nicolás Venturo
parent
963f1eb35b
commit
5a2b349992
533
test/drafts/ERC777/ERC777.behavior.js
Normal file
533
test/drafts/ERC777/ERC777.behavior.js
Normal file
@ -0,0 +1,533 @@
|
||||
const { BN, constants, expectEvent, shouldFail } = require('openzeppelin-test-helpers');
|
||||
const { ZERO_ADDRESS } = constants;
|
||||
|
||||
const ERC777SenderRecipientMock = artifacts.require('ERC777SenderRecipientMock');
|
||||
|
||||
function shouldBehaveLikeERC777DirectSendBurn (holder, recipient, data) {
|
||||
shouldBehaveLikeERC777DirectSend(holder, recipient, data);
|
||||
shouldBehaveLikeERC777DirectBurn(holder, data);
|
||||
}
|
||||
|
||||
function shouldBehaveLikeERC777OperatorSendBurn (holder, recipient, operator, data, operatorData) {
|
||||
shouldBehaveLikeERC777OperatorSend(holder, recipient, operator, data, operatorData);
|
||||
shouldBehaveLikeERC777OperatorBurn(holder, operator, data, operatorData);
|
||||
}
|
||||
|
||||
function shouldBehaveLikeERC777UnauthorizedOperatorSendBurn (holder, recipient, operator, data, operatorData) {
|
||||
shouldBehaveLikeERC777UnauthorizedOperatorSend(holder, recipient, operator, data, operatorData);
|
||||
shouldBehaveLikeERC777UnauthorizedOperatorBurn(holder, operator, data, operatorData);
|
||||
}
|
||||
|
||||
function shouldBehaveLikeERC777DirectSend (holder, recipient, data) {
|
||||
describe('direct send', function () {
|
||||
context('when the sender has tokens', function () {
|
||||
shouldDirectSendTokens(holder, recipient, new BN('0'), data);
|
||||
shouldDirectSendTokens(holder, recipient, new BN('1'), data);
|
||||
|
||||
it('reverts when sending more than the balance', async function () {
|
||||
const balance = await this.token.balanceOf(holder);
|
||||
await shouldFail.reverting(this.token.send(recipient, balance.addn(1), data, { from: holder }));
|
||||
});
|
||||
|
||||
it('reverts when sending to the zero address', async function () {
|
||||
await shouldFail.reverting(this.token.send(ZERO_ADDRESS, new BN('1'), data, { from: holder }));
|
||||
});
|
||||
});
|
||||
|
||||
context('when the sender has no tokens', function () {
|
||||
removeBalance(holder);
|
||||
|
||||
shouldDirectSendTokens(holder, recipient, new BN('0'), data);
|
||||
|
||||
it('reverts when sending a non-zero amount', async function () {
|
||||
await shouldFail.reverting(this.token.send(recipient, new BN('1'), data, { from: holder }));
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function shouldBehaveLikeERC777OperatorSend (holder, recipient, operator, data, operatorData) {
|
||||
describe('operator send', function () {
|
||||
context('when the sender has tokens', async function () {
|
||||
shouldOperatorSendTokens(holder, operator, recipient, new BN('0'), data, operatorData);
|
||||
shouldOperatorSendTokens(holder, operator, recipient, new BN('1'), data, operatorData);
|
||||
|
||||
it('reverts when sending more than the balance', async function () {
|
||||
const balance = await this.token.balanceOf(holder);
|
||||
await shouldFail.reverting(
|
||||
this.token.operatorSend(holder, recipient, balance.addn(1), data, operatorData, { from: operator })
|
||||
);
|
||||
});
|
||||
|
||||
it('reverts when sending to the zero address', async function () {
|
||||
await shouldFail.reverting(
|
||||
this.token.operatorSend(
|
||||
holder, ZERO_ADDRESS, new BN('1'), data, operatorData, { from: operator }
|
||||
)
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
context('when the sender has no tokens', function () {
|
||||
removeBalance(holder);
|
||||
|
||||
shouldOperatorSendTokens(holder, operator, recipient, new BN('0'), data, operatorData);
|
||||
|
||||
it('reverts when sending a non-zero amount', async function () {
|
||||
await shouldFail.reverting(
|
||||
this.token.operatorSend(holder, recipient, new BN('1'), data, operatorData, { from: operator })
|
||||
);
|
||||
});
|
||||
|
||||
it('reverts when sending from the zero address', async function () {
|
||||
// This is not yet reflected in the spec
|
||||
await shouldFail.reverting(
|
||||
this.token.operatorSend(
|
||||
ZERO_ADDRESS, recipient, new BN('0'), data, operatorData, { from: operator }
|
||||
)
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function shouldBehaveLikeERC777UnauthorizedOperatorSend (holder, recipient, operator, data, operatorData) {
|
||||
describe('operator send', function () {
|
||||
it('reverts', async function () {
|
||||
await shouldFail.reverting(this.token.operatorSend(holder, recipient, new BN('0'), data, operatorData));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function shouldBehaveLikeERC777DirectBurn (holder, data) {
|
||||
describe('direct burn', function () {
|
||||
context('when the sender has tokens', function () {
|
||||
shouldDirectBurnTokens(holder, new BN('0'), data);
|
||||
shouldDirectBurnTokens(holder, new BN('1'), data);
|
||||
|
||||
it('reverts when burning more than the balance', async function () {
|
||||
const balance = await this.token.balanceOf(holder);
|
||||
await shouldFail.reverting(this.token.burn(balance.addn(1), data, { from: holder }));
|
||||
});
|
||||
});
|
||||
|
||||
context('when the sender has no tokens', function () {
|
||||
removeBalance(holder);
|
||||
|
||||
shouldDirectBurnTokens(holder, new BN('0'), data);
|
||||
|
||||
it('reverts when burning a non-zero amount', async function () {
|
||||
await shouldFail.reverting(this.token.burn(new BN('1'), data, { from: holder }));
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function shouldBehaveLikeERC777OperatorBurn (holder, operator, data, operatorData) {
|
||||
describe('operator burn', function () {
|
||||
context('when the sender has tokens', async function () {
|
||||
shouldOperatorBurnTokens(holder, operator, new BN('0'), data, operatorData);
|
||||
shouldOperatorBurnTokens(holder, operator, new BN('1'), data, operatorData);
|
||||
|
||||
it('reverts when burning more than the balance', async function () {
|
||||
const balance = await this.token.balanceOf(holder);
|
||||
await shouldFail.reverting(
|
||||
this.token.operatorBurn(holder, balance.addn(1), data, operatorData, { from: operator })
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
context('when the sender has no tokens', function () {
|
||||
removeBalance(holder);
|
||||
|
||||
shouldOperatorBurnTokens(holder, operator, new BN('0'), data, operatorData);
|
||||
|
||||
it('reverts when burning a non-zero amount', async function () {
|
||||
await shouldFail.reverting(
|
||||
this.token.operatorBurn(holder, new BN('1'), data, operatorData, { from: operator })
|
||||
);
|
||||
});
|
||||
|
||||
it('reverts when burning from the zero address', async function () {
|
||||
// This is not yet reflected in the spec
|
||||
await shouldFail.reverting(
|
||||
this.token.operatorBurn(
|
||||
ZERO_ADDRESS, new BN('0'), data, operatorData, { from: operator }
|
||||
)
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function shouldBehaveLikeERC777UnauthorizedOperatorBurn (holder, operator, data, operatorData) {
|
||||
describe('operator burn', function () {
|
||||
it('reverts', async function () {
|
||||
await shouldFail.reverting(this.token.operatorBurn(holder, new BN('0'), data, operatorData));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function shouldDirectSendTokens (from, to, amount, data) {
|
||||
shouldSendTokens(from, null, to, amount, data, null);
|
||||
}
|
||||
|
||||
function shouldOperatorSendTokens (from, operator, to, amount, data, operatorData) {
|
||||
shouldSendTokens(from, operator, to, amount, data, operatorData);
|
||||
}
|
||||
|
||||
function shouldSendTokens (from, operator, to, amount, data, operatorData) {
|
||||
const operatorCall = operator !== null;
|
||||
|
||||
it(`${operatorCall ? 'operator ' : ''}can send an amount of ${amount}`, async function () {
|
||||
const initialTotalSupply = await this.token.totalSupply();
|
||||
const initialFromBalance = await this.token.balanceOf(from);
|
||||
const initialToBalance = await this.token.balanceOf(to);
|
||||
|
||||
if (!operatorCall) {
|
||||
const { logs } = await this.token.send(to, amount, data, { from });
|
||||
expectEvent.inLogs(logs, 'Sent', {
|
||||
operator: from,
|
||||
from,
|
||||
to,
|
||||
amount,
|
||||
data,
|
||||
operatorData: null,
|
||||
});
|
||||
} else {
|
||||
const { logs } = await this.token.operatorSend(from, to, amount, data, operatorData, { from: operator });
|
||||
expectEvent.inLogs(logs, 'Sent', {
|
||||
operator,
|
||||
from,
|
||||
to,
|
||||
amount,
|
||||
data,
|
||||
operatorData,
|
||||
});
|
||||
}
|
||||
|
||||
const finalTotalSupply = await this.token.totalSupply();
|
||||
const finalFromBalance = await this.token.balanceOf(from);
|
||||
const finalToBalance = await this.token.balanceOf(to);
|
||||
|
||||
finalTotalSupply.should.be.bignumber.equal(initialTotalSupply);
|
||||
finalToBalance.sub(initialToBalance).should.be.bignumber.equal(amount);
|
||||
finalFromBalance.sub(initialFromBalance).should.be.bignumber.equal(amount.neg());
|
||||
});
|
||||
}
|
||||
|
||||
function shouldDirectBurnTokens (from, amount, data) {
|
||||
shouldBurnTokens(from, null, amount, data, null);
|
||||
}
|
||||
|
||||
function shouldOperatorBurnTokens (from, operator, amount, data, operatorData) {
|
||||
shouldBurnTokens(from, operator, amount, data, operatorData);
|
||||
}
|
||||
|
||||
function shouldBurnTokens (from, operator, amount, data, operatorData) {
|
||||
const operatorCall = operator !== null;
|
||||
|
||||
it(`${operatorCall ? 'operator ' : ''}can burn an amount of ${amount}`, async function () {
|
||||
const initialTotalSupply = await this.token.totalSupply();
|
||||
const initialFromBalance = await this.token.balanceOf(from);
|
||||
|
||||
if (!operatorCall) {
|
||||
const { logs } = await this.token.burn(amount, data, { from });
|
||||
expectEvent.inLogs(logs, 'Burned', {
|
||||
operator: from,
|
||||
from,
|
||||
amount,
|
||||
data,
|
||||
operatorData: null,
|
||||
});
|
||||
} else {
|
||||
const { logs } = await this.token.operatorBurn(from, amount, data, operatorData, { from: operator });
|
||||
expectEvent.inLogs(logs, 'Burned', {
|
||||
operator,
|
||||
from,
|
||||
amount,
|
||||
data,
|
||||
operatorData,
|
||||
});
|
||||
}
|
||||
|
||||
const finalTotalSupply = await this.token.totalSupply();
|
||||
const finalFromBalance = await this.token.balanceOf(from);
|
||||
|
||||
finalTotalSupply.sub(initialTotalSupply).should.be.bignumber.equal(amount.neg());
|
||||
finalFromBalance.sub(initialFromBalance).should.be.bignumber.equal(amount.neg());
|
||||
});
|
||||
}
|
||||
|
||||
function shouldBehaveLikeERC777InternalMint (recipient, operator, amount, data, operatorData) {
|
||||
shouldInternalMintTokens(operator, recipient, new BN('0'), data, operatorData);
|
||||
shouldInternalMintTokens(operator, recipient, amount, data, operatorData);
|
||||
|
||||
it('reverts when minting tokens for the zero address', async function () {
|
||||
await shouldFail.reverting(this.token.mintInternal(operator, ZERO_ADDRESS, amount, data, operatorData));
|
||||
});
|
||||
}
|
||||
|
||||
function shouldInternalMintTokens (operator, to, amount, data, operatorData) {
|
||||
it(`can (internal) mint an amount of ${amount}`, async function () {
|
||||
const initialTotalSupply = await this.token.totalSupply();
|
||||
const initialToBalance = await this.token.balanceOf(to);
|
||||
|
||||
const { logs } = await this.token.mintInternal(operator, to, amount, data, operatorData);
|
||||
expectEvent.inLogs(logs, 'Minted', {
|
||||
operator,
|
||||
to,
|
||||
amount,
|
||||
data,
|
||||
operatorData,
|
||||
});
|
||||
|
||||
const finalTotalSupply = await this.token.totalSupply();
|
||||
const finalToBalance = await this.token.balanceOf(to);
|
||||
|
||||
finalTotalSupply.sub(initialTotalSupply).should.be.bignumber.equal(amount);
|
||||
finalToBalance.sub(initialToBalance).should.be.bignumber.equal(amount);
|
||||
});
|
||||
}
|
||||
|
||||
function shouldBehaveLikeERC777SendBurnMintInternalWithReceiveHook (operator, amount, data, operatorData) {
|
||||
context('when TokensRecipient reverts', function () {
|
||||
beforeEach(async function () {
|
||||
await this.tokensRecipientImplementer.setShouldRevertReceive(true);
|
||||
});
|
||||
|
||||
it('send reverts', async function () {
|
||||
await shouldFail.reverting(sendFromHolder(this.token, this.sender, this.recipient, amount, data));
|
||||
});
|
||||
|
||||
it('operatorSend reverts', async function () {
|
||||
await shouldFail.reverting(
|
||||
this.token.operatorSend(this.sender, this.recipient, amount, data, operatorData, { from: operator })
|
||||
);
|
||||
});
|
||||
|
||||
it('mint (internal) reverts', async function () {
|
||||
await shouldFail.reverting(
|
||||
this.token.mintInternal(operator, this.recipient, amount, data, operatorData)
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
context('when TokensRecipient does not revert', function () {
|
||||
beforeEach(async function () {
|
||||
await this.tokensRecipientImplementer.setShouldRevertSend(false);
|
||||
});
|
||||
|
||||
it('TokensRecipient receives send data and is called after state mutation', async function () {
|
||||
const { tx } = await sendFromHolder(this.token, this.sender, this.recipient, amount, data);
|
||||
|
||||
const postSenderBalance = await this.token.balanceOf(this.sender);
|
||||
const postRecipientBalance = await this.token.balanceOf(this.recipient);
|
||||
|
||||
await assertTokensReceivedCalled(
|
||||
this.token,
|
||||
tx,
|
||||
this.sender,
|
||||
this.sender,
|
||||
this.recipient,
|
||||
amount,
|
||||
data,
|
||||
null,
|
||||
postSenderBalance,
|
||||
postRecipientBalance,
|
||||
);
|
||||
});
|
||||
|
||||
it('TokensRecipient receives operatorSend data and is called after state mutation', async function () {
|
||||
const { tx } = await this.token.operatorSend(
|
||||
this.sender, this.recipient, amount, data, operatorData,
|
||||
{ from: operator },
|
||||
);
|
||||
|
||||
const postSenderBalance = await this.token.balanceOf(this.sender);
|
||||
const postRecipientBalance = await this.token.balanceOf(this.recipient);
|
||||
|
||||
await assertTokensReceivedCalled(
|
||||
this.token,
|
||||
tx,
|
||||
operator,
|
||||
this.sender,
|
||||
this.recipient,
|
||||
amount,
|
||||
data,
|
||||
operatorData,
|
||||
postSenderBalance,
|
||||
postRecipientBalance,
|
||||
);
|
||||
});
|
||||
|
||||
it('TokensRecipient receives mint (internal) data and is called after state mutation', async function () {
|
||||
const { tx } = await this.token.mintInternal(
|
||||
operator, this.recipient, amount, data, operatorData,
|
||||
);
|
||||
|
||||
const postRecipientBalance = await this.token.balanceOf(this.recipient);
|
||||
|
||||
await assertTokensReceivedCalled(
|
||||
this.token,
|
||||
tx,
|
||||
operator,
|
||||
ZERO_ADDRESS,
|
||||
this.recipient,
|
||||
amount,
|
||||
data,
|
||||
operatorData,
|
||||
new BN('0'),
|
||||
postRecipientBalance,
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function shouldBehaveLikeERC777SendBurnWithSendHook (operator, amount, data, operatorData) {
|
||||
context('when TokensSender reverts', function () {
|
||||
beforeEach(async function () {
|
||||
await this.tokensSenderImplementer.setShouldRevertSend(true);
|
||||
});
|
||||
|
||||
it('send reverts', async function () {
|
||||
await shouldFail.reverting(sendFromHolder(this.token, this.sender, this.recipient, amount, data));
|
||||
});
|
||||
|
||||
it('operatorSend reverts', async function () {
|
||||
await shouldFail.reverting(
|
||||
this.token.operatorSend(this.sender, this.recipient, amount, data, operatorData, { from: operator })
|
||||
);
|
||||
});
|
||||
|
||||
it('burn reverts', async function () {
|
||||
await shouldFail.reverting(burnFromHolder(this.token, this.sender, amount, data));
|
||||
});
|
||||
|
||||
it('operatorBurn reverts', async function () {
|
||||
await shouldFail.reverting(
|
||||
this.token.operatorBurn(this.sender, amount, data, operatorData, { from: operator })
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
context('when TokensSender does not revert', function () {
|
||||
beforeEach(async function () {
|
||||
await this.tokensSenderImplementer.setShouldRevertSend(false);
|
||||
});
|
||||
|
||||
it('TokensSender receives send data and is called before state mutation', async function () {
|
||||
const preSenderBalance = await this.token.balanceOf(this.sender);
|
||||
const preRecipientBalance = await this.token.balanceOf(this.recipient);
|
||||
|
||||
const { tx } = await sendFromHolder(this.token, this.sender, this.recipient, amount, data);
|
||||
|
||||
await assertTokensToSendCalled(
|
||||
this.token,
|
||||
tx,
|
||||
this.sender,
|
||||
this.sender,
|
||||
this.recipient,
|
||||
amount,
|
||||
data,
|
||||
null,
|
||||
preSenderBalance,
|
||||
preRecipientBalance,
|
||||
);
|
||||
});
|
||||
|
||||
it('TokensSender receives operatorSend data and is called before state mutation', async function () {
|
||||
const preSenderBalance = await this.token.balanceOf(this.sender);
|
||||
const preRecipientBalance = await this.token.balanceOf(this.recipient);
|
||||
|
||||
const { tx } = await this.token.operatorSend(
|
||||
this.sender, this.recipient, amount, data, operatorData,
|
||||
{ from: operator },
|
||||
);
|
||||
|
||||
await assertTokensToSendCalled(
|
||||
this.token,
|
||||
tx,
|
||||
operator,
|
||||
this.sender,
|
||||
this.recipient,
|
||||
amount,
|
||||
data,
|
||||
operatorData,
|
||||
preSenderBalance,
|
||||
preRecipientBalance,
|
||||
);
|
||||
});
|
||||
|
||||
it('TokensSender receives burn data and is called before state mutation', async function () {
|
||||
const preSenderBalance = await this.token.balanceOf(this.sender);
|
||||
|
||||
const { tx } = await burnFromHolder(this.token, this.sender, amount, data, { from: this.sender });
|
||||
|
||||
await assertTokensToSendCalled(
|
||||
this.token, tx, this.sender, this.sender, ZERO_ADDRESS, amount, data, null, preSenderBalance
|
||||
);
|
||||
});
|
||||
|
||||
it('TokensSender receives operatorBurn data and is called before state mutation', async function () {
|
||||
const preSenderBalance = await this.token.balanceOf(this.sender);
|
||||
|
||||
const { tx } = await this.token.operatorBurn(this.sender, amount, data, operatorData, { from: operator });
|
||||
|
||||
await assertTokensToSendCalled(
|
||||
this.token, tx, operator, this.sender, ZERO_ADDRESS, amount, data, operatorData, preSenderBalance
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function removeBalance (holder) {
|
||||
beforeEach(async function () {
|
||||
await this.token.burn(await this.token.balanceOf(holder), '0x', { from: holder });
|
||||
(await this.token.balanceOf(holder)).should.be.bignumber.equal('0');
|
||||
});
|
||||
}
|
||||
|
||||
async function assertTokensReceivedCalled (token, txHash, operator, from, to, amount, data, operatorData, fromBalance,
|
||||
toBalance = '0') {
|
||||
await expectEvent.inTransaction(txHash, ERC777SenderRecipientMock, 'TokensReceivedCalled', {
|
||||
operator, from, to, amount, data, operatorData, token: token.address, fromBalance, toBalance,
|
||||
});
|
||||
}
|
||||
|
||||
async function assertTokensToSendCalled (token, txHash, operator, from, to, amount, data, operatorData, fromBalance,
|
||||
toBalance = '0') {
|
||||
await expectEvent.inTransaction(txHash, ERC777SenderRecipientMock, 'TokensToSendCalled', {
|
||||
operator, from, to, amount, data, operatorData, token: token.address, fromBalance, toBalance,
|
||||
});
|
||||
}
|
||||
|
||||
async function sendFromHolder (token, holder, to, amount, data) {
|
||||
if ((await web3.eth.getCode(holder)).length <= '0x'.length) {
|
||||
return token.send(to, amount, data, { from: holder });
|
||||
} else {
|
||||
// assume holder is ERC777SenderRecipientMock contract
|
||||
return (await ERC777SenderRecipientMock.at(holder)).send(token.address, to, amount, data);
|
||||
}
|
||||
}
|
||||
|
||||
async function burnFromHolder (token, holder, amount, data) {
|
||||
if ((await web3.eth.getCode(holder)).length <= '0x'.length) {
|
||||
return token.burn(amount, data, { from: holder });
|
||||
} else {
|
||||
// assume holder is ERC777SenderRecipientMock contract
|
||||
return (await ERC777SenderRecipientMock.at(holder)).burn(token.address, amount, data);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
shouldBehaveLikeERC777DirectSendBurn,
|
||||
shouldBehaveLikeERC777OperatorSendBurn,
|
||||
shouldBehaveLikeERC777UnauthorizedOperatorSendBurn,
|
||||
shouldDirectSendTokens,
|
||||
shouldDirectBurnTokens,
|
||||
shouldBehaveLikeERC777InternalMint,
|
||||
shouldInternalMintTokens,
|
||||
shouldBehaveLikeERC777SendBurnMintInternalWithReceiveHook,
|
||||
shouldBehaveLikeERC777SendBurnWithSendHook,
|
||||
};
|
||||
448
test/drafts/ERC777/ERC777.test.js
Normal file
448
test/drafts/ERC777/ERC777.test.js
Normal file
@ -0,0 +1,448 @@
|
||||
const { BN, expectEvent, shouldFail, singletons } = require('openzeppelin-test-helpers');
|
||||
|
||||
const {
|
||||
shouldBehaveLikeERC777DirectSendBurn,
|
||||
shouldBehaveLikeERC777OperatorSendBurn,
|
||||
shouldBehaveLikeERC777UnauthorizedOperatorSendBurn,
|
||||
shouldDirectSendTokens,
|
||||
shouldDirectBurnTokens,
|
||||
shouldBehaveLikeERC777InternalMint,
|
||||
shouldInternalMintTokens,
|
||||
shouldBehaveLikeERC777SendBurnMintInternalWithReceiveHook,
|
||||
shouldBehaveLikeERC777SendBurnWithSendHook,
|
||||
} = require('./ERC777.behavior');
|
||||
|
||||
const ERC777 = artifacts.require('ERC777Mock');
|
||||
const ERC777SenderRecipientMock = artifacts.require('ERC777SenderRecipientMock');
|
||||
|
||||
contract('ERC777', function ([
|
||||
_, registryFunder, holder, defaultOperatorA, defaultOperatorB, newOperator, anyone,
|
||||
]) {
|
||||
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);
|
||||
});
|
||||
|
||||
it('reverts with a granularity of zero', async function () {
|
||||
await shouldFail.reverting(ERC777.new(holder, initialSupply, name, symbol, 0, []));
|
||||
});
|
||||
|
||||
context('with a granularity of one', function () {
|
||||
const granularity = new BN('1');
|
||||
|
||||
context('with default operators', function () {
|
||||
beforeEach(async function () {
|
||||
this.token = await ERC777.new(holder, initialSupply, name, symbol, granularity, defaultOperators);
|
||||
});
|
||||
|
||||
it.skip('does not emit AuthorizedOperator events for default operators', async function () {
|
||||
expectEvent.not.inConstructor(this.token, 'AuthorizedOperator'); // This helper needs to be implemented
|
||||
});
|
||||
|
||||
describe('basic information', function () {
|
||||
it('returns the name', async function () {
|
||||
(await this.token.name()).should.equal(name);
|
||||
});
|
||||
|
||||
it('returns the symbol', async function () {
|
||||
(await this.token.symbol()).should.equal(symbol);
|
||||
});
|
||||
|
||||
it('returns the granularity', async function () {
|
||||
(await this.token.granularity()).should.be.bignumber.equal(granularity);
|
||||
});
|
||||
|
||||
it('returns the default operators', async function () {
|
||||
(await this.token.defaultOperators()).should.deep.equal(defaultOperators);
|
||||
});
|
||||
|
||||
it('default operators are operators for all accounts', async function () {
|
||||
for (const operator of defaultOperators) {
|
||||
(await this.token.isOperatorFor(operator, anyone)).should.equal(true);
|
||||
}
|
||||
});
|
||||
|
||||
it('returns thte total supply', async function () {
|
||||
(await this.token.totalSupply()).should.be.bignumber.equal(initialSupply);
|
||||
});
|
||||
|
||||
it('is registered in the registry', async function () {
|
||||
(await this.erc1820.getInterfaceImplementer(this.token.address, web3.utils.soliditySha3('ERC777Token')))
|
||||
.should.equal(this.token.address);
|
||||
});
|
||||
});
|
||||
|
||||
describe('balanceOf', function () {
|
||||
context('for an account with no tokens', function () {
|
||||
it('returns zero', async function () {
|
||||
(await this.token.balanceOf(anyone)).should.be.bignumber.equal('0');
|
||||
});
|
||||
});
|
||||
|
||||
context('for an account with tokens', function () {
|
||||
it('returns their balance', async function () {
|
||||
(await this.token.balanceOf(holder)).should.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('operator management', function () {
|
||||
it('accounts are their own operator', async function () {
|
||||
(await this.token.isOperatorFor(holder, holder)).should.equal(true);
|
||||
});
|
||||
|
||||
it('reverts when self-authorizing', async function () {
|
||||
await shouldFail.reverting(this.token.authorizeOperator(holder, { from: holder }));
|
||||
});
|
||||
|
||||
it('reverts when self-revoking', async function () {
|
||||
await shouldFail.reverting(this.token.revokeOperator(holder, { from: holder }));
|
||||
});
|
||||
|
||||
it('non-operators can be revoked', async function () {
|
||||
(await this.token.isOperatorFor(newOperator, holder)).should.equal(false);
|
||||
|
||||
const { logs } = await this.token.revokeOperator(newOperator, { from: holder });
|
||||
expectEvent.inLogs(logs, 'RevokedOperator', { operator: newOperator, tokenHolder: holder });
|
||||
|
||||
(await this.token.isOperatorFor(newOperator, holder)).should.equal(false);
|
||||
});
|
||||
|
||||
it('non-operators can be authorized', async function () {
|
||||
(await this.token.isOperatorFor(newOperator, holder)).should.equal(false);
|
||||
|
||||
const { logs } = await this.token.authorizeOperator(newOperator, { from: holder });
|
||||
expectEvent.inLogs(logs, 'AuthorizedOperator', { operator: newOperator, tokenHolder: holder });
|
||||
|
||||
(await this.token.isOperatorFor(newOperator, holder)).should.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 () {
|
||||
(await this.token.defaultOperators()).should.deep.equal(defaultOperators);
|
||||
});
|
||||
|
||||
it('can be re-authorized', async function () {
|
||||
const { logs } = await this.token.authorizeOperator(newOperator, { from: holder });
|
||||
expectEvent.inLogs(logs, 'AuthorizedOperator', { operator: newOperator, tokenHolder: holder });
|
||||
|
||||
(await this.token.isOperatorFor(newOperator, holder)).should.equal(true);
|
||||
});
|
||||
|
||||
it('can be revoked', async function () {
|
||||
const { logs } = await this.token.revokeOperator(newOperator, { from: holder });
|
||||
expectEvent.inLogs(logs, 'RevokedOperator', { operator: newOperator, tokenHolder: holder });
|
||||
|
||||
(await this.token.isOperatorFor(newOperator, holder)).should.equal(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('default operators', function () {
|
||||
it('can be re-authorized', async function () {
|
||||
const { logs } = await this.token.authorizeOperator(defaultOperatorA, { from: holder });
|
||||
expectEvent.inLogs(logs, 'AuthorizedOperator', { operator: defaultOperatorA, tokenHolder: holder });
|
||||
|
||||
(await this.token.isOperatorFor(defaultOperatorA, holder)).should.equal(true);
|
||||
});
|
||||
|
||||
it('can be revoked', async function () {
|
||||
const { logs } = await this.token.revokeOperator(defaultOperatorA, { from: holder });
|
||||
expectEvent.inLogs(logs, 'RevokedOperator', { operator: defaultOperatorA, tokenHolder: holder });
|
||||
|
||||
(await this.token.isOperatorFor(defaultOperatorA, holder)).should.equal(false);
|
||||
});
|
||||
|
||||
it('cannot be revoked for themselves', async function () {
|
||||
await shouldFail.reverting(this.token.revokeOperator(defaultOperatorA, { from: defaultOperatorA }));
|
||||
});
|
||||
|
||||
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 () {
|
||||
(await this.token.isOperatorFor(defaultOperatorA, anyone)).should.equal(true);
|
||||
});
|
||||
|
||||
it('other default operators are not revoked', async function () {
|
||||
(await this.token.isOperatorFor(defaultOperatorB, holder)).should.equal(true);
|
||||
});
|
||||
|
||||
it('default operators list is not modified', async function () {
|
||||
(await this.token.defaultOperators()).should.deep.equal(defaultOperators);
|
||||
});
|
||||
|
||||
it('revoked default operator can be re-authorized', async function () {
|
||||
const { logs } = await this.token.authorizeOperator(defaultOperatorA, { from: holder });
|
||||
expectEvent.inLogs(logs, 'AuthorizedOperator', { operator: defaultOperatorA, tokenHolder: holder });
|
||||
|
||||
(await this.token.isOperatorFor(defaultOperatorA, holder)).should.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 shouldFail.reverting(this.token.send(this.recipient, amount, data));
|
||||
});
|
||||
|
||||
it('operatorSend reverts', async function () {
|
||||
await shouldFail.reverting(
|
||||
this.token.operatorSend(this.sender, this.recipient, amount, data, operatorData, { from: operator })
|
||||
);
|
||||
});
|
||||
|
||||
it('mint (internal) reverts', async function () {
|
||||
await shouldFail.reverting(
|
||||
this.token.mintInternal(operator, this.recipient, amount, data, operatorData)
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
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(holder, initialSupply, name, symbol, granularity, []);
|
||||
});
|
||||
|
||||
it('default operators list is empty', async function () {
|
||||
(await this.token.defaultOperators()).should.deep.equal([]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
context('with granularity larger than 1', function () {
|
||||
const granularity = new BN('4');
|
||||
|
||||
beforeEach(async function () {
|
||||
initialSupply.mod(granularity).should.be.bignumber.equal('0');
|
||||
|
||||
this.token = await ERC777.new(holder, initialSupply, name, symbol, granularity, defaultOperators);
|
||||
});
|
||||
|
||||
it('returns the granularity', async function () {
|
||||
(await this.token.granularity()).should.be.bignumber.equal(granularity);
|
||||
});
|
||||
|
||||
context('when the sender has tokens', function () {
|
||||
const from = holder;
|
||||
|
||||
shouldDirectSendTokens(from, anyone, new BN('0'), data);
|
||||
shouldDirectSendTokens(from, anyone, granularity, data);
|
||||
shouldDirectSendTokens(from, anyone, granularity.muln(2), data);
|
||||
|
||||
it('reverts when sending an amount non-multiple of the granularity', async function () {
|
||||
await shouldFail.reverting(this.token.send(anyone, granularity.subn(1), data, { from }));
|
||||
});
|
||||
|
||||
shouldDirectBurnTokens(from, new BN('0'), data);
|
||||
shouldDirectBurnTokens(from, granularity, data);
|
||||
shouldDirectBurnTokens(from, granularity.muln(2), data);
|
||||
|
||||
it('reverts when burning an amount non-multiple of the granularity', async function () {
|
||||
await shouldFail.reverting(this.token.burn(granularity.subn(1), data, { from }));
|
||||
});
|
||||
});
|
||||
|
||||
shouldInternalMintTokens(anyone, defaultOperatorA, new BN('0'), data, operatorData);
|
||||
shouldInternalMintTokens(anyone, defaultOperatorA, granularity, data, operatorData);
|
||||
shouldInternalMintTokens(anyone, defaultOperatorA, granularity.muln(2), data, operatorData);
|
||||
|
||||
it('reverts when minting an amount non-multiple of the granularity', async function () {
|
||||
await shouldFail.reverting(
|
||||
this.token.mintInternal(defaultOperatorA, anyone, granularity.subn(1), data, operatorData)
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user