@animoca/ethereum-contracts-assets
Version:
Base assets contracts
320 lines (277 loc) • 13.9 kB
JavaScript
const {config, artifacts, accounts, web3} = require('hardhat');
const {createFixtureLoader} = require('@animoca/ethereum-contracts-core/test/utils/fixture');
const {expectEventWithParamsOverride} = require('@animoca/ethereum-contracts-core/test/utils/events');
const {BN, expectEvent, expectRevert} = require('@openzeppelin/test-helpers');
const {behaviors, constants} = require('@animoca/ethereum-contracts-core');
const {Zero, One, MaxUInt256, ZeroAddress} = constants;
const interfaces20 = require('../../../../../src/interfaces/ERC165/ERC20');
function shouldBehaveLikeERC20Safe(implementation) {
const {features, interfaces, revertMessages, eventParamsOverrides, deploy} = implementation;
const [deployer, owner, recipient, spender, maxSpender] = accounts;
describe('like a safe ERC20', function () {
const initialSupply = new BN(100);
const initialAllowance = initialSupply.sub(One);
const data = '0x42';
const fixtureLoader = createFixtureLoader(accounts, web3.eth.currentProvider);
const fixture = async function () {
this.token = await deploy([owner], [initialSupply], deployer);
await this.token.approve(spender, initialAllowance, {from: owner});
await this.token.approve(maxSpender, MaxUInt256, {from: owner});
this.nonReceiver = await artifacts.require('ERC20Mock').new([], [], ZeroAddress, ZeroAddress);
this.receiver = await artifacts.require('ERC20ReceiverMock').new(true, this.token.address);
this.refusingReceiver = await artifacts.require('ERC20ReceiverMock').new(false, this.token.address);
this.wrongTokenReceiver = await artifacts.require('ERC20ReceiverMock').new(false, ZeroAddress);
};
beforeEach(async function () {
await fixtureLoader(fixture, this);
});
describe('safeTransfer(address,uint256,bytes)', function () {
context('Pre-conditions', function () {
it('reverts when sent to the zero address', async function () {
await expectRevert(this.token.safeTransfer(ZeroAddress, One, data, {from: owner}), revertMessages.TransferToZero);
});
it('reverts with an insufficient balance', async function () {
await expectRevert(this.token.safeTransfer(recipient, initialSupply.add(One), data, {from: owner}), revertMessages.TransferExceedsBalance);
});
it('reverts when sent to a non-receiver contract', async function () {
await expectRevert.unspecified(this.token.safeTransfer(this.nonReceiver.address, One, data, {from: owner}));
});
it('reverts when sent to a refusing receiver contract', async function () {
await expectRevert(this.token.safeTransfer(this.refusingReceiver.address, One, data, {from: owner}), revertMessages.TransferRefused);
});
it('reverts when sent to a receiver contract receiving another token', async function () {
await expectRevert(this.token.safeTransfer(this.wrongTokenReceiver.address, One, data, {from: owner}), 'ERC20Receiver: wrong token');
});
});
const transferWasSuccessful = function (to, value, options, toReceiver = false) {
if (options.from == to) {
it('does not affect the sender balance', async function () {
(await this.token.balanceOf(options.from)).should.be.bignumber.equal(initialSupply);
});
} else {
it('decreases the sender balance', async function () {
(await this.token.balanceOf(options.from)).should.be.bignumber.equal(initialSupply.sub(value));
});
it('increases the recipient balance', async function () {
(await this.token.balanceOf(toReceiver ? this.receiver.address : to)).should.be.bignumber.equal(value);
});
}
it('does not affect the token(s) total supply', async function () {
(await this.token.totalSupply()).should.be.bignumber.equal(initialSupply);
});
it('emits a Transfer event', function () {
expectEventWithParamsOverride(
this.receipt,
'Transfer',
{
_from: options.from,
_to: toReceiver ? this.receiver.address : to,
_value: value,
},
eventParamsOverrides
);
});
if (toReceiver) {
it('calls onERC20Received(address,address,uint256,bytes) on a receiver contract', function () {
expectEvent.inTransaction(this.receipt.tx, this.receiver, 'ERC20Received', {
sender: options.from,
from: options.from,
value: value,
data: data,
});
});
}
};
const shouldTransferTokenByRecipient = function (value) {
const options = {from: owner};
context('when transferring to the sender', function () {
const to = owner;
beforeEach(async function () {
this.fromBalance = await this.token.balanceOf(options.from);
this.toBalance = await this.token.balanceOf(to);
this.receipt = await this.token.safeTransfer(to, value, data, options);
});
transferWasSuccessful(to, value, options);
});
context('when transferring to another account', function () {
const to = recipient;
beforeEach(async function () {
this.fromBalance = await this.token.balanceOf(options.from);
this.toBalance = await this.token.balanceOf(to);
this.receipt = await this.token.safeTransfer(to, value, data, options);
});
transferWasSuccessful(to, value, options);
});
context('when transferring to an ERC20Receiver contract', function () {
beforeEach(async function () {
this.fromBalance = await this.token.balanceOf(options.from);
this.toBalance = await this.token.balanceOf(this.receiver.address);
this.receipt = await this.token.safeTransfer(this.receiver.address, value, data, options);
});
transferWasSuccessful(null, value, options, true);
});
};
context('when transferring a zero value', function () {
shouldTransferTokenByRecipient(Zero);
});
context('when transferring a non-zero value', function () {
shouldTransferTokenByRecipient(One);
});
context('when transferring the full balance', function () {
shouldTransferTokenByRecipient(initialSupply);
});
});
describe('safeTransferFrom(address,address,uint256,bytes)', function () {
context('Pre-conditions', function () {
it('reverts when from is the zero address', async function () {
await expectRevert(this.token.safeTransferFrom(ZeroAddress, recipient, One, data, {from: spender}), revertMessages.TransferFromZero);
});
it('reverts when sent to the zero address', async function () {
await expectRevert(this.token.safeTransferFrom(owner, ZeroAddress, One, data, {from: spender}), revertMessages.TransferToZero);
});
it('reverts with an insufficient balance', async function () {
await this.token.approve(spender, initialSupply.add(One), {from: owner});
await expectRevert(
this.token.safeTransferFrom(owner, recipient, initialSupply.add(One), data, {from: spender}),
revertMessages.TransferExceedsBalance
);
});
it('reverts with an insufficient allowance', async function () {
await expectRevert(
this.token.safeTransferFrom(owner, recipient, initialAllowance.add(One), data, {from: spender}),
revertMessages.TransferExceedsAllowance
);
});
it('reverts when sent to a non-receiver contract', async function () {
await expectRevert.unspecified(this.token.safeTransferFrom(owner, this.nonReceiver.address, One, data, {from: spender}));
});
it('reverts when sent to a refusing receiver contract', async function () {
await expectRevert(
this.token.safeTransferFrom(owner, this.refusingReceiver.address, One, data, {from: spender}),
revertMessages.TransferRefused
);
});
const transferWasSuccessful = function (from, to, value, options, withEIP717, toReceiver = false) {
if (from == to) {
it('does not affect the sender balance', async function () {
(await this.token.balanceOf(from)).should.be.bignumber.equal(initialSupply);
});
} else {
it('decreases the sender balance', async function () {
(await this.token.balanceOf(from)).should.be.bignumber.equal(initialSupply.sub(value));
});
it('increases the recipient balance', async function () {
(await this.token.balanceOf(toReceiver ? this.receiver.address : to)).should.be.bignumber.equal(value);
});
}
it('does not affect the token(s) total supply', async function () {
(await this.token.totalSupply()).should.be.bignumber.equal(initialSupply);
});
it('emits a Transfer event', function () {
expectEventWithParamsOverride(
this.receipt,
'Transfer',
{
_from: from,
_to: toReceiver ? this.receiver.address : to,
_value: value,
},
eventParamsOverrides
);
});
if (from != options.from) {
if (withEIP717) {
it('[EIP717] keeps allowance at max ', async function () {
(await this.token.allowance(from, options.from)).should.be.bignumber.equal(MaxUInt256);
});
} else {
it('decreases the spender allowance', async function () {
(await this.token.allowance(from, options.from)).should.be.bignumber.equal(this.allowance.sub(value));
});
}
if (features.AllowanceTracking) {
it('emits an Approval event', function () {
expectEventWithParamsOverride(
this.receipt,
'Approval',
{
_owner: from,
_spender: options.from,
_value: withEIP717 ? MaxUInt256 : this.allowance.sub(value),
},
eventParamsOverrides
);
});
}
}
};
const shouldTransferTokenByRecipient = function (from, value, options, withEIP717 = false) {
context('when transferring to different recipients', function () {
beforeEach(async function () {
this.fromBalance = await this.token.balanceOf(options.from);
this.allowance = await this.token.allowance(from, options.from);
});
context('when transferring to the owner', function () {
const to = owner;
beforeEach(async function () {
this.toBalance = await this.token.balanceOf(to);
this.receipt = await this.token.safeTransferFrom(from, to, value, data, options);
});
transferWasSuccessful(from, to, value, options, withEIP717);
});
context('when transferring to the spender', function () {
const to = spender;
beforeEach(async function () {
this.toBalance = await this.token.balanceOf(to);
this.receipt = await this.token.safeTransferFrom(from, to, value, data, options);
});
transferWasSuccessful(from, to, value, options, withEIP717);
});
context('when transferring to another account', function () {
const to = recipient;
beforeEach(async function () {
this.toBalance = await this.token.balanceOf(to);
this.receipt = await this.token.safeTransferFrom(from, to, value, data, options);
});
transferWasSuccessful(from, to, value, options, withEIP717);
});
context('when transferring to an ERC20Receiver contract', function () {
beforeEach(async function () {
this.toBalance = await this.token.balanceOf(this.receiver.address);
this.receipt = await this.token.safeTransferFrom(from, this.receiver.address, value, data, options);
});
transferWasSuccessful(from, null, value, options, withEIP717, true);
});
});
};
const shouldTransferTokenBySender = function (value) {
const from = owner;
context('when transfer started by the owner', function () {
shouldTransferTokenByRecipient(from, value, {from: owner});
});
context('when transfer started by an approved sender', function () {
shouldTransferTokenByRecipient(from, value, {from: spender});
});
context('when transfer started by a sender with max approval', function () {
shouldTransferTokenByRecipient(from, value, {from: maxSpender}, features.EIP717);
});
};
context('when transferring a zero value', function () {
shouldTransferTokenBySender(Zero);
});
context('when transferring a non-zero value', function () {
shouldTransferTokenBySender(One);
});
context('when transferring the full allowance', function () {
shouldTransferTokenBySender(initialAllowance);
});
});
});
if (features.ERC165) {
behaviors.shouldSupportInterfaces([interfaces20.ERC20SafeTransfers]);
}
});
}
module.exports = {
shouldBehaveLikeERC20Safe,
};