mirror of
https://github.com/th30d4y/OpenLearnX.git
synced 2026-05-26 19:26:33 +00:00
Fix .gitignore: stop tracking ignored files
This commit is contained in:
@@ -0,0 +1,322 @@
|
||||
const { BN, constants, expectEvent, expectRevert } = require('@openzeppelin/test-helpers');
|
||||
const { expect } = require('chai');
|
||||
const { ZERO_ADDRESS, MAX_UINT256 } = constants;
|
||||
|
||||
function shouldBehaveLikeERC20(errorPrefix, initialSupply, initialHolder, recipient, anotherAccount) {
|
||||
describe('total supply', function () {
|
||||
it('returns the total amount of tokens', async function () {
|
||||
expect(await this.token.totalSupply()).to.be.bignumber.equal(initialSupply);
|
||||
});
|
||||
});
|
||||
|
||||
describe('balanceOf', function () {
|
||||
describe('when the requested account has no tokens', function () {
|
||||
it('returns zero', async function () {
|
||||
expect(await this.token.balanceOf(anotherAccount)).to.be.bignumber.equal('0');
|
||||
});
|
||||
});
|
||||
|
||||
describe('when the requested account has some tokens', function () {
|
||||
it('returns the total amount of tokens', async function () {
|
||||
expect(await this.token.balanceOf(initialHolder)).to.be.bignumber.equal(initialSupply);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('transfer', function () {
|
||||
shouldBehaveLikeERC20Transfer(errorPrefix, initialHolder, recipient, initialSupply, function (from, to, value) {
|
||||
return this.token.transfer(to, value, { from });
|
||||
});
|
||||
});
|
||||
|
||||
describe('transfer from', function () {
|
||||
const spender = recipient;
|
||||
|
||||
describe('when the token owner is not the zero address', function () {
|
||||
const tokenOwner = initialHolder;
|
||||
|
||||
describe('when the recipient is not the zero address', function () {
|
||||
const to = anotherAccount;
|
||||
|
||||
describe('when the spender has enough allowance', function () {
|
||||
beforeEach(async function () {
|
||||
await this.token.approve(spender, initialSupply, { from: initialHolder });
|
||||
});
|
||||
|
||||
describe('when the token owner has enough balance', function () {
|
||||
const amount = initialSupply;
|
||||
|
||||
it('transfers the requested amount', async function () {
|
||||
await this.token.transferFrom(tokenOwner, to, amount, { from: spender });
|
||||
|
||||
expect(await this.token.balanceOf(tokenOwner)).to.be.bignumber.equal('0');
|
||||
|
||||
expect(await this.token.balanceOf(to)).to.be.bignumber.equal(amount);
|
||||
});
|
||||
|
||||
it('decreases the spender allowance', async function () {
|
||||
await this.token.transferFrom(tokenOwner, to, amount, { from: spender });
|
||||
|
||||
expect(await this.token.allowance(tokenOwner, spender)).to.be.bignumber.equal('0');
|
||||
});
|
||||
|
||||
it('emits a transfer event', async function () {
|
||||
expectEvent(await this.token.transferFrom(tokenOwner, to, amount, { from: spender }), 'Transfer', {
|
||||
from: tokenOwner,
|
||||
to: to,
|
||||
value: amount,
|
||||
});
|
||||
});
|
||||
|
||||
it('emits an approval event', async function () {
|
||||
expectEvent(await this.token.transferFrom(tokenOwner, to, amount, { from: spender }), 'Approval', {
|
||||
owner: tokenOwner,
|
||||
spender: spender,
|
||||
value: await this.token.allowance(tokenOwner, spender),
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when the token owner does not have enough balance', function () {
|
||||
const amount = initialSupply;
|
||||
|
||||
beforeEach('reducing balance', async function () {
|
||||
await this.token.transfer(to, 1, { from: tokenOwner });
|
||||
});
|
||||
|
||||
it('reverts', async function () {
|
||||
await expectRevert(
|
||||
this.token.transferFrom(tokenOwner, to, amount, { from: spender }),
|
||||
`${errorPrefix}: transfer amount exceeds balance`,
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when the spender does not have enough allowance', function () {
|
||||
const allowance = initialSupply.subn(1);
|
||||
|
||||
beforeEach(async function () {
|
||||
await this.token.approve(spender, allowance, { from: tokenOwner });
|
||||
});
|
||||
|
||||
describe('when the token owner has enough balance', function () {
|
||||
const amount = initialSupply;
|
||||
|
||||
it('reverts', async function () {
|
||||
await expectRevert(
|
||||
this.token.transferFrom(tokenOwner, to, amount, { from: spender }),
|
||||
`${errorPrefix}: insufficient allowance`,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when the token owner does not have enough balance', function () {
|
||||
const amount = allowance;
|
||||
|
||||
beforeEach('reducing balance', async function () {
|
||||
await this.token.transfer(to, 2, { from: tokenOwner });
|
||||
});
|
||||
|
||||
it('reverts', async function () {
|
||||
await expectRevert(
|
||||
this.token.transferFrom(tokenOwner, to, amount, { from: spender }),
|
||||
`${errorPrefix}: transfer amount exceeds balance`,
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when the spender has unlimited allowance', function () {
|
||||
beforeEach(async function () {
|
||||
await this.token.approve(spender, MAX_UINT256, { from: initialHolder });
|
||||
});
|
||||
|
||||
it('does not decrease the spender allowance', async function () {
|
||||
await this.token.transferFrom(tokenOwner, to, 1, { from: spender });
|
||||
|
||||
expect(await this.token.allowance(tokenOwner, spender)).to.be.bignumber.equal(MAX_UINT256);
|
||||
});
|
||||
|
||||
it('does not emit an approval event', async function () {
|
||||
expectEvent.notEmitted(await this.token.transferFrom(tokenOwner, to, 1, { from: spender }), 'Approval');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when the recipient is the zero address', function () {
|
||||
const amount = initialSupply;
|
||||
const to = ZERO_ADDRESS;
|
||||
|
||||
beforeEach(async function () {
|
||||
await this.token.approve(spender, amount, { from: tokenOwner });
|
||||
});
|
||||
|
||||
it('reverts', async function () {
|
||||
await expectRevert(
|
||||
this.token.transferFrom(tokenOwner, to, amount, { from: spender }),
|
||||
`${errorPrefix}: transfer to the zero address`,
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when the token owner is the zero address', function () {
|
||||
const amount = 0;
|
||||
const tokenOwner = ZERO_ADDRESS;
|
||||
const to = recipient;
|
||||
|
||||
it('reverts', async function () {
|
||||
await expectRevert(this.token.transferFrom(tokenOwner, to, amount, { from: spender }), 'from the zero address');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('approve', function () {
|
||||
shouldBehaveLikeERC20Approve(
|
||||
errorPrefix,
|
||||
initialHolder,
|
||||
recipient,
|
||||
initialSupply,
|
||||
function (owner, spender, amount) {
|
||||
return this.token.approve(spender, amount, { from: owner });
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
function shouldBehaveLikeERC20Transfer(errorPrefix, from, to, balance, transfer) {
|
||||
describe('when the recipient is not the zero address', function () {
|
||||
describe('when the sender does not have enough balance', function () {
|
||||
const amount = balance.addn(1);
|
||||
|
||||
it('reverts', async function () {
|
||||
await expectRevert(transfer.call(this, from, to, amount), `${errorPrefix}: transfer amount exceeds balance`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when the sender transfers all balance', function () {
|
||||
const amount = balance;
|
||||
|
||||
it('transfers the requested amount', async function () {
|
||||
await transfer.call(this, from, to, amount);
|
||||
|
||||
expect(await this.token.balanceOf(from)).to.be.bignumber.equal('0');
|
||||
|
||||
expect(await this.token.balanceOf(to)).to.be.bignumber.equal(amount);
|
||||
});
|
||||
|
||||
it('emits a transfer event', async function () {
|
||||
expectEvent(await transfer.call(this, from, to, amount), 'Transfer', { from, to, value: amount });
|
||||
});
|
||||
});
|
||||
|
||||
describe('when the sender transfers zero tokens', function () {
|
||||
const amount = new BN('0');
|
||||
|
||||
it('transfers the requested amount', async function () {
|
||||
await transfer.call(this, from, to, amount);
|
||||
|
||||
expect(await this.token.balanceOf(from)).to.be.bignumber.equal(balance);
|
||||
|
||||
expect(await this.token.balanceOf(to)).to.be.bignumber.equal('0');
|
||||
});
|
||||
|
||||
it('emits a transfer event', async function () {
|
||||
expectEvent(await transfer.call(this, from, to, amount), 'Transfer', { from, to, value: amount });
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when the recipient is the zero address', function () {
|
||||
it('reverts', async function () {
|
||||
await expectRevert(
|
||||
transfer.call(this, from, ZERO_ADDRESS, balance),
|
||||
`${errorPrefix}: transfer to the zero address`,
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function shouldBehaveLikeERC20Approve(errorPrefix, owner, spender, supply, approve) {
|
||||
describe('when the spender is not the zero address', function () {
|
||||
describe('when the sender has enough balance', function () {
|
||||
const amount = supply;
|
||||
|
||||
it('emits an approval event', async function () {
|
||||
expectEvent(await approve.call(this, owner, spender, amount), 'Approval', {
|
||||
owner: owner,
|
||||
spender: spender,
|
||||
value: amount,
|
||||
});
|
||||
});
|
||||
|
||||
describe('when there was no approved amount before', function () {
|
||||
it('approves the requested amount', async function () {
|
||||
await approve.call(this, owner, spender, amount);
|
||||
|
||||
expect(await this.token.allowance(owner, spender)).to.be.bignumber.equal(amount);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when the spender had an approved amount', function () {
|
||||
beforeEach(async function () {
|
||||
await approve.call(this, owner, spender, new BN(1));
|
||||
});
|
||||
|
||||
it('approves the requested amount and replaces the previous one', async function () {
|
||||
await approve.call(this, owner, spender, amount);
|
||||
|
||||
expect(await this.token.allowance(owner, spender)).to.be.bignumber.equal(amount);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when the sender does not have enough balance', function () {
|
||||
const amount = supply.addn(1);
|
||||
|
||||
it('emits an approval event', async function () {
|
||||
expectEvent(await approve.call(this, owner, spender, amount), 'Approval', {
|
||||
owner: owner,
|
||||
spender: spender,
|
||||
value: amount,
|
||||
});
|
||||
});
|
||||
|
||||
describe('when there was no approved amount before', function () {
|
||||
it('approves the requested amount', async function () {
|
||||
await approve.call(this, owner, spender, amount);
|
||||
|
||||
expect(await this.token.allowance(owner, spender)).to.be.bignumber.equal(amount);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when the spender had an approved amount', function () {
|
||||
beforeEach(async function () {
|
||||
await approve.call(this, owner, spender, new BN(1));
|
||||
});
|
||||
|
||||
it('approves the requested amount and replaces the previous one', async function () {
|
||||
await approve.call(this, owner, spender, amount);
|
||||
|
||||
expect(await this.token.allowance(owner, spender)).to.be.bignumber.equal(amount);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when the spender is the zero address', function () {
|
||||
it('reverts', async function () {
|
||||
await expectRevert(
|
||||
approve.call(this, owner, ZERO_ADDRESS, supply),
|
||||
`${errorPrefix}: approve to the zero address`,
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
shouldBehaveLikeERC20,
|
||||
shouldBehaveLikeERC20Transfer,
|
||||
shouldBehaveLikeERC20Approve,
|
||||
};
|
||||
@@ -0,0 +1,305 @@
|
||||
const { BN, constants, expectEvent, expectRevert } = require('@openzeppelin/test-helpers');
|
||||
const { expect } = require('chai');
|
||||
const { ZERO_ADDRESS } = constants;
|
||||
|
||||
const {
|
||||
shouldBehaveLikeERC20,
|
||||
shouldBehaveLikeERC20Transfer,
|
||||
shouldBehaveLikeERC20Approve,
|
||||
} = require('./ERC20.behavior');
|
||||
|
||||
const ERC20 = artifacts.require('$ERC20');
|
||||
const ERC20Decimals = artifacts.require('$ERC20DecimalsMock');
|
||||
|
||||
contract('ERC20', function (accounts) {
|
||||
const [initialHolder, recipient, anotherAccount] = accounts;
|
||||
|
||||
const name = 'My Token';
|
||||
const symbol = 'MTKN';
|
||||
|
||||
const initialSupply = new BN(100);
|
||||
|
||||
beforeEach(async function () {
|
||||
this.token = await ERC20.new(name, symbol);
|
||||
await this.token.$_mint(initialHolder, initialSupply);
|
||||
});
|
||||
|
||||
it('has a name', async function () {
|
||||
expect(await this.token.name()).to.equal(name);
|
||||
});
|
||||
|
||||
it('has a symbol', async function () {
|
||||
expect(await this.token.symbol()).to.equal(symbol);
|
||||
});
|
||||
|
||||
it('has 18 decimals', async function () {
|
||||
expect(await this.token.decimals()).to.be.bignumber.equal('18');
|
||||
});
|
||||
|
||||
describe('set decimals', function () {
|
||||
const decimals = new BN(6);
|
||||
|
||||
it('can set decimals during construction', async function () {
|
||||
const token = await ERC20Decimals.new(name, symbol, decimals);
|
||||
expect(await token.decimals()).to.be.bignumber.equal(decimals);
|
||||
});
|
||||
});
|
||||
|
||||
shouldBehaveLikeERC20('ERC20', initialSupply, initialHolder, recipient, anotherAccount);
|
||||
|
||||
describe('decrease allowance', function () {
|
||||
describe('when the spender is not the zero address', function () {
|
||||
const spender = recipient;
|
||||
|
||||
function shouldDecreaseApproval(amount) {
|
||||
describe('when there was no approved amount before', function () {
|
||||
it('reverts', async function () {
|
||||
await expectRevert(
|
||||
this.token.decreaseAllowance(spender, amount, { from: initialHolder }),
|
||||
'ERC20: decreased allowance below zero',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when the spender had an approved amount', function () {
|
||||
const approvedAmount = amount;
|
||||
|
||||
beforeEach(async function () {
|
||||
await this.token.approve(spender, approvedAmount, { from: initialHolder });
|
||||
});
|
||||
|
||||
it('emits an approval event', async function () {
|
||||
expectEvent(
|
||||
await this.token.decreaseAllowance(spender, approvedAmount, { from: initialHolder }),
|
||||
'Approval',
|
||||
{ owner: initialHolder, spender: spender, value: new BN(0) },
|
||||
);
|
||||
});
|
||||
|
||||
it('decreases the spender allowance subtracting the requested amount', async function () {
|
||||
await this.token.decreaseAllowance(spender, approvedAmount.subn(1), { from: initialHolder });
|
||||
|
||||
expect(await this.token.allowance(initialHolder, spender)).to.be.bignumber.equal('1');
|
||||
});
|
||||
|
||||
it('sets the allowance to zero when all allowance is removed', async function () {
|
||||
await this.token.decreaseAllowance(spender, approvedAmount, { from: initialHolder });
|
||||
expect(await this.token.allowance(initialHolder, spender)).to.be.bignumber.equal('0');
|
||||
});
|
||||
|
||||
it('reverts when more than the full allowance is removed', async function () {
|
||||
await expectRevert(
|
||||
this.token.decreaseAllowance(spender, approvedAmount.addn(1), { from: initialHolder }),
|
||||
'ERC20: decreased allowance below zero',
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
describe('when the sender has enough balance', function () {
|
||||
const amount = initialSupply;
|
||||
|
||||
shouldDecreaseApproval(amount);
|
||||
});
|
||||
|
||||
describe('when the sender does not have enough balance', function () {
|
||||
const amount = initialSupply.addn(1);
|
||||
|
||||
shouldDecreaseApproval(amount);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when the spender is the zero address', function () {
|
||||
const amount = initialSupply;
|
||||
const spender = ZERO_ADDRESS;
|
||||
|
||||
it('reverts', async function () {
|
||||
await expectRevert(
|
||||
this.token.decreaseAllowance(spender, amount, { from: initialHolder }),
|
||||
'ERC20: decreased allowance below zero',
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('increase allowance', function () {
|
||||
const amount = initialSupply;
|
||||
|
||||
describe('when the spender is not the zero address', function () {
|
||||
const spender = recipient;
|
||||
|
||||
describe('when the sender has enough balance', function () {
|
||||
it('emits an approval event', async function () {
|
||||
expectEvent(await this.token.increaseAllowance(spender, amount, { from: initialHolder }), 'Approval', {
|
||||
owner: initialHolder,
|
||||
spender: spender,
|
||||
value: amount,
|
||||
});
|
||||
});
|
||||
|
||||
describe('when there was no approved amount before', function () {
|
||||
it('approves the requested amount', async function () {
|
||||
await this.token.increaseAllowance(spender, amount, { from: initialHolder });
|
||||
|
||||
expect(await this.token.allowance(initialHolder, spender)).to.be.bignumber.equal(amount);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when the spender had an approved amount', function () {
|
||||
beforeEach(async function () {
|
||||
await this.token.approve(spender, new BN(1), { from: initialHolder });
|
||||
});
|
||||
|
||||
it('increases the spender allowance adding the requested amount', async function () {
|
||||
await this.token.increaseAllowance(spender, amount, { from: initialHolder });
|
||||
|
||||
expect(await this.token.allowance(initialHolder, spender)).to.be.bignumber.equal(amount.addn(1));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when the sender does not have enough balance', function () {
|
||||
const amount = initialSupply.addn(1);
|
||||
|
||||
it('emits an approval event', async function () {
|
||||
expectEvent(await this.token.increaseAllowance(spender, amount, { from: initialHolder }), 'Approval', {
|
||||
owner: initialHolder,
|
||||
spender: spender,
|
||||
value: amount,
|
||||
});
|
||||
});
|
||||
|
||||
describe('when there was no approved amount before', function () {
|
||||
it('approves the requested amount', async function () {
|
||||
await this.token.increaseAllowance(spender, amount, { from: initialHolder });
|
||||
|
||||
expect(await this.token.allowance(initialHolder, spender)).to.be.bignumber.equal(amount);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when the spender had an approved amount', function () {
|
||||
beforeEach(async function () {
|
||||
await this.token.approve(spender, new BN(1), { from: initialHolder });
|
||||
});
|
||||
|
||||
it('increases the spender allowance adding the requested amount', async function () {
|
||||
await this.token.increaseAllowance(spender, amount, { from: initialHolder });
|
||||
|
||||
expect(await this.token.allowance(initialHolder, spender)).to.be.bignumber.equal(amount.addn(1));
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when the spender is the zero address', function () {
|
||||
const spender = ZERO_ADDRESS;
|
||||
|
||||
it('reverts', async function () {
|
||||
await expectRevert(
|
||||
this.token.increaseAllowance(spender, amount, { from: initialHolder }),
|
||||
'ERC20: approve to the zero address',
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('_mint', function () {
|
||||
const amount = new BN(50);
|
||||
it('rejects a null account', async function () {
|
||||
await expectRevert(this.token.$_mint(ZERO_ADDRESS, amount), 'ERC20: mint to the zero address');
|
||||
});
|
||||
|
||||
describe('for a non zero account', function () {
|
||||
beforeEach('minting', async function () {
|
||||
this.receipt = await this.token.$_mint(recipient, amount);
|
||||
});
|
||||
|
||||
it('increments totalSupply', async function () {
|
||||
const expectedSupply = initialSupply.add(amount);
|
||||
expect(await this.token.totalSupply()).to.be.bignumber.equal(expectedSupply);
|
||||
});
|
||||
|
||||
it('increments recipient balance', async function () {
|
||||
expect(await this.token.balanceOf(recipient)).to.be.bignumber.equal(amount);
|
||||
});
|
||||
|
||||
it('emits Transfer event', async function () {
|
||||
const event = expectEvent(this.receipt, 'Transfer', { from: ZERO_ADDRESS, to: recipient });
|
||||
|
||||
expect(event.args.value).to.be.bignumber.equal(amount);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('_burn', function () {
|
||||
it('rejects a null account', async function () {
|
||||
await expectRevert(this.token.$_burn(ZERO_ADDRESS, new BN(1)), 'ERC20: burn from the zero address');
|
||||
});
|
||||
|
||||
describe('for a non zero account', function () {
|
||||
it('rejects burning more than balance', async function () {
|
||||
await expectRevert(
|
||||
this.token.$_burn(initialHolder, initialSupply.addn(1)),
|
||||
'ERC20: burn amount exceeds balance',
|
||||
);
|
||||
});
|
||||
|
||||
const describeBurn = function (description, amount) {
|
||||
describe(description, function () {
|
||||
beforeEach('burning', async function () {
|
||||
this.receipt = await this.token.$_burn(initialHolder, amount);
|
||||
});
|
||||
|
||||
it('decrements totalSupply', async function () {
|
||||
const expectedSupply = initialSupply.sub(amount);
|
||||
expect(await this.token.totalSupply()).to.be.bignumber.equal(expectedSupply);
|
||||
});
|
||||
|
||||
it('decrements initialHolder balance', async function () {
|
||||
const expectedBalance = initialSupply.sub(amount);
|
||||
expect(await this.token.balanceOf(initialHolder)).to.be.bignumber.equal(expectedBalance);
|
||||
});
|
||||
|
||||
it('emits Transfer event', async function () {
|
||||
const event = expectEvent(this.receipt, 'Transfer', { from: initialHolder, to: ZERO_ADDRESS });
|
||||
|
||||
expect(event.args.value).to.be.bignumber.equal(amount);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
describeBurn('for entire balance', initialSupply);
|
||||
describeBurn('for less amount than balance', initialSupply.subn(1));
|
||||
});
|
||||
});
|
||||
|
||||
describe('_transfer', function () {
|
||||
shouldBehaveLikeERC20Transfer('ERC20', initialHolder, recipient, initialSupply, function (from, to, amount) {
|
||||
return this.token.$_transfer(from, to, amount);
|
||||
});
|
||||
|
||||
describe('when the sender is the zero address', function () {
|
||||
it('reverts', async function () {
|
||||
await expectRevert(
|
||||
this.token.$_transfer(ZERO_ADDRESS, recipient, initialSupply),
|
||||
'ERC20: transfer from the zero address',
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('_approve', function () {
|
||||
shouldBehaveLikeERC20Approve('ERC20', initialHolder, recipient, 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, recipient, initialSupply),
|
||||
'ERC20: approve from the zero address',
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
+106
@@ -0,0 +1,106 @@
|
||||
const { BN, constants, expectEvent, expectRevert } = require('@openzeppelin/test-helpers');
|
||||
const { ZERO_ADDRESS } = constants;
|
||||
|
||||
const { expect } = require('chai');
|
||||
|
||||
function shouldBehaveLikeERC20Burnable(owner, initialBalance, [burner]) {
|
||||
describe('burn', function () {
|
||||
describe('when the given amount is not greater than balance of the sender', function () {
|
||||
context('for a zero amount', function () {
|
||||
shouldBurn(new BN(0));
|
||||
});
|
||||
|
||||
context('for a non-zero amount', function () {
|
||||
shouldBurn(new BN(100));
|
||||
});
|
||||
|
||||
function shouldBurn(amount) {
|
||||
beforeEach(async function () {
|
||||
this.receipt = await this.token.burn(amount, { from: owner });
|
||||
});
|
||||
|
||||
it('burns the requested amount', async function () {
|
||||
expect(await this.token.balanceOf(owner)).to.be.bignumber.equal(initialBalance.sub(amount));
|
||||
});
|
||||
|
||||
it('emits a transfer event', async function () {
|
||||
expectEvent(this.receipt, 'Transfer', {
|
||||
from: owner,
|
||||
to: ZERO_ADDRESS,
|
||||
value: amount,
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
describe('when the given amount is greater than the balance of the sender', function () {
|
||||
const amount = initialBalance.addn(1);
|
||||
|
||||
it('reverts', async function () {
|
||||
await expectRevert(this.token.burn(amount, { from: owner }), 'ERC20: burn amount exceeds balance');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('burnFrom', function () {
|
||||
describe('on success', function () {
|
||||
context('for a zero amount', function () {
|
||||
shouldBurnFrom(new BN(0));
|
||||
});
|
||||
|
||||
context('for a non-zero amount', function () {
|
||||
shouldBurnFrom(new BN(100));
|
||||
});
|
||||
|
||||
function shouldBurnFrom(amount) {
|
||||
const originalAllowance = amount.muln(3);
|
||||
|
||||
beforeEach(async function () {
|
||||
await this.token.approve(burner, originalAllowance, { from: owner });
|
||||
this.receipt = await this.token.burnFrom(owner, amount, { from: burner });
|
||||
});
|
||||
|
||||
it('burns the requested amount', async function () {
|
||||
expect(await this.token.balanceOf(owner)).to.be.bignumber.equal(initialBalance.sub(amount));
|
||||
});
|
||||
|
||||
it('decrements allowance', async function () {
|
||||
expect(await this.token.allowance(owner, burner)).to.be.bignumber.equal(originalAllowance.sub(amount));
|
||||
});
|
||||
|
||||
it('emits a transfer event', async function () {
|
||||
expectEvent(this.receipt, 'Transfer', {
|
||||
from: owner,
|
||||
to: ZERO_ADDRESS,
|
||||
value: amount,
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
describe('when the given amount is greater than the balance of the sender', function () {
|
||||
const amount = initialBalance.addn(1);
|
||||
|
||||
it('reverts', async function () {
|
||||
await this.token.approve(burner, amount, { from: owner });
|
||||
await expectRevert(this.token.burnFrom(owner, amount, { from: burner }), 'ERC20: burn amount exceeds balance');
|
||||
});
|
||||
});
|
||||
|
||||
describe('when the given amount is greater than the allowance', function () {
|
||||
const allowance = new BN(100);
|
||||
|
||||
it('reverts', async function () {
|
||||
await this.token.approve(burner, allowance, { from: owner });
|
||||
await expectRevert(
|
||||
this.token.burnFrom(owner, allowance.addn(1), { from: burner }),
|
||||
'ERC20: insufficient allowance',
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
shouldBehaveLikeERC20Burnable,
|
||||
};
|
||||
@@ -0,0 +1,20 @@
|
||||
const { BN } = require('@openzeppelin/test-helpers');
|
||||
|
||||
const { shouldBehaveLikeERC20Burnable } = require('./ERC20Burnable.behavior');
|
||||
const ERC20Burnable = artifacts.require('$ERC20Burnable');
|
||||
|
||||
contract('ERC20Burnable', function (accounts) {
|
||||
const [owner, ...otherAccounts] = accounts;
|
||||
|
||||
const initialBalance = new BN(1000);
|
||||
|
||||
const name = 'My Token';
|
||||
const symbol = 'MTKN';
|
||||
|
||||
beforeEach(async function () {
|
||||
this.token = await ERC20Burnable.new(name, symbol, { from: owner });
|
||||
await this.token.$_mint(owner, initialBalance);
|
||||
});
|
||||
|
||||
shouldBehaveLikeERC20Burnable(owner, initialBalance, otherAccounts);
|
||||
});
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
const { expectRevert } = require('@openzeppelin/test-helpers');
|
||||
|
||||
const { expect } = require('chai');
|
||||
|
||||
function shouldBehaveLikeERC20Capped(accounts, cap) {
|
||||
describe('capped token', function () {
|
||||
const user = accounts[0];
|
||||
|
||||
it('starts with the correct cap', async function () {
|
||||
expect(await this.token.cap()).to.be.bignumber.equal(cap);
|
||||
});
|
||||
|
||||
it('mints when amount is less than cap', async function () {
|
||||
await this.token.$_mint(user, cap.subn(1));
|
||||
expect(await this.token.totalSupply()).to.be.bignumber.equal(cap.subn(1));
|
||||
});
|
||||
|
||||
it('fails to mint if the amount exceeds the cap', async function () {
|
||||
await this.token.$_mint(user, cap.subn(1));
|
||||
await expectRevert(this.token.$_mint(user, 2), 'ERC20Capped: cap exceeded');
|
||||
});
|
||||
|
||||
it('fails to mint after cap is reached', async function () {
|
||||
await this.token.$_mint(user, cap);
|
||||
await expectRevert(this.token.$_mint(user, 1), 'ERC20Capped: cap exceeded');
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
shouldBehaveLikeERC20Capped,
|
||||
};
|
||||
@@ -0,0 +1,23 @@
|
||||
const { ether, expectRevert } = require('@openzeppelin/test-helpers');
|
||||
const { shouldBehaveLikeERC20Capped } = require('./ERC20Capped.behavior');
|
||||
|
||||
const ERC20Capped = artifacts.require('$ERC20Capped');
|
||||
|
||||
contract('ERC20Capped', function (accounts) {
|
||||
const cap = ether('1000');
|
||||
|
||||
const name = 'My Token';
|
||||
const symbol = 'MTKN';
|
||||
|
||||
it('requires a non-zero cap', async function () {
|
||||
await expectRevert(ERC20Capped.new(name, symbol, 0), 'ERC20Capped: cap is 0');
|
||||
});
|
||||
|
||||
context('once deployed', async function () {
|
||||
beforeEach(async function () {
|
||||
this.token = await ERC20Capped.new(name, symbol, cap);
|
||||
});
|
||||
|
||||
shouldBehaveLikeERC20Capped(accounts, cap);
|
||||
});
|
||||
});
|
||||
+204
@@ -0,0 +1,204 @@
|
||||
/* eslint-disable */
|
||||
|
||||
const { BN, constants, expectEvent, expectRevert } = require('@openzeppelin/test-helpers');
|
||||
const { expect } = require('chai');
|
||||
const { MAX_UINT256, ZERO_ADDRESS } = constants;
|
||||
|
||||
const ERC20FlashMintMock = artifacts.require('$ERC20FlashMintMock');
|
||||
const ERC3156FlashBorrowerMock = artifacts.require('ERC3156FlashBorrowerMock');
|
||||
|
||||
contract('ERC20FlashMint', function (accounts) {
|
||||
const [initialHolder, other, anotherAccount] = accounts;
|
||||
|
||||
const name = 'My Token';
|
||||
const symbol = 'MTKN';
|
||||
|
||||
const initialSupply = new BN(100);
|
||||
const loanAmount = new BN(10000000000000);
|
||||
|
||||
beforeEach(async function () {
|
||||
this.token = await ERC20FlashMintMock.new(name, symbol);
|
||||
await this.token.$_mint(initialHolder, initialSupply);
|
||||
});
|
||||
|
||||
describe('maxFlashLoan', function () {
|
||||
it('token match', async function () {
|
||||
expect(await this.token.maxFlashLoan(this.token.address)).to.be.bignumber.equal(MAX_UINT256.sub(initialSupply));
|
||||
});
|
||||
|
||||
it('token mismatch', async function () {
|
||||
expect(await this.token.maxFlashLoan(ZERO_ADDRESS)).to.be.bignumber.equal('0');
|
||||
});
|
||||
});
|
||||
|
||||
describe('flashFee', function () {
|
||||
it('token match', async function () {
|
||||
expect(await this.token.flashFee(this.token.address, loanAmount)).to.be.bignumber.equal('0');
|
||||
});
|
||||
|
||||
it('token mismatch', async function () {
|
||||
await expectRevert(this.token.flashFee(ZERO_ADDRESS, loanAmount), 'ERC20FlashMint: wrong token');
|
||||
});
|
||||
});
|
||||
|
||||
describe('flashFeeReceiver', function () {
|
||||
it('default receiver', async function () {
|
||||
expect(await this.token.$_flashFeeReceiver()).to.be.eq(ZERO_ADDRESS);
|
||||
});
|
||||
});
|
||||
|
||||
describe('flashLoan', function () {
|
||||
it('success', async function () {
|
||||
const receiver = await ERC3156FlashBorrowerMock.new(true, true);
|
||||
const { tx } = await this.token.flashLoan(receiver.address, this.token.address, loanAmount, '0x');
|
||||
|
||||
await expectEvent.inTransaction(tx, this.token, 'Transfer', {
|
||||
from: ZERO_ADDRESS,
|
||||
to: receiver.address,
|
||||
value: loanAmount,
|
||||
});
|
||||
await expectEvent.inTransaction(tx, this.token, 'Transfer', {
|
||||
from: receiver.address,
|
||||
to: ZERO_ADDRESS,
|
||||
value: loanAmount,
|
||||
});
|
||||
await expectEvent.inTransaction(tx, receiver, 'BalanceOf', {
|
||||
token: this.token.address,
|
||||
account: receiver.address,
|
||||
value: loanAmount,
|
||||
});
|
||||
await expectEvent.inTransaction(tx, receiver, 'TotalSupply', {
|
||||
token: this.token.address,
|
||||
value: initialSupply.add(loanAmount),
|
||||
});
|
||||
|
||||
expect(await this.token.totalSupply()).to.be.bignumber.equal(initialSupply);
|
||||
expect(await this.token.balanceOf(receiver.address)).to.be.bignumber.equal('0');
|
||||
expect(await this.token.allowance(receiver.address, this.token.address)).to.be.bignumber.equal('0');
|
||||
});
|
||||
|
||||
it('missing return value', async function () {
|
||||
const receiver = await ERC3156FlashBorrowerMock.new(false, true);
|
||||
await expectRevert(
|
||||
this.token.flashLoan(receiver.address, this.token.address, loanAmount, '0x'),
|
||||
'ERC20FlashMint: invalid return value',
|
||||
);
|
||||
});
|
||||
|
||||
it('missing approval', async function () {
|
||||
const receiver = await ERC3156FlashBorrowerMock.new(true, false);
|
||||
await expectRevert(
|
||||
this.token.flashLoan(receiver.address, this.token.address, loanAmount, '0x'),
|
||||
'ERC20: insufficient allowance',
|
||||
);
|
||||
});
|
||||
|
||||
it('unavailable funds', async function () {
|
||||
const receiver = await ERC3156FlashBorrowerMock.new(true, true);
|
||||
const data = this.token.contract.methods.transfer(other, 10).encodeABI();
|
||||
await expectRevert(
|
||||
this.token.flashLoan(receiver.address, this.token.address, loanAmount, data),
|
||||
'ERC20: burn amount exceeds balance',
|
||||
);
|
||||
});
|
||||
|
||||
it('more than maxFlashLoan', async function () {
|
||||
const receiver = await ERC3156FlashBorrowerMock.new(true, true);
|
||||
const data = this.token.contract.methods.transfer(other, 10).encodeABI();
|
||||
// _mint overflow reverts using a panic code. No reason string.
|
||||
await expectRevert.unspecified(this.token.flashLoan(receiver.address, this.token.address, MAX_UINT256, data));
|
||||
});
|
||||
|
||||
describe('custom flash fee & custom fee receiver', function () {
|
||||
const receiverInitialBalance = new BN(200000);
|
||||
const flashFee = new BN(5000);
|
||||
|
||||
beforeEach('init receiver balance & set flash fee', async function () {
|
||||
this.receiver = await ERC3156FlashBorrowerMock.new(true, true);
|
||||
const receipt = await this.token.$_mint(this.receiver.address, receiverInitialBalance);
|
||||
await expectEvent(receipt, 'Transfer', {
|
||||
from: ZERO_ADDRESS,
|
||||
to: this.receiver.address,
|
||||
value: receiverInitialBalance,
|
||||
});
|
||||
expect(await this.token.balanceOf(this.receiver.address)).to.be.bignumber.equal(receiverInitialBalance);
|
||||
|
||||
await this.token.setFlashFee(flashFee);
|
||||
expect(await this.token.flashFee(this.token.address, loanAmount)).to.be.bignumber.equal(flashFee);
|
||||
});
|
||||
|
||||
it('default flash fee receiver', async function () {
|
||||
const { tx } = await this.token.flashLoan(this.receiver.address, this.token.address, loanAmount, '0x');
|
||||
await expectEvent.inTransaction(tx, this.token, 'Transfer', {
|
||||
from: ZERO_ADDRESS,
|
||||
to: this.receiver.address,
|
||||
value: loanAmount,
|
||||
});
|
||||
await expectEvent.inTransaction(tx, this.token, 'Transfer', {
|
||||
from: this.receiver.address,
|
||||
to: ZERO_ADDRESS,
|
||||
value: loanAmount.add(flashFee),
|
||||
});
|
||||
await expectEvent.inTransaction(tx, this.receiver, 'BalanceOf', {
|
||||
token: this.token.address,
|
||||
account: this.receiver.address,
|
||||
value: receiverInitialBalance.add(loanAmount),
|
||||
});
|
||||
await expectEvent.inTransaction(tx, this.receiver, 'TotalSupply', {
|
||||
token: this.token.address,
|
||||
value: initialSupply.add(receiverInitialBalance).add(loanAmount),
|
||||
});
|
||||
|
||||
expect(await this.token.totalSupply()).to.be.bignumber.equal(
|
||||
initialSupply.add(receiverInitialBalance).sub(flashFee),
|
||||
);
|
||||
expect(await this.token.balanceOf(this.receiver.address)).to.be.bignumber.equal(
|
||||
receiverInitialBalance.sub(flashFee),
|
||||
);
|
||||
expect(await this.token.balanceOf(await this.token.$_flashFeeReceiver())).to.be.bignumber.equal('0');
|
||||
expect(await this.token.allowance(this.receiver.address, this.token.address)).to.be.bignumber.equal('0');
|
||||
});
|
||||
|
||||
it('custom flash fee receiver', async function () {
|
||||
const flashFeeReceiverAddress = anotherAccount;
|
||||
await this.token.setFlashFeeReceiver(flashFeeReceiverAddress);
|
||||
expect(await this.token.$_flashFeeReceiver()).to.be.eq(flashFeeReceiverAddress);
|
||||
|
||||
expect(await this.token.balanceOf(flashFeeReceiverAddress)).to.be.bignumber.equal('0');
|
||||
|
||||
const { tx } = await this.token.flashLoan(this.receiver.address, this.token.address, loanAmount, '0x');
|
||||
await expectEvent.inTransaction(tx, this.token, 'Transfer', {
|
||||
from: ZERO_ADDRESS,
|
||||
to: this.receiver.address,
|
||||
value: loanAmount,
|
||||
});
|
||||
await expectEvent.inTransaction(tx, this.token, 'Transfer', {
|
||||
from: this.receiver.address,
|
||||
to: ZERO_ADDRESS,
|
||||
value: loanAmount,
|
||||
});
|
||||
await expectEvent.inTransaction(tx, this.token, 'Transfer', {
|
||||
from: this.receiver.address,
|
||||
to: flashFeeReceiverAddress,
|
||||
value: flashFee,
|
||||
});
|
||||
await expectEvent.inTransaction(tx, this.receiver, 'BalanceOf', {
|
||||
token: this.token.address,
|
||||
account: this.receiver.address,
|
||||
value: receiverInitialBalance.add(loanAmount),
|
||||
});
|
||||
await expectEvent.inTransaction(tx, this.receiver, 'TotalSupply', {
|
||||
token: this.token.address,
|
||||
value: initialSupply.add(receiverInitialBalance).add(loanAmount),
|
||||
});
|
||||
|
||||
expect(await this.token.totalSupply()).to.be.bignumber.equal(initialSupply.add(receiverInitialBalance));
|
||||
expect(await this.token.balanceOf(this.receiver.address)).to.be.bignumber.equal(
|
||||
receiverInitialBalance.sub(flashFee),
|
||||
);
|
||||
expect(await this.token.balanceOf(flashFeeReceiverAddress)).to.be.bignumber.equal(flashFee);
|
||||
expect(await this.token.allowance(this.receiver.address, flashFeeReceiverAddress)).to.be.bignumber.equal('0');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,133 @@
|
||||
const { BN, expectRevert } = require('@openzeppelin/test-helpers');
|
||||
|
||||
const { expect } = require('chai');
|
||||
|
||||
const ERC20Pausable = artifacts.require('$ERC20Pausable');
|
||||
|
||||
contract('ERC20Pausable', function (accounts) {
|
||||
const [holder, recipient, anotherAccount] = accounts;
|
||||
|
||||
const initialSupply = new BN(100);
|
||||
|
||||
const name = 'My Token';
|
||||
const symbol = 'MTKN';
|
||||
|
||||
beforeEach(async function () {
|
||||
this.token = await ERC20Pausable.new(name, symbol);
|
||||
await this.token.$_mint(holder, initialSupply);
|
||||
});
|
||||
|
||||
describe('pausable token', function () {
|
||||
describe('transfer', function () {
|
||||
it('allows to transfer when unpaused', async function () {
|
||||
await this.token.transfer(recipient, initialSupply, { from: holder });
|
||||
|
||||
expect(await this.token.balanceOf(holder)).to.be.bignumber.equal('0');
|
||||
expect(await this.token.balanceOf(recipient)).to.be.bignumber.equal(initialSupply);
|
||||
});
|
||||
|
||||
it('allows to transfer when paused and then unpaused', async function () {
|
||||
await this.token.$_pause();
|
||||
await this.token.$_unpause();
|
||||
|
||||
await this.token.transfer(recipient, initialSupply, { from: holder });
|
||||
|
||||
expect(await this.token.balanceOf(holder)).to.be.bignumber.equal('0');
|
||||
expect(await this.token.balanceOf(recipient)).to.be.bignumber.equal(initialSupply);
|
||||
});
|
||||
|
||||
it('reverts when trying to transfer when paused', async function () {
|
||||
await this.token.$_pause();
|
||||
|
||||
await expectRevert(
|
||||
this.token.transfer(recipient, initialSupply, { from: holder }),
|
||||
'ERC20Pausable: token transfer while paused',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('transfer from', function () {
|
||||
const allowance = new BN(40);
|
||||
|
||||
beforeEach(async function () {
|
||||
await this.token.approve(anotherAccount, allowance, { from: holder });
|
||||
});
|
||||
|
||||
it('allows to transfer from when unpaused', async function () {
|
||||
await this.token.transferFrom(holder, recipient, allowance, { from: anotherAccount });
|
||||
|
||||
expect(await this.token.balanceOf(recipient)).to.be.bignumber.equal(allowance);
|
||||
expect(await this.token.balanceOf(holder)).to.be.bignumber.equal(initialSupply.sub(allowance));
|
||||
});
|
||||
|
||||
it('allows to transfer when paused and then unpaused', async function () {
|
||||
await this.token.$_pause();
|
||||
await this.token.$_unpause();
|
||||
|
||||
await this.token.transferFrom(holder, recipient, allowance, { from: anotherAccount });
|
||||
|
||||
expect(await this.token.balanceOf(recipient)).to.be.bignumber.equal(allowance);
|
||||
expect(await this.token.balanceOf(holder)).to.be.bignumber.equal(initialSupply.sub(allowance));
|
||||
});
|
||||
|
||||
it('reverts when trying to transfer from when paused', async function () {
|
||||
await this.token.$_pause();
|
||||
|
||||
await expectRevert(
|
||||
this.token.transferFrom(holder, recipient, allowance, { from: anotherAccount }),
|
||||
'ERC20Pausable: token transfer while paused',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('mint', function () {
|
||||
const amount = new BN('42');
|
||||
|
||||
it('allows to mint when unpaused', async function () {
|
||||
await this.token.$_mint(recipient, amount);
|
||||
|
||||
expect(await this.token.balanceOf(recipient)).to.be.bignumber.equal(amount);
|
||||
});
|
||||
|
||||
it('allows to mint when paused and then unpaused', async function () {
|
||||
await this.token.$_pause();
|
||||
await this.token.$_unpause();
|
||||
|
||||
await this.token.$_mint(recipient, amount);
|
||||
|
||||
expect(await this.token.balanceOf(recipient)).to.be.bignumber.equal(amount);
|
||||
});
|
||||
|
||||
it('reverts when trying to mint when paused', async function () {
|
||||
await this.token.$_pause();
|
||||
|
||||
await expectRevert(this.token.$_mint(recipient, amount), 'ERC20Pausable: token transfer while paused');
|
||||
});
|
||||
});
|
||||
|
||||
describe('burn', function () {
|
||||
const amount = new BN('42');
|
||||
|
||||
it('allows to burn when unpaused', async function () {
|
||||
await this.token.$_burn(holder, amount);
|
||||
|
||||
expect(await this.token.balanceOf(holder)).to.be.bignumber.equal(initialSupply.sub(amount));
|
||||
});
|
||||
|
||||
it('allows to burn when paused and then unpaused', async function () {
|
||||
await this.token.$_pause();
|
||||
await this.token.$_unpause();
|
||||
|
||||
await this.token.$_burn(holder, amount);
|
||||
|
||||
expect(await this.token.balanceOf(holder)).to.be.bignumber.equal(initialSupply.sub(amount));
|
||||
});
|
||||
|
||||
it('reverts when trying to burn when paused', async function () {
|
||||
await this.token.$_pause();
|
||||
|
||||
await expectRevert(this.token.$_burn(holder, amount), 'ERC20Pausable: token transfer while paused');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,207 @@
|
||||
const { BN, expectEvent, expectRevert } = require('@openzeppelin/test-helpers');
|
||||
const ERC20Snapshot = artifacts.require('$ERC20Snapshot');
|
||||
|
||||
const { expect } = require('chai');
|
||||
|
||||
contract('ERC20Snapshot', function (accounts) {
|
||||
const [initialHolder, recipient, other] = accounts;
|
||||
|
||||
const initialSupply = new BN(100);
|
||||
|
||||
const name = 'My Token';
|
||||
const symbol = 'MTKN';
|
||||
|
||||
beforeEach(async function () {
|
||||
this.token = await ERC20Snapshot.new(name, symbol);
|
||||
await this.token.$_mint(initialHolder, initialSupply);
|
||||
});
|
||||
|
||||
describe('snapshot', function () {
|
||||
it('emits a snapshot event', async function () {
|
||||
const receipt = await this.token.$_snapshot();
|
||||
expectEvent(receipt, 'Snapshot');
|
||||
});
|
||||
|
||||
it('creates increasing snapshots ids, starting from 1', async function () {
|
||||
for (const id of ['1', '2', '3', '4', '5']) {
|
||||
const receipt = await this.token.$_snapshot();
|
||||
expectEvent(receipt, 'Snapshot', { id });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('totalSupplyAt', function () {
|
||||
it('reverts with a snapshot id of 0', async function () {
|
||||
await expectRevert(this.token.totalSupplyAt(0), 'ERC20Snapshot: id is 0');
|
||||
});
|
||||
|
||||
it('reverts with a not-yet-created snapshot id', async function () {
|
||||
await expectRevert(this.token.totalSupplyAt(1), 'ERC20Snapshot: nonexistent id');
|
||||
});
|
||||
|
||||
context('with initial snapshot', function () {
|
||||
beforeEach(async function () {
|
||||
this.initialSnapshotId = new BN('1');
|
||||
|
||||
const receipt = await this.token.$_snapshot();
|
||||
expectEvent(receipt, 'Snapshot', { id: this.initialSnapshotId });
|
||||
});
|
||||
|
||||
context('with no supply changes after the snapshot', function () {
|
||||
it('returns the current total supply', async function () {
|
||||
expect(await this.token.totalSupplyAt(this.initialSnapshotId)).to.be.bignumber.equal(initialSupply);
|
||||
});
|
||||
});
|
||||
|
||||
context('with supply changes after the snapshot', function () {
|
||||
beforeEach(async function () {
|
||||
await this.token.$_mint(other, new BN('50'));
|
||||
await this.token.$_burn(initialHolder, new BN('20'));
|
||||
});
|
||||
|
||||
it('returns the total supply before the changes', async function () {
|
||||
expect(await this.token.totalSupplyAt(this.initialSnapshotId)).to.be.bignumber.equal(initialSupply);
|
||||
});
|
||||
|
||||
context('with a second snapshot after supply changes', function () {
|
||||
beforeEach(async function () {
|
||||
this.secondSnapshotId = new BN('2');
|
||||
|
||||
const receipt = await this.token.$_snapshot();
|
||||
expectEvent(receipt, 'Snapshot', { id: this.secondSnapshotId });
|
||||
});
|
||||
|
||||
it('snapshots return the supply before and after the changes', async function () {
|
||||
expect(await this.token.totalSupplyAt(this.initialSnapshotId)).to.be.bignumber.equal(initialSupply);
|
||||
|
||||
expect(await this.token.totalSupplyAt(this.secondSnapshotId)).to.be.bignumber.equal(
|
||||
await this.token.totalSupply(),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
context('with multiple snapshots after supply changes', function () {
|
||||
beforeEach(async function () {
|
||||
this.secondSnapshotIds = ['2', '3', '4'];
|
||||
|
||||
for (const id of this.secondSnapshotIds) {
|
||||
const receipt = await this.token.$_snapshot();
|
||||
expectEvent(receipt, 'Snapshot', { id });
|
||||
}
|
||||
});
|
||||
|
||||
it('all posterior snapshots return the supply after the changes', async function () {
|
||||
expect(await this.token.totalSupplyAt(this.initialSnapshotId)).to.be.bignumber.equal(initialSupply);
|
||||
|
||||
const currentSupply = await this.token.totalSupply();
|
||||
|
||||
for (const id of this.secondSnapshotIds) {
|
||||
expect(await this.token.totalSupplyAt(id)).to.be.bignumber.equal(currentSupply);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('balanceOfAt', function () {
|
||||
it('reverts with a snapshot id of 0', async function () {
|
||||
await expectRevert(this.token.balanceOfAt(other, 0), 'ERC20Snapshot: id is 0');
|
||||
});
|
||||
|
||||
it('reverts with a not-yet-created snapshot id', async function () {
|
||||
await expectRevert(this.token.balanceOfAt(other, 1), 'ERC20Snapshot: nonexistent id');
|
||||
});
|
||||
|
||||
context('with initial snapshot', function () {
|
||||
beforeEach(async function () {
|
||||
this.initialSnapshotId = new BN('1');
|
||||
|
||||
const receipt = await this.token.$_snapshot();
|
||||
expectEvent(receipt, 'Snapshot', { id: this.initialSnapshotId });
|
||||
});
|
||||
|
||||
context('with no balance changes after the snapshot', function () {
|
||||
it('returns the current balance for all accounts', async function () {
|
||||
expect(await this.token.balanceOfAt(initialHolder, this.initialSnapshotId)).to.be.bignumber.equal(
|
||||
initialSupply,
|
||||
);
|
||||
expect(await this.token.balanceOfAt(recipient, this.initialSnapshotId)).to.be.bignumber.equal('0');
|
||||
expect(await this.token.balanceOfAt(other, this.initialSnapshotId)).to.be.bignumber.equal('0');
|
||||
});
|
||||
});
|
||||
|
||||
context('with balance changes after the snapshot', function () {
|
||||
beforeEach(async function () {
|
||||
await this.token.transfer(recipient, new BN('10'), { from: initialHolder });
|
||||
await this.token.$_mint(other, new BN('50'));
|
||||
await this.token.$_burn(initialHolder, new BN('20'));
|
||||
});
|
||||
|
||||
it('returns the balances before the changes', async function () {
|
||||
expect(await this.token.balanceOfAt(initialHolder, this.initialSnapshotId)).to.be.bignumber.equal(
|
||||
initialSupply,
|
||||
);
|
||||
expect(await this.token.balanceOfAt(recipient, this.initialSnapshotId)).to.be.bignumber.equal('0');
|
||||
expect(await this.token.balanceOfAt(other, this.initialSnapshotId)).to.be.bignumber.equal('0');
|
||||
});
|
||||
|
||||
context('with a second snapshot after supply changes', function () {
|
||||
beforeEach(async function () {
|
||||
this.secondSnapshotId = new BN('2');
|
||||
|
||||
const receipt = await this.token.$_snapshot();
|
||||
expectEvent(receipt, 'Snapshot', { id: this.secondSnapshotId });
|
||||
});
|
||||
|
||||
it('snapshots return the balances before and after the changes', async function () {
|
||||
expect(await this.token.balanceOfAt(initialHolder, this.initialSnapshotId)).to.be.bignumber.equal(
|
||||
initialSupply,
|
||||
);
|
||||
expect(await this.token.balanceOfAt(recipient, this.initialSnapshotId)).to.be.bignumber.equal('0');
|
||||
expect(await this.token.balanceOfAt(other, this.initialSnapshotId)).to.be.bignumber.equal('0');
|
||||
|
||||
expect(await this.token.balanceOfAt(initialHolder, this.secondSnapshotId)).to.be.bignumber.equal(
|
||||
await this.token.balanceOf(initialHolder),
|
||||
);
|
||||
expect(await this.token.balanceOfAt(recipient, this.secondSnapshotId)).to.be.bignumber.equal(
|
||||
await this.token.balanceOf(recipient),
|
||||
);
|
||||
expect(await this.token.balanceOfAt(other, this.secondSnapshotId)).to.be.bignumber.equal(
|
||||
await this.token.balanceOf(other),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
context('with multiple snapshots after supply changes', function () {
|
||||
beforeEach(async function () {
|
||||
this.secondSnapshotIds = ['2', '3', '4'];
|
||||
|
||||
for (const id of this.secondSnapshotIds) {
|
||||
const receipt = await this.token.$_snapshot();
|
||||
expectEvent(receipt, 'Snapshot', { id });
|
||||
}
|
||||
});
|
||||
|
||||
it('all posterior snapshots return the supply after the changes', async function () {
|
||||
expect(await this.token.balanceOfAt(initialHolder, this.initialSnapshotId)).to.be.bignumber.equal(
|
||||
initialSupply,
|
||||
);
|
||||
expect(await this.token.balanceOfAt(recipient, this.initialSnapshotId)).to.be.bignumber.equal('0');
|
||||
expect(await this.token.balanceOfAt(other, this.initialSnapshotId)).to.be.bignumber.equal('0');
|
||||
|
||||
for (const id of this.secondSnapshotIds) {
|
||||
expect(await this.token.balanceOfAt(initialHolder, id)).to.be.bignumber.equal(
|
||||
await this.token.balanceOf(initialHolder),
|
||||
);
|
||||
expect(await this.token.balanceOfAt(recipient, id)).to.be.bignumber.equal(
|
||||
await this.token.balanceOf(recipient),
|
||||
);
|
||||
expect(await this.token.balanceOfAt(other, id)).to.be.bignumber.equal(await this.token.balanceOf(other));
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,578 @@
|
||||
/* eslint-disable */
|
||||
|
||||
const { BN, constants, expectEvent, expectRevert, time } = require('@openzeppelin/test-helpers');
|
||||
const { expect } = require('chai');
|
||||
const { MAX_UINT256, ZERO_ADDRESS } = constants;
|
||||
|
||||
const { fromRpcSig } = require('ethereumjs-util');
|
||||
const ethSigUtil = require('eth-sig-util');
|
||||
const Wallet = require('ethereumjs-wallet').default;
|
||||
|
||||
const { batchInBlock } = require('../../../helpers/txpool');
|
||||
const { getDomain, domainType, domainSeparator } = require('../../../helpers/eip712');
|
||||
const { clock, clockFromReceipt } = require('../../../helpers/time');
|
||||
|
||||
const { shouldBehaveLikeEIP6372 } = require('../../../governance/utils/EIP6372.behavior');
|
||||
|
||||
const Delegation = [
|
||||
{ name: 'delegatee', type: 'address' },
|
||||
{ name: 'nonce', type: 'uint256' },
|
||||
{ name: 'expiry', type: 'uint256' },
|
||||
];
|
||||
|
||||
const MODES = {
|
||||
blocknumber: artifacts.require('$ERC20Votes'),
|
||||
timestamp: artifacts.require('$ERC20VotesTimestampMock'),
|
||||
};
|
||||
|
||||
contract('ERC20Votes', function (accounts) {
|
||||
const [holder, recipient, holderDelegatee, other1, other2] = accounts;
|
||||
|
||||
const name = 'My Token';
|
||||
const symbol = 'MTKN';
|
||||
const supply = new BN('10000000000000000000000000');
|
||||
|
||||
for (const [mode, artifact] of Object.entries(MODES)) {
|
||||
describe(`vote with ${mode}`, function () {
|
||||
beforeEach(async function () {
|
||||
this.token = await artifact.new(name, symbol, name);
|
||||
});
|
||||
|
||||
shouldBehaveLikeEIP6372(mode);
|
||||
|
||||
it('initial nonce is 0', async function () {
|
||||
expect(await this.token.nonces(holder)).to.be.bignumber.equal('0');
|
||||
});
|
||||
|
||||
it('domain separator', async function () {
|
||||
expect(await this.token.DOMAIN_SEPARATOR()).to.equal(await getDomain(this.token).then(domainSeparator));
|
||||
});
|
||||
|
||||
it('minting restriction', async function () {
|
||||
const amount = new BN('2').pow(new BN('224'));
|
||||
await expectRevert(this.token.$_mint(holder, amount), 'ERC20Votes: total supply risks overflowing votes');
|
||||
});
|
||||
|
||||
it('recent checkpoints', async function () {
|
||||
await this.token.delegate(holder, { from: holder });
|
||||
for (let i = 0; i < 6; i++) {
|
||||
await this.token.$_mint(holder, 1);
|
||||
}
|
||||
const block = await clock[mode]();
|
||||
expect(await this.token.numCheckpoints(holder)).to.be.bignumber.equal('6');
|
||||
// recent
|
||||
expect(await this.token.getPastVotes(holder, block - 1)).to.be.bignumber.equal('5');
|
||||
// non-recent
|
||||
expect(await this.token.getPastVotes(holder, block - 6)).to.be.bignumber.equal('0');
|
||||
});
|
||||
|
||||
describe('set delegation', function () {
|
||||
describe('call', function () {
|
||||
it('delegation with balance', async function () {
|
||||
await this.token.$_mint(holder, supply);
|
||||
expect(await this.token.delegates(holder)).to.be.equal(ZERO_ADDRESS);
|
||||
|
||||
const { receipt } = await this.token.delegate(holder, { from: holder });
|
||||
const timepoint = await clockFromReceipt[mode](receipt);
|
||||
|
||||
expectEvent(receipt, 'DelegateChanged', {
|
||||
delegator: holder,
|
||||
fromDelegate: ZERO_ADDRESS,
|
||||
toDelegate: holder,
|
||||
});
|
||||
expectEvent(receipt, 'DelegateVotesChanged', {
|
||||
delegate: holder,
|
||||
previousBalance: '0',
|
||||
newBalance: supply,
|
||||
});
|
||||
|
||||
expect(await this.token.delegates(holder)).to.be.equal(holder);
|
||||
|
||||
expect(await this.token.getVotes(holder)).to.be.bignumber.equal(supply);
|
||||
expect(await this.token.getPastVotes(holder, timepoint - 1)).to.be.bignumber.equal('0');
|
||||
await time.advanceBlock();
|
||||
expect(await this.token.getPastVotes(holder, timepoint)).to.be.bignumber.equal(supply);
|
||||
});
|
||||
|
||||
it('delegation without balance', async function () {
|
||||
expect(await this.token.delegates(holder)).to.be.equal(ZERO_ADDRESS);
|
||||
|
||||
const { receipt } = await this.token.delegate(holder, { from: holder });
|
||||
expectEvent(receipt, 'DelegateChanged', {
|
||||
delegator: holder,
|
||||
fromDelegate: ZERO_ADDRESS,
|
||||
toDelegate: holder,
|
||||
});
|
||||
expectEvent.notEmitted(receipt, 'DelegateVotesChanged');
|
||||
|
||||
expect(await this.token.delegates(holder)).to.be.equal(holder);
|
||||
});
|
||||
});
|
||||
|
||||
describe('with signature', function () {
|
||||
const delegator = Wallet.generate();
|
||||
const delegatorAddress = web3.utils.toChecksumAddress(delegator.getAddressString());
|
||||
const nonce = 0;
|
||||
|
||||
const buildData = (contract, message) =>
|
||||
getDomain(contract).then(domain => ({
|
||||
primaryType: 'Delegation',
|
||||
types: { EIP712Domain: domainType(domain), Delegation },
|
||||
domain,
|
||||
message,
|
||||
}));
|
||||
|
||||
beforeEach(async function () {
|
||||
await this.token.$_mint(delegatorAddress, supply);
|
||||
});
|
||||
|
||||
it('accept signed delegation', async function () {
|
||||
const { v, r, s } = await buildData(this.token, {
|
||||
delegatee: delegatorAddress,
|
||||
nonce,
|
||||
expiry: MAX_UINT256,
|
||||
}).then(data => fromRpcSig(ethSigUtil.signTypedMessage(delegator.getPrivateKey(), { data })));
|
||||
|
||||
expect(await this.token.delegates(delegatorAddress)).to.be.equal(ZERO_ADDRESS);
|
||||
|
||||
const { receipt } = await this.token.delegateBySig(delegatorAddress, nonce, MAX_UINT256, v, r, s);
|
||||
const timepoint = await clockFromReceipt[mode](receipt);
|
||||
|
||||
expectEvent(receipt, 'DelegateChanged', {
|
||||
delegator: delegatorAddress,
|
||||
fromDelegate: ZERO_ADDRESS,
|
||||
toDelegate: delegatorAddress,
|
||||
});
|
||||
expectEvent(receipt, 'DelegateVotesChanged', {
|
||||
delegate: delegatorAddress,
|
||||
previousBalance: '0',
|
||||
newBalance: supply,
|
||||
});
|
||||
|
||||
expect(await this.token.delegates(delegatorAddress)).to.be.equal(delegatorAddress);
|
||||
|
||||
expect(await this.token.getVotes(delegatorAddress)).to.be.bignumber.equal(supply);
|
||||
expect(await this.token.getPastVotes(delegatorAddress, timepoint - 1)).to.be.bignumber.equal('0');
|
||||
await time.advanceBlock();
|
||||
expect(await this.token.getPastVotes(delegatorAddress, timepoint)).to.be.bignumber.equal(supply);
|
||||
});
|
||||
|
||||
it('rejects reused signature', async function () {
|
||||
const { v, r, s } = await buildData(this.token, {
|
||||
delegatee: delegatorAddress,
|
||||
nonce,
|
||||
expiry: MAX_UINT256,
|
||||
}).then(data => fromRpcSig(ethSigUtil.signTypedMessage(delegator.getPrivateKey(), { data })));
|
||||
|
||||
await this.token.delegateBySig(delegatorAddress, nonce, MAX_UINT256, v, r, s);
|
||||
|
||||
await expectRevert(
|
||||
this.token.delegateBySig(delegatorAddress, nonce, MAX_UINT256, v, r, s),
|
||||
'ERC20Votes: invalid nonce',
|
||||
);
|
||||
});
|
||||
|
||||
it('rejects bad delegatee', async function () {
|
||||
const { v, r, s } = await buildData(this.token, {
|
||||
delegatee: delegatorAddress,
|
||||
nonce,
|
||||
expiry: MAX_UINT256,
|
||||
}).then(data => fromRpcSig(ethSigUtil.signTypedMessage(delegator.getPrivateKey(), { data })));
|
||||
|
||||
const receipt = await this.token.delegateBySig(holderDelegatee, nonce, MAX_UINT256, v, r, s);
|
||||
const { args } = receipt.logs.find(({ event }) => event == 'DelegateChanged');
|
||||
expect(args.delegator).to.not.be.equal(delegatorAddress);
|
||||
expect(args.fromDelegate).to.be.equal(ZERO_ADDRESS);
|
||||
expect(args.toDelegate).to.be.equal(holderDelegatee);
|
||||
});
|
||||
|
||||
it('rejects bad nonce', async function () {
|
||||
const { v, r, s } = await buildData(this.token, {
|
||||
delegatee: delegatorAddress,
|
||||
nonce,
|
||||
expiry: MAX_UINT256,
|
||||
}).then(data => fromRpcSig(ethSigUtil.signTypedMessage(delegator.getPrivateKey(), { data })));
|
||||
|
||||
await expectRevert(
|
||||
this.token.delegateBySig(delegatorAddress, nonce + 1, MAX_UINT256, v, r, s),
|
||||
'ERC20Votes: invalid nonce',
|
||||
);
|
||||
});
|
||||
|
||||
it('rejects expired permit', async function () {
|
||||
const expiry = (await time.latest()) - time.duration.weeks(1);
|
||||
const { v, r, s } = await buildData(this.token, {
|
||||
delegatee: delegatorAddress,
|
||||
nonce,
|
||||
expiry,
|
||||
}).then(data => fromRpcSig(ethSigUtil.signTypedMessage(delegator.getPrivateKey(), { data })));
|
||||
|
||||
await expectRevert(
|
||||
this.token.delegateBySig(delegatorAddress, nonce, expiry, v, r, s),
|
||||
'ERC20Votes: signature expired',
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('change delegation', function () {
|
||||
beforeEach(async function () {
|
||||
await this.token.$_mint(holder, supply);
|
||||
await this.token.delegate(holder, { from: holder });
|
||||
});
|
||||
|
||||
it('call', async function () {
|
||||
expect(await this.token.delegates(holder)).to.be.equal(holder);
|
||||
|
||||
const { receipt } = await this.token.delegate(holderDelegatee, { from: holder });
|
||||
const timepoint = await clockFromReceipt[mode](receipt);
|
||||
|
||||
expectEvent(receipt, 'DelegateChanged', {
|
||||
delegator: holder,
|
||||
fromDelegate: holder,
|
||||
toDelegate: holderDelegatee,
|
||||
});
|
||||
expectEvent(receipt, 'DelegateVotesChanged', {
|
||||
delegate: holder,
|
||||
previousBalance: supply,
|
||||
newBalance: '0',
|
||||
});
|
||||
expectEvent(receipt, 'DelegateVotesChanged', {
|
||||
delegate: holderDelegatee,
|
||||
previousBalance: '0',
|
||||
newBalance: supply,
|
||||
});
|
||||
|
||||
expect(await this.token.delegates(holder)).to.be.equal(holderDelegatee);
|
||||
|
||||
expect(await this.token.getVotes(holder)).to.be.bignumber.equal('0');
|
||||
expect(await this.token.getVotes(holderDelegatee)).to.be.bignumber.equal(supply);
|
||||
expect(await this.token.getPastVotes(holder, timepoint - 1)).to.be.bignumber.equal(supply);
|
||||
expect(await this.token.getPastVotes(holderDelegatee, timepoint - 1)).to.be.bignumber.equal('0');
|
||||
await time.advanceBlock();
|
||||
expect(await this.token.getPastVotes(holder, timepoint)).to.be.bignumber.equal('0');
|
||||
expect(await this.token.getPastVotes(holderDelegatee, timepoint)).to.be.bignumber.equal(supply);
|
||||
});
|
||||
});
|
||||
|
||||
describe('transfers', function () {
|
||||
beforeEach(async function () {
|
||||
await this.token.$_mint(holder, supply);
|
||||
});
|
||||
|
||||
it('no delegation', async function () {
|
||||
const { receipt } = await this.token.transfer(recipient, 1, { from: holder });
|
||||
expectEvent(receipt, 'Transfer', { from: holder, to: recipient, value: '1' });
|
||||
expectEvent.notEmitted(receipt, 'DelegateVotesChanged');
|
||||
|
||||
this.holderVotes = '0';
|
||||
this.recipientVotes = '0';
|
||||
});
|
||||
|
||||
it('sender delegation', async function () {
|
||||
await this.token.delegate(holder, { from: holder });
|
||||
|
||||
const { receipt } = await this.token.transfer(recipient, 1, { from: holder });
|
||||
expectEvent(receipt, 'Transfer', { from: holder, to: recipient, value: '1' });
|
||||
expectEvent(receipt, 'DelegateVotesChanged', {
|
||||
delegate: holder,
|
||||
previousBalance: supply,
|
||||
newBalance: supply.subn(1),
|
||||
});
|
||||
|
||||
const { logIndex: transferLogIndex } = receipt.logs.find(({ event }) => event == 'Transfer');
|
||||
expect(
|
||||
receipt.logs
|
||||
.filter(({ event }) => event == 'DelegateVotesChanged')
|
||||
.every(({ logIndex }) => transferLogIndex < logIndex),
|
||||
).to.be.equal(true);
|
||||
|
||||
this.holderVotes = supply.subn(1);
|
||||
this.recipientVotes = '0';
|
||||
});
|
||||
|
||||
it('receiver delegation', async function () {
|
||||
await this.token.delegate(recipient, { from: recipient });
|
||||
|
||||
const { receipt } = await this.token.transfer(recipient, 1, { from: holder });
|
||||
expectEvent(receipt, 'Transfer', { from: holder, to: recipient, value: '1' });
|
||||
expectEvent(receipt, 'DelegateVotesChanged', { delegate: recipient, previousBalance: '0', newBalance: '1' });
|
||||
|
||||
const { logIndex: transferLogIndex } = receipt.logs.find(({ event }) => event == 'Transfer');
|
||||
expect(
|
||||
receipt.logs
|
||||
.filter(({ event }) => event == 'DelegateVotesChanged')
|
||||
.every(({ logIndex }) => transferLogIndex < logIndex),
|
||||
).to.be.equal(true);
|
||||
|
||||
this.holderVotes = '0';
|
||||
this.recipientVotes = '1';
|
||||
});
|
||||
|
||||
it('full delegation', async function () {
|
||||
await this.token.delegate(holder, { from: holder });
|
||||
await this.token.delegate(recipient, { from: recipient });
|
||||
|
||||
const { receipt } = await this.token.transfer(recipient, 1, { from: holder });
|
||||
expectEvent(receipt, 'Transfer', { from: holder, to: recipient, value: '1' });
|
||||
expectEvent(receipt, 'DelegateVotesChanged', {
|
||||
delegate: holder,
|
||||
previousBalance: supply,
|
||||
newBalance: supply.subn(1),
|
||||
});
|
||||
expectEvent(receipt, 'DelegateVotesChanged', { delegate: recipient, previousBalance: '0', newBalance: '1' });
|
||||
|
||||
const { logIndex: transferLogIndex } = receipt.logs.find(({ event }) => event == 'Transfer');
|
||||
expect(
|
||||
receipt.logs
|
||||
.filter(({ event }) => event == 'DelegateVotesChanged')
|
||||
.every(({ logIndex }) => transferLogIndex < logIndex),
|
||||
).to.be.equal(true);
|
||||
|
||||
this.holderVotes = supply.subn(1);
|
||||
this.recipientVotes = '1';
|
||||
});
|
||||
|
||||
afterEach(async function () {
|
||||
expect(await this.token.getVotes(holder)).to.be.bignumber.equal(this.holderVotes);
|
||||
expect(await this.token.getVotes(recipient)).to.be.bignumber.equal(this.recipientVotes);
|
||||
|
||||
// need to advance 2 blocks to see the effect of a transfer on "getPastVotes"
|
||||
const timepoint = await clock[mode]();
|
||||
await time.advanceBlock();
|
||||
expect(await this.token.getPastVotes(holder, timepoint)).to.be.bignumber.equal(this.holderVotes);
|
||||
expect(await this.token.getPastVotes(recipient, timepoint)).to.be.bignumber.equal(this.recipientVotes);
|
||||
});
|
||||
});
|
||||
|
||||
// The following tests are a adaptation of https://github.com/compound-finance/compound-protocol/blob/master/tests/Governance/CompTest.js.
|
||||
describe('Compound test suite', function () {
|
||||
beforeEach(async function () {
|
||||
await this.token.$_mint(holder, supply);
|
||||
});
|
||||
|
||||
describe('balanceOf', function () {
|
||||
it('grants to initial account', async function () {
|
||||
expect(await this.token.balanceOf(holder)).to.be.bignumber.equal('10000000000000000000000000');
|
||||
});
|
||||
});
|
||||
|
||||
describe('numCheckpoints', function () {
|
||||
it('returns the number of checkpoints for a delegate', async function () {
|
||||
await this.token.transfer(recipient, '100', { from: holder }); //give an account a few tokens for readability
|
||||
expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('0');
|
||||
|
||||
const t1 = await this.token.delegate(other1, { from: recipient });
|
||||
t1.timepoint = await clockFromReceipt[mode](t1.receipt);
|
||||
expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('1');
|
||||
|
||||
const t2 = await this.token.transfer(other2, 10, { from: recipient });
|
||||
t2.timepoint = await clockFromReceipt[mode](t2.receipt);
|
||||
expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('2');
|
||||
|
||||
const t3 = await this.token.transfer(other2, 10, { from: recipient });
|
||||
t3.timepoint = await clockFromReceipt[mode](t3.receipt);
|
||||
expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('3');
|
||||
|
||||
const t4 = await this.token.transfer(recipient, 20, { from: holder });
|
||||
t4.timepoint = await clockFromReceipt[mode](t4.receipt);
|
||||
expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('4');
|
||||
|
||||
expect(await this.token.checkpoints(other1, 0)).to.be.deep.equal([t1.timepoint.toString(), '100']);
|
||||
expect(await this.token.checkpoints(other1, 1)).to.be.deep.equal([t2.timepoint.toString(), '90']);
|
||||
expect(await this.token.checkpoints(other1, 2)).to.be.deep.equal([t3.timepoint.toString(), '80']);
|
||||
expect(await this.token.checkpoints(other1, 3)).to.be.deep.equal([t4.timepoint.toString(), '100']);
|
||||
|
||||
await time.advanceBlock();
|
||||
expect(await this.token.getPastVotes(other1, t1.timepoint)).to.be.bignumber.equal('100');
|
||||
expect(await this.token.getPastVotes(other1, t2.timepoint)).to.be.bignumber.equal('90');
|
||||
expect(await this.token.getPastVotes(other1, t3.timepoint)).to.be.bignumber.equal('80');
|
||||
expect(await this.token.getPastVotes(other1, t4.timepoint)).to.be.bignumber.equal('100');
|
||||
});
|
||||
|
||||
it('does not add more than one checkpoint in a block', async function () {
|
||||
await this.token.transfer(recipient, '100', { from: holder });
|
||||
expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('0');
|
||||
|
||||
const [t1, t2, t3] = await batchInBlock([
|
||||
() => this.token.delegate(other1, { from: recipient, gas: 100000 }),
|
||||
() => this.token.transfer(other2, 10, { from: recipient, gas: 100000 }),
|
||||
() => this.token.transfer(other2, 10, { from: recipient, gas: 100000 }),
|
||||
]);
|
||||
t1.timepoint = await clockFromReceipt[mode](t1.receipt);
|
||||
t2.timepoint = await clockFromReceipt[mode](t2.receipt);
|
||||
t3.timepoint = await clockFromReceipt[mode](t3.receipt);
|
||||
|
||||
expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('1');
|
||||
expect(await this.token.checkpoints(other1, 0)).to.be.deep.equal([t1.timepoint.toString(), '80']);
|
||||
|
||||
const t4 = await this.token.transfer(recipient, 20, { from: holder });
|
||||
t4.timepoint = await clockFromReceipt[mode](t4.receipt);
|
||||
|
||||
expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('2');
|
||||
expect(await this.token.checkpoints(other1, 1)).to.be.deep.equal([t4.timepoint.toString(), '100']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getPastVotes', function () {
|
||||
it('reverts if block number >= current block', async function () {
|
||||
await expectRevert(this.token.getPastVotes(other1, 5e10), 'ERC20Votes: future lookup');
|
||||
});
|
||||
|
||||
it('returns 0 if there are no checkpoints', async function () {
|
||||
expect(await this.token.getPastVotes(other1, 0)).to.be.bignumber.equal('0');
|
||||
});
|
||||
|
||||
it('returns the latest block if >= last checkpoint block', async function () {
|
||||
const { receipt } = await this.token.delegate(other1, { from: holder });
|
||||
const timepoint = await clockFromReceipt[mode](receipt);
|
||||
await time.advanceBlock();
|
||||
await time.advanceBlock();
|
||||
|
||||
expect(await this.token.getPastVotes(other1, timepoint)).to.be.bignumber.equal(
|
||||
'10000000000000000000000000',
|
||||
);
|
||||
expect(await this.token.getPastVotes(other1, timepoint + 1)).to.be.bignumber.equal(
|
||||
'10000000000000000000000000',
|
||||
);
|
||||
});
|
||||
|
||||
it('returns zero if < first checkpoint block', async function () {
|
||||
await time.advanceBlock();
|
||||
const { receipt } = await this.token.delegate(other1, { from: holder });
|
||||
const timepoint = await clockFromReceipt[mode](receipt);
|
||||
await time.advanceBlock();
|
||||
await time.advanceBlock();
|
||||
|
||||
expect(await this.token.getPastVotes(other1, timepoint - 1)).to.be.bignumber.equal('0');
|
||||
expect(await this.token.getPastVotes(other1, timepoint + 1)).to.be.bignumber.equal(
|
||||
'10000000000000000000000000',
|
||||
);
|
||||
});
|
||||
|
||||
it('generally returns the voting balance at the appropriate checkpoint', async function () {
|
||||
const t1 = await this.token.delegate(other1, { from: holder });
|
||||
await time.advanceBlock();
|
||||
await time.advanceBlock();
|
||||
const t2 = await this.token.transfer(other2, 10, { from: holder });
|
||||
await time.advanceBlock();
|
||||
await time.advanceBlock();
|
||||
const t3 = await this.token.transfer(other2, 10, { from: holder });
|
||||
await time.advanceBlock();
|
||||
await time.advanceBlock();
|
||||
const t4 = await this.token.transfer(holder, 20, { from: other2 });
|
||||
await time.advanceBlock();
|
||||
await time.advanceBlock();
|
||||
|
||||
t1.timepoint = await clockFromReceipt[mode](t1.receipt);
|
||||
t2.timepoint = await clockFromReceipt[mode](t2.receipt);
|
||||
t3.timepoint = await clockFromReceipt[mode](t3.receipt);
|
||||
t4.timepoint = await clockFromReceipt[mode](t4.receipt);
|
||||
|
||||
expect(await this.token.getPastVotes(other1, t1.timepoint - 1)).to.be.bignumber.equal('0');
|
||||
expect(await this.token.getPastVotes(other1, t1.timepoint)).to.be.bignumber.equal(
|
||||
'10000000000000000000000000',
|
||||
);
|
||||
expect(await this.token.getPastVotes(other1, t1.timepoint + 1)).to.be.bignumber.equal(
|
||||
'10000000000000000000000000',
|
||||
);
|
||||
expect(await this.token.getPastVotes(other1, t2.timepoint)).to.be.bignumber.equal(
|
||||
'9999999999999999999999990',
|
||||
);
|
||||
expect(await this.token.getPastVotes(other1, t2.timepoint + 1)).to.be.bignumber.equal(
|
||||
'9999999999999999999999990',
|
||||
);
|
||||
expect(await this.token.getPastVotes(other1, t3.timepoint)).to.be.bignumber.equal(
|
||||
'9999999999999999999999980',
|
||||
);
|
||||
expect(await this.token.getPastVotes(other1, t3.timepoint + 1)).to.be.bignumber.equal(
|
||||
'9999999999999999999999980',
|
||||
);
|
||||
expect(await this.token.getPastVotes(other1, t4.timepoint)).to.be.bignumber.equal(
|
||||
'10000000000000000000000000',
|
||||
);
|
||||
expect(await this.token.getPastVotes(other1, t4.timepoint + 1)).to.be.bignumber.equal(
|
||||
'10000000000000000000000000',
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('getPastTotalSupply', function () {
|
||||
beforeEach(async function () {
|
||||
await this.token.delegate(holder, { from: holder });
|
||||
});
|
||||
|
||||
it('reverts if block number >= current block', async function () {
|
||||
await expectRevert(this.token.getPastTotalSupply(5e10), 'ERC20Votes: future lookup');
|
||||
});
|
||||
|
||||
it('returns 0 if there are no checkpoints', async function () {
|
||||
expect(await this.token.getPastTotalSupply(0)).to.be.bignumber.equal('0');
|
||||
});
|
||||
|
||||
it('returns the latest block if >= last checkpoint block', async function () {
|
||||
const { receipt } = await this.token.$_mint(holder, supply);
|
||||
const timepoint = await clockFromReceipt[mode](receipt);
|
||||
|
||||
await time.advanceBlock();
|
||||
await time.advanceBlock();
|
||||
|
||||
expect(await this.token.getPastTotalSupply(timepoint)).to.be.bignumber.equal(supply);
|
||||
expect(await this.token.getPastTotalSupply(timepoint + 1)).to.be.bignumber.equal(supply);
|
||||
});
|
||||
|
||||
it('returns zero if < first checkpoint block', async function () {
|
||||
await time.advanceBlock();
|
||||
const { receipt } = await this.token.$_mint(holder, supply);
|
||||
const timepoint = await clockFromReceipt[mode](receipt);
|
||||
await time.advanceBlock();
|
||||
await time.advanceBlock();
|
||||
|
||||
expect(await this.token.getPastTotalSupply(timepoint - 1)).to.be.bignumber.equal('0');
|
||||
expect(await this.token.getPastTotalSupply(timepoint + 1)).to.be.bignumber.equal(
|
||||
'10000000000000000000000000',
|
||||
);
|
||||
});
|
||||
|
||||
it('generally returns the voting balance at the appropriate checkpoint', async function () {
|
||||
const t1 = await this.token.$_mint(holder, supply);
|
||||
await time.advanceBlock();
|
||||
await time.advanceBlock();
|
||||
const t2 = await this.token.$_burn(holder, 10);
|
||||
await time.advanceBlock();
|
||||
await time.advanceBlock();
|
||||
const t3 = await this.token.$_burn(holder, 10);
|
||||
await time.advanceBlock();
|
||||
await time.advanceBlock();
|
||||
const t4 = await this.token.$_mint(holder, 20);
|
||||
await time.advanceBlock();
|
||||
await time.advanceBlock();
|
||||
|
||||
t1.timepoint = await clockFromReceipt[mode](t1.receipt);
|
||||
t2.timepoint = await clockFromReceipt[mode](t2.receipt);
|
||||
t3.timepoint = await clockFromReceipt[mode](t3.receipt);
|
||||
t4.timepoint = await clockFromReceipt[mode](t4.receipt);
|
||||
|
||||
expect(await this.token.getPastTotalSupply(t1.timepoint - 1)).to.be.bignumber.equal('0');
|
||||
expect(await this.token.getPastTotalSupply(t1.timepoint)).to.be.bignumber.equal('10000000000000000000000000');
|
||||
expect(await this.token.getPastTotalSupply(t1.timepoint + 1)).to.be.bignumber.equal(
|
||||
'10000000000000000000000000',
|
||||
);
|
||||
expect(await this.token.getPastTotalSupply(t2.timepoint)).to.be.bignumber.equal('9999999999999999999999990');
|
||||
expect(await this.token.getPastTotalSupply(t2.timepoint + 1)).to.be.bignumber.equal(
|
||||
'9999999999999999999999990',
|
||||
);
|
||||
expect(await this.token.getPastTotalSupply(t3.timepoint)).to.be.bignumber.equal('9999999999999999999999980');
|
||||
expect(await this.token.getPastTotalSupply(t3.timepoint + 1)).to.be.bignumber.equal(
|
||||
'9999999999999999999999980',
|
||||
);
|
||||
expect(await this.token.getPastTotalSupply(t4.timepoint)).to.be.bignumber.equal('10000000000000000000000000');
|
||||
expect(await this.token.getPastTotalSupply(t4.timepoint + 1)).to.be.bignumber.equal(
|
||||
'10000000000000000000000000',
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
+543
@@ -0,0 +1,543 @@
|
||||
/* eslint-disable */
|
||||
|
||||
const { BN, constants, expectEvent, expectRevert, time } = require('@openzeppelin/test-helpers');
|
||||
const { expect } = require('chai');
|
||||
const { MAX_UINT256, ZERO_ADDRESS } = constants;
|
||||
|
||||
const { fromRpcSig } = require('ethereumjs-util');
|
||||
const ethSigUtil = require('eth-sig-util');
|
||||
const Wallet = require('ethereumjs-wallet').default;
|
||||
|
||||
const { batchInBlock } = require('../../../helpers/txpool');
|
||||
const { getDomain, domainType, domainSeparator } = require('../../../helpers/eip712');
|
||||
const { clock, clockFromReceipt } = require('../../../helpers/time');
|
||||
|
||||
const { shouldBehaveLikeEIP6372 } = require('../../../governance/utils/EIP6372.behavior');
|
||||
|
||||
const Delegation = [
|
||||
{ name: 'delegatee', type: 'address' },
|
||||
{ name: 'nonce', type: 'uint256' },
|
||||
{ name: 'expiry', type: 'uint256' },
|
||||
];
|
||||
|
||||
const MODES = {
|
||||
blocknumber: artifacts.require('$ERC20VotesComp'),
|
||||
// no timestamp mode for ERC20VotesComp yet
|
||||
};
|
||||
|
||||
contract('ERC20VotesComp', function (accounts) {
|
||||
const [holder, recipient, holderDelegatee, other1, other2] = accounts;
|
||||
|
||||
const name = 'My Token';
|
||||
const symbol = 'MTKN';
|
||||
const supply = new BN('10000000000000000000000000');
|
||||
|
||||
for (const [mode, artifact] of Object.entries(MODES)) {
|
||||
describe(`vote with ${mode}`, function () {
|
||||
beforeEach(async function () {
|
||||
this.token = await artifact.new(name, symbol, name);
|
||||
});
|
||||
|
||||
shouldBehaveLikeEIP6372(mode);
|
||||
|
||||
it('initial nonce is 0', async function () {
|
||||
expect(await this.token.nonces(holder)).to.be.bignumber.equal('0');
|
||||
});
|
||||
|
||||
it('domain separator', async function () {
|
||||
expect(await this.token.DOMAIN_SEPARATOR()).to.equal(await getDomain(this.token).then(domainSeparator));
|
||||
});
|
||||
|
||||
it('minting restriction', async function () {
|
||||
const amount = new BN('2').pow(new BN('96'));
|
||||
await expectRevert(this.token.$_mint(holder, amount), 'ERC20Votes: total supply risks overflowing votes');
|
||||
});
|
||||
|
||||
describe('set delegation', function () {
|
||||
describe('call', function () {
|
||||
it('delegation with balance', async function () {
|
||||
await this.token.$_mint(holder, supply);
|
||||
expect(await this.token.delegates(holder)).to.be.equal(ZERO_ADDRESS);
|
||||
|
||||
const { receipt } = await this.token.delegate(holder, { from: holder });
|
||||
const timepoint = await clockFromReceipt[mode](receipt);
|
||||
|
||||
expectEvent(receipt, 'DelegateChanged', {
|
||||
delegator: holder,
|
||||
fromDelegate: ZERO_ADDRESS,
|
||||
toDelegate: holder,
|
||||
});
|
||||
expectEvent(receipt, 'DelegateVotesChanged', {
|
||||
delegate: holder,
|
||||
previousBalance: '0',
|
||||
newBalance: supply,
|
||||
});
|
||||
|
||||
expect(await this.token.delegates(holder)).to.be.equal(holder);
|
||||
|
||||
expect(await this.token.getCurrentVotes(holder)).to.be.bignumber.equal(supply);
|
||||
expect(await this.token.getPriorVotes(holder, timepoint - 1)).to.be.bignumber.equal('0');
|
||||
await time.advanceBlock();
|
||||
expect(await this.token.getPriorVotes(holder, timepoint)).to.be.bignumber.equal(supply);
|
||||
});
|
||||
|
||||
it('delegation without balance', async function () {
|
||||
expect(await this.token.delegates(holder)).to.be.equal(ZERO_ADDRESS);
|
||||
|
||||
const { receipt } = await this.token.delegate(holder, { from: holder });
|
||||
expectEvent(receipt, 'DelegateChanged', {
|
||||
delegator: holder,
|
||||
fromDelegate: ZERO_ADDRESS,
|
||||
toDelegate: holder,
|
||||
});
|
||||
expectEvent.notEmitted(receipt, 'DelegateVotesChanged');
|
||||
|
||||
expect(await this.token.delegates(holder)).to.be.equal(holder);
|
||||
});
|
||||
});
|
||||
|
||||
describe('with signature', function () {
|
||||
const delegator = Wallet.generate();
|
||||
const delegatorAddress = web3.utils.toChecksumAddress(delegator.getAddressString());
|
||||
const nonce = 0;
|
||||
|
||||
const buildData = (contract, message) =>
|
||||
getDomain(contract).then(domain => ({
|
||||
primaryType: 'Delegation',
|
||||
types: { EIP712Domain: domainType(domain), Delegation },
|
||||
domain,
|
||||
message,
|
||||
}));
|
||||
|
||||
beforeEach(async function () {
|
||||
await this.token.$_mint(delegatorAddress, supply);
|
||||
});
|
||||
|
||||
it('accept signed delegation', async function () {
|
||||
const { v, r, s } = await buildData(this.token, {
|
||||
delegatee: delegatorAddress,
|
||||
nonce,
|
||||
expiry: MAX_UINT256,
|
||||
}).then(data => fromRpcSig(ethSigUtil.signTypedMessage(delegator.getPrivateKey(), { data })));
|
||||
|
||||
expect(await this.token.delegates(delegatorAddress)).to.be.equal(ZERO_ADDRESS);
|
||||
|
||||
const { receipt } = await this.token.delegateBySig(delegatorAddress, nonce, MAX_UINT256, v, r, s);
|
||||
const timepoint = await clockFromReceipt[mode](receipt);
|
||||
|
||||
expectEvent(receipt, 'DelegateChanged', {
|
||||
delegator: delegatorAddress,
|
||||
fromDelegate: ZERO_ADDRESS,
|
||||
toDelegate: delegatorAddress,
|
||||
});
|
||||
expectEvent(receipt, 'DelegateVotesChanged', {
|
||||
delegate: delegatorAddress,
|
||||
previousBalance: '0',
|
||||
newBalance: supply,
|
||||
});
|
||||
|
||||
expect(await this.token.delegates(delegatorAddress)).to.be.equal(delegatorAddress);
|
||||
|
||||
expect(await this.token.getCurrentVotes(delegatorAddress)).to.be.bignumber.equal(supply);
|
||||
expect(await this.token.getPriorVotes(delegatorAddress, timepoint - 1)).to.be.bignumber.equal('0');
|
||||
await time.advanceBlock();
|
||||
expect(await this.token.getPriorVotes(delegatorAddress, timepoint)).to.be.bignumber.equal(supply);
|
||||
});
|
||||
|
||||
it('rejects reused signature', async function () {
|
||||
const { v, r, s } = await buildData(this.token, {
|
||||
delegatee: delegatorAddress,
|
||||
nonce,
|
||||
expiry: MAX_UINT256,
|
||||
}).then(data => fromRpcSig(ethSigUtil.signTypedMessage(delegator.getPrivateKey(), { data })));
|
||||
|
||||
await this.token.delegateBySig(delegatorAddress, nonce, MAX_UINT256, v, r, s);
|
||||
|
||||
await expectRevert(
|
||||
this.token.delegateBySig(delegatorAddress, nonce, MAX_UINT256, v, r, s),
|
||||
'ERC20Votes: invalid nonce',
|
||||
);
|
||||
});
|
||||
|
||||
it('rejects bad delegatee', async function () {
|
||||
const { v, r, s } = await buildData(this.token, {
|
||||
delegatee: delegatorAddress,
|
||||
nonce,
|
||||
expiry: MAX_UINT256,
|
||||
}).then(data => fromRpcSig(ethSigUtil.signTypedMessage(delegator.getPrivateKey(), { data })));
|
||||
|
||||
const receipt = await this.token.delegateBySig(holderDelegatee, nonce, MAX_UINT256, v, r, s);
|
||||
const { args } = receipt.logs.find(({ event }) => event == 'DelegateChanged');
|
||||
expect(args.delegator).to.not.be.equal(delegatorAddress);
|
||||
expect(args.fromDelegate).to.be.equal(ZERO_ADDRESS);
|
||||
expect(args.toDelegate).to.be.equal(holderDelegatee);
|
||||
});
|
||||
|
||||
it('rejects bad nonce', async function () {
|
||||
const { v, r, s } = await buildData(this.token, {
|
||||
delegatee: delegatorAddress,
|
||||
nonce,
|
||||
expiry: MAX_UINT256,
|
||||
}).then(data => fromRpcSig(ethSigUtil.signTypedMessage(delegator.getPrivateKey(), { data })));
|
||||
|
||||
await expectRevert(
|
||||
this.token.delegateBySig(delegatorAddress, nonce + 1, MAX_UINT256, v, r, s),
|
||||
'ERC20Votes: invalid nonce',
|
||||
);
|
||||
});
|
||||
|
||||
it('rejects expired permit', async function () {
|
||||
const expiry = (await time.latest()) - time.duration.weeks(1);
|
||||
const { v, r, s } = await buildData(this.token, {
|
||||
delegatee: delegatorAddress,
|
||||
nonce,
|
||||
expiry,
|
||||
}).then(data => fromRpcSig(ethSigUtil.signTypedMessage(delegator.getPrivateKey(), { data })));
|
||||
|
||||
await expectRevert(
|
||||
this.token.delegateBySig(delegatorAddress, nonce, expiry, v, r, s),
|
||||
'ERC20Votes: signature expired',
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('change delegation', function () {
|
||||
beforeEach(async function () {
|
||||
await this.token.$_mint(holder, supply);
|
||||
await this.token.delegate(holder, { from: holder });
|
||||
});
|
||||
|
||||
it('call', async function () {
|
||||
expect(await this.token.delegates(holder)).to.be.equal(holder);
|
||||
|
||||
const { receipt } = await this.token.delegate(holderDelegatee, { from: holder });
|
||||
const timepoint = await clockFromReceipt[mode](receipt);
|
||||
|
||||
expectEvent(receipt, 'DelegateChanged', {
|
||||
delegator: holder,
|
||||
fromDelegate: holder,
|
||||
toDelegate: holderDelegatee,
|
||||
});
|
||||
expectEvent(receipt, 'DelegateVotesChanged', {
|
||||
delegate: holder,
|
||||
previousBalance: supply,
|
||||
newBalance: '0',
|
||||
});
|
||||
expectEvent(receipt, 'DelegateVotesChanged', {
|
||||
delegate: holderDelegatee,
|
||||
previousBalance: '0',
|
||||
newBalance: supply,
|
||||
});
|
||||
|
||||
expect(await this.token.delegates(holder)).to.be.equal(holderDelegatee);
|
||||
|
||||
expect(await this.token.getCurrentVotes(holder)).to.be.bignumber.equal('0');
|
||||
expect(await this.token.getCurrentVotes(holderDelegatee)).to.be.bignumber.equal(supply);
|
||||
expect(await this.token.getPriorVotes(holder, timepoint - 1)).to.be.bignumber.equal(supply);
|
||||
expect(await this.token.getPriorVotes(holderDelegatee, timepoint - 1)).to.be.bignumber.equal('0');
|
||||
await time.advanceBlock();
|
||||
expect(await this.token.getPriorVotes(holder, timepoint)).to.be.bignumber.equal('0');
|
||||
expect(await this.token.getPriorVotes(holderDelegatee, timepoint)).to.be.bignumber.equal(supply);
|
||||
});
|
||||
});
|
||||
|
||||
describe('transfers', function () {
|
||||
beforeEach(async function () {
|
||||
await this.token.$_mint(holder, supply);
|
||||
});
|
||||
|
||||
it('no delegation', async function () {
|
||||
const { receipt } = await this.token.transfer(recipient, 1, { from: holder });
|
||||
expectEvent(receipt, 'Transfer', { from: holder, to: recipient, value: '1' });
|
||||
expectEvent.notEmitted(receipt, 'DelegateVotesChanged');
|
||||
|
||||
this.holderVotes = '0';
|
||||
this.recipientVotes = '0';
|
||||
});
|
||||
|
||||
it('sender delegation', async function () {
|
||||
await this.token.delegate(holder, { from: holder });
|
||||
|
||||
const { receipt } = await this.token.transfer(recipient, 1, { from: holder });
|
||||
expectEvent(receipt, 'Transfer', { from: holder, to: recipient, value: '1' });
|
||||
expectEvent(receipt, 'DelegateVotesChanged', {
|
||||
delegate: holder,
|
||||
previousBalance: supply,
|
||||
newBalance: supply.subn(1),
|
||||
});
|
||||
|
||||
this.holderVotes = supply.subn(1);
|
||||
this.recipientVotes = '0';
|
||||
});
|
||||
|
||||
it('receiver delegation', async function () {
|
||||
await this.token.delegate(recipient, { from: recipient });
|
||||
|
||||
const { receipt } = await this.token.transfer(recipient, 1, { from: holder });
|
||||
expectEvent(receipt, 'Transfer', { from: holder, to: recipient, value: '1' });
|
||||
expectEvent(receipt, 'DelegateVotesChanged', { delegate: recipient, previousBalance: '0', newBalance: '1' });
|
||||
|
||||
this.holderVotes = '0';
|
||||
this.recipientVotes = '1';
|
||||
});
|
||||
|
||||
it('full delegation', async function () {
|
||||
await this.token.delegate(holder, { from: holder });
|
||||
await this.token.delegate(recipient, { from: recipient });
|
||||
|
||||
const { receipt } = await this.token.transfer(recipient, 1, { from: holder });
|
||||
expectEvent(receipt, 'Transfer', { from: holder, to: recipient, value: '1' });
|
||||
expectEvent(receipt, 'DelegateVotesChanged', {
|
||||
delegate: holder,
|
||||
previousBalance: supply,
|
||||
newBalance: supply.subn(1),
|
||||
});
|
||||
expectEvent(receipt, 'DelegateVotesChanged', { delegate: recipient, previousBalance: '0', newBalance: '1' });
|
||||
|
||||
this.holderVotes = supply.subn(1);
|
||||
this.recipientVotes = '1';
|
||||
});
|
||||
|
||||
afterEach(async function () {
|
||||
expect(await this.token.getCurrentVotes(holder)).to.be.bignumber.equal(this.holderVotes);
|
||||
expect(await this.token.getCurrentVotes(recipient)).to.be.bignumber.equal(this.recipientVotes);
|
||||
|
||||
// need to advance 2 blocks to see the effect of a transfer on "getPriorVotes"
|
||||
const timepoint = await clock[mode]();
|
||||
await time.advanceBlock();
|
||||
expect(await this.token.getPriorVotes(holder, timepoint)).to.be.bignumber.equal(this.holderVotes);
|
||||
expect(await this.token.getPriorVotes(recipient, timepoint)).to.be.bignumber.equal(this.recipientVotes);
|
||||
});
|
||||
});
|
||||
|
||||
// The following tests are a adaptation of https://github.com/compound-finance/compound-protocol/blob/master/tests/Governance/CompTest.js.
|
||||
describe('Compound test suite', function () {
|
||||
beforeEach(async function () {
|
||||
await this.token.$_mint(holder, supply);
|
||||
});
|
||||
|
||||
describe('balanceOf', function () {
|
||||
it('grants to initial account', async function () {
|
||||
expect(await this.token.balanceOf(holder)).to.be.bignumber.equal('10000000000000000000000000');
|
||||
});
|
||||
});
|
||||
|
||||
describe('numCheckpoints', function () {
|
||||
it('returns the number of checkpoints for a delegate', async function () {
|
||||
await this.token.transfer(recipient, '100', { from: holder }); //give an account a few tokens for readability
|
||||
expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('0');
|
||||
|
||||
const t1 = await this.token.delegate(other1, { from: recipient });
|
||||
t1.timepoint = await clockFromReceipt[mode](t1.receipt);
|
||||
expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('1');
|
||||
|
||||
const t2 = await this.token.transfer(other2, 10, { from: recipient });
|
||||
t2.timepoint = await clockFromReceipt[mode](t2.receipt);
|
||||
expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('2');
|
||||
|
||||
const t3 = await this.token.transfer(other2, 10, { from: recipient });
|
||||
t3.timepoint = await clockFromReceipt[mode](t3.receipt);
|
||||
expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('3');
|
||||
|
||||
const t4 = await this.token.transfer(recipient, 20, { from: holder });
|
||||
t4.timepoint = await clockFromReceipt[mode](t4.receipt);
|
||||
expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('4');
|
||||
|
||||
expect(await this.token.checkpoints(other1, 0)).to.be.deep.equal([t1.timepoint.toString(), '100']);
|
||||
expect(await this.token.checkpoints(other1, 1)).to.be.deep.equal([t2.timepoint.toString(), '90']);
|
||||
expect(await this.token.checkpoints(other1, 2)).to.be.deep.equal([t3.timepoint.toString(), '80']);
|
||||
expect(await this.token.checkpoints(other1, 3)).to.be.deep.equal([t4.timepoint.toString(), '100']);
|
||||
|
||||
await time.advanceBlock();
|
||||
expect(await this.token.getPriorVotes(other1, t1.timepoint)).to.be.bignumber.equal('100');
|
||||
expect(await this.token.getPriorVotes(other1, t2.timepoint)).to.be.bignumber.equal('90');
|
||||
expect(await this.token.getPriorVotes(other1, t3.timepoint)).to.be.bignumber.equal('80');
|
||||
expect(await this.token.getPriorVotes(other1, t4.timepoint)).to.be.bignumber.equal('100');
|
||||
});
|
||||
|
||||
it('does not add more than one checkpoint in a block', async function () {
|
||||
await this.token.transfer(recipient, '100', { from: holder });
|
||||
expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('0');
|
||||
|
||||
const [t1, t2, t3] = await batchInBlock([
|
||||
() => this.token.delegate(other1, { from: recipient, gas: 100000 }),
|
||||
() => this.token.transfer(other2, 10, { from: recipient, gas: 100000 }),
|
||||
() => this.token.transfer(other2, 10, { from: recipient, gas: 100000 }),
|
||||
]);
|
||||
t1.timepoint = await clockFromReceipt[mode](t1.receipt);
|
||||
t2.timepoint = await clockFromReceipt[mode](t2.receipt);
|
||||
t3.timepoint = await clockFromReceipt[mode](t3.receipt);
|
||||
|
||||
expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('1');
|
||||
expect(await this.token.checkpoints(other1, 0)).to.be.deep.equal([t1.timepoint.toString(), '80']);
|
||||
|
||||
const t4 = await this.token.transfer(recipient, 20, { from: holder });
|
||||
t4.timepoint = await clockFromReceipt[mode](t4.receipt);
|
||||
|
||||
expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('2');
|
||||
expect(await this.token.checkpoints(other1, 1)).to.be.deep.equal([t4.timepoint.toString(), '100']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getPriorVotes', function () {
|
||||
it('reverts if block number >= current block', async function () {
|
||||
await expectRevert(this.token.getPriorVotes(other1, 5e10), 'ERC20Votes: future lookup');
|
||||
});
|
||||
|
||||
it('returns 0 if there are no checkpoints', async function () {
|
||||
expect(await this.token.getPriorVotes(other1, 0)).to.be.bignumber.equal('0');
|
||||
});
|
||||
|
||||
it('returns the latest block if >= last checkpoint block', async function () {
|
||||
const { receipt } = await this.token.delegate(other1, { from: holder });
|
||||
const timepoint = await clockFromReceipt[mode](receipt);
|
||||
await time.advanceBlock();
|
||||
await time.advanceBlock();
|
||||
|
||||
expect(await this.token.getPriorVotes(other1, timepoint)).to.be.bignumber.equal(
|
||||
'10000000000000000000000000',
|
||||
);
|
||||
expect(await this.token.getPriorVotes(other1, timepoint + 1)).to.be.bignumber.equal(
|
||||
'10000000000000000000000000',
|
||||
);
|
||||
});
|
||||
|
||||
it('returns zero if < first checkpoint block', async function () {
|
||||
await time.advanceBlock();
|
||||
const { receipt } = await this.token.delegate(other1, { from: holder });
|
||||
const timepoint = await clockFromReceipt[mode](receipt);
|
||||
await time.advanceBlock();
|
||||
await time.advanceBlock();
|
||||
|
||||
expect(await this.token.getPriorVotes(other1, timepoint - 1)).to.be.bignumber.equal('0');
|
||||
expect(await this.token.getPriorVotes(other1, timepoint + 1)).to.be.bignumber.equal(
|
||||
'10000000000000000000000000',
|
||||
);
|
||||
});
|
||||
|
||||
it('generally returns the voting balance at the appropriate checkpoint', async function () {
|
||||
const t1 = await this.token.delegate(other1, { from: holder });
|
||||
await time.advanceBlock();
|
||||
await time.advanceBlock();
|
||||
const t2 = await this.token.transfer(other2, 10, { from: holder });
|
||||
await time.advanceBlock();
|
||||
await time.advanceBlock();
|
||||
const t3 = await this.token.transfer(other2, 10, { from: holder });
|
||||
await time.advanceBlock();
|
||||
await time.advanceBlock();
|
||||
const t4 = await this.token.transfer(holder, 20, { from: other2 });
|
||||
await time.advanceBlock();
|
||||
await time.advanceBlock();
|
||||
|
||||
t1.timepoint = await clockFromReceipt[mode](t1.receipt);
|
||||
t2.timepoint = await clockFromReceipt[mode](t2.receipt);
|
||||
t3.timepoint = await clockFromReceipt[mode](t3.receipt);
|
||||
t4.timepoint = await clockFromReceipt[mode](t4.receipt);
|
||||
|
||||
expect(await this.token.getPriorVotes(other1, t1.timepoint - 1)).to.be.bignumber.equal('0');
|
||||
expect(await this.token.getPriorVotes(other1, t1.timepoint)).to.be.bignumber.equal(
|
||||
'10000000000000000000000000',
|
||||
);
|
||||
expect(await this.token.getPriorVotes(other1, t1.timepoint + 1)).to.be.bignumber.equal(
|
||||
'10000000000000000000000000',
|
||||
);
|
||||
expect(await this.token.getPriorVotes(other1, t2.timepoint)).to.be.bignumber.equal(
|
||||
'9999999999999999999999990',
|
||||
);
|
||||
expect(await this.token.getPriorVotes(other1, t2.timepoint + 1)).to.be.bignumber.equal(
|
||||
'9999999999999999999999990',
|
||||
);
|
||||
expect(await this.token.getPriorVotes(other1, t3.timepoint)).to.be.bignumber.equal(
|
||||
'9999999999999999999999980',
|
||||
);
|
||||
expect(await this.token.getPriorVotes(other1, t3.timepoint + 1)).to.be.bignumber.equal(
|
||||
'9999999999999999999999980',
|
||||
);
|
||||
expect(await this.token.getPriorVotes(other1, t4.timepoint)).to.be.bignumber.equal(
|
||||
'10000000000000000000000000',
|
||||
);
|
||||
expect(await this.token.getPriorVotes(other1, t4.timepoint + 1)).to.be.bignumber.equal(
|
||||
'10000000000000000000000000',
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('getPastTotalSupply', function () {
|
||||
beforeEach(async function () {
|
||||
await this.token.delegate(holder, { from: holder });
|
||||
});
|
||||
|
||||
it('reverts if block number >= current block', async function () {
|
||||
await expectRevert(this.token.getPastTotalSupply(5e10), 'ERC20Votes: future lookup');
|
||||
});
|
||||
|
||||
it('returns 0 if there are no checkpoints', async function () {
|
||||
expect(await this.token.getPastTotalSupply(0)).to.be.bignumber.equal('0');
|
||||
});
|
||||
|
||||
it('returns the latest block if >= last checkpoint block', async function () {
|
||||
const { receipt } = await this.token.$_mint(holder, supply);
|
||||
const timepoint = await clockFromReceipt[mode](receipt);
|
||||
await time.advanceBlock();
|
||||
await time.advanceBlock();
|
||||
|
||||
expect(await this.token.getPastTotalSupply(timepoint)).to.be.bignumber.equal(supply);
|
||||
expect(await this.token.getPastTotalSupply(timepoint + 1)).to.be.bignumber.equal(supply);
|
||||
});
|
||||
|
||||
it('returns zero if < first checkpoint block', async function () {
|
||||
await time.advanceBlock();
|
||||
const { receipt } = await this.token.$_mint(holder, supply);
|
||||
const timepoint = await clockFromReceipt[mode](receipt);
|
||||
await time.advanceBlock();
|
||||
await time.advanceBlock();
|
||||
|
||||
expect(await this.token.getPastTotalSupply(timepoint - 1)).to.be.bignumber.equal('0');
|
||||
expect(await this.token.getPastTotalSupply(timepoint + 1)).to.be.bignumber.equal(
|
||||
'10000000000000000000000000',
|
||||
);
|
||||
});
|
||||
|
||||
it('generally returns the voting balance at the appropriate checkpoint', async function () {
|
||||
const t1 = await this.token.$_mint(holder, supply);
|
||||
await time.advanceBlock();
|
||||
await time.advanceBlock();
|
||||
const t2 = await this.token.$_burn(holder, 10);
|
||||
await time.advanceBlock();
|
||||
await time.advanceBlock();
|
||||
const t3 = await this.token.$_burn(holder, 10);
|
||||
await time.advanceBlock();
|
||||
await time.advanceBlock();
|
||||
const t4 = await this.token.$_mint(holder, 20);
|
||||
await time.advanceBlock();
|
||||
await time.advanceBlock();
|
||||
|
||||
t1.timepoint = await clockFromReceipt[mode](t1.receipt);
|
||||
t2.timepoint = await clockFromReceipt[mode](t2.receipt);
|
||||
t3.timepoint = await clockFromReceipt[mode](t3.receipt);
|
||||
t4.timepoint = await clockFromReceipt[mode](t4.receipt);
|
||||
|
||||
expect(await this.token.getPastTotalSupply(t1.timepoint - 1)).to.be.bignumber.equal('0');
|
||||
expect(await this.token.getPastTotalSupply(t1.timepoint)).to.be.bignumber.equal('10000000000000000000000000');
|
||||
expect(await this.token.getPastTotalSupply(t1.timepoint + 1)).to.be.bignumber.equal(
|
||||
'10000000000000000000000000',
|
||||
);
|
||||
expect(await this.token.getPastTotalSupply(t2.timepoint)).to.be.bignumber.equal('9999999999999999999999990');
|
||||
expect(await this.token.getPastTotalSupply(t2.timepoint + 1)).to.be.bignumber.equal(
|
||||
'9999999999999999999999990',
|
||||
);
|
||||
expect(await this.token.getPastTotalSupply(t3.timepoint)).to.be.bignumber.equal('9999999999999999999999980');
|
||||
expect(await this.token.getPastTotalSupply(t3.timepoint + 1)).to.be.bignumber.equal(
|
||||
'9999999999999999999999980',
|
||||
);
|
||||
expect(await this.token.getPastTotalSupply(t4.timepoint)).to.be.bignumber.equal('10000000000000000000000000');
|
||||
expect(await this.token.getPastTotalSupply(t4.timepoint + 1)).to.be.bignumber.equal(
|
||||
'10000000000000000000000000',
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,190 @@
|
||||
const { BN, constants, expectEvent, expectRevert } = require('@openzeppelin/test-helpers');
|
||||
const { expect } = require('chai');
|
||||
const { ZERO_ADDRESS, MAX_UINT256 } = constants;
|
||||
|
||||
const { shouldBehaveLikeERC20 } = require('../ERC20.behavior');
|
||||
|
||||
const NotAnERC20 = artifacts.require('CallReceiverMock');
|
||||
const ERC20Decimals = artifacts.require('$ERC20DecimalsMock');
|
||||
const ERC20Wrapper = artifacts.require('$ERC20Wrapper');
|
||||
|
||||
contract('ERC20', function (accounts) {
|
||||
const [initialHolder, recipient, anotherAccount] = accounts;
|
||||
|
||||
const name = 'My Token';
|
||||
const symbol = 'MTKN';
|
||||
|
||||
const initialSupply = new BN(100);
|
||||
|
||||
beforeEach(async function () {
|
||||
this.underlying = await ERC20Decimals.new(name, symbol, 9);
|
||||
await this.underlying.$_mint(initialHolder, initialSupply);
|
||||
|
||||
this.token = await ERC20Wrapper.new(`Wrapped ${name}`, `W${symbol}`, this.underlying.address);
|
||||
});
|
||||
|
||||
afterEach(async function () {
|
||||
expect(await this.underlying.balanceOf(this.token.address)).to.be.bignumber.equal(await this.token.totalSupply());
|
||||
});
|
||||
|
||||
it('has a name', async function () {
|
||||
expect(await this.token.name()).to.equal(`Wrapped ${name}`);
|
||||
});
|
||||
|
||||
it('has a symbol', async function () {
|
||||
expect(await this.token.symbol()).to.equal(`W${symbol}`);
|
||||
});
|
||||
|
||||
it('has the same decimals as the underlying token', async function () {
|
||||
expect(await this.token.decimals()).to.be.bignumber.equal('9');
|
||||
});
|
||||
|
||||
it('decimals default back to 18 if token has no metadata', async function () {
|
||||
const noDecimals = await NotAnERC20.new();
|
||||
const otherToken = await ERC20Wrapper.new(`Wrapped ${name}`, `W${symbol}`, noDecimals.address);
|
||||
expect(await otherToken.decimals()).to.be.bignumber.equal('18');
|
||||
});
|
||||
|
||||
it('has underlying', async function () {
|
||||
expect(await this.token.underlying()).to.be.bignumber.equal(this.underlying.address);
|
||||
});
|
||||
|
||||
describe('deposit', function () {
|
||||
it('valid', async function () {
|
||||
await this.underlying.approve(this.token.address, initialSupply, { from: initialHolder });
|
||||
const { tx } = await this.token.depositFor(initialHolder, initialSupply, { from: initialHolder });
|
||||
await expectEvent.inTransaction(tx, this.underlying, 'Transfer', {
|
||||
from: initialHolder,
|
||||
to: this.token.address,
|
||||
value: initialSupply,
|
||||
});
|
||||
await expectEvent.inTransaction(tx, this.token, 'Transfer', {
|
||||
from: ZERO_ADDRESS,
|
||||
to: initialHolder,
|
||||
value: initialSupply,
|
||||
});
|
||||
});
|
||||
|
||||
it('missing approval', async function () {
|
||||
await expectRevert(
|
||||
this.token.depositFor(initialHolder, initialSupply, { from: initialHolder }),
|
||||
'ERC20: insufficient allowance',
|
||||
);
|
||||
});
|
||||
|
||||
it('missing balance', async function () {
|
||||
await this.underlying.approve(this.token.address, MAX_UINT256, { from: initialHolder });
|
||||
await expectRevert(
|
||||
this.token.depositFor(initialHolder, MAX_UINT256, { from: initialHolder }),
|
||||
'ERC20: transfer amount exceeds balance',
|
||||
);
|
||||
});
|
||||
|
||||
it('to other account', async function () {
|
||||
await this.underlying.approve(this.token.address, initialSupply, { from: initialHolder });
|
||||
const { tx } = await this.token.depositFor(anotherAccount, initialSupply, { from: initialHolder });
|
||||
await expectEvent.inTransaction(tx, this.underlying, 'Transfer', {
|
||||
from: initialHolder,
|
||||
to: this.token.address,
|
||||
value: initialSupply,
|
||||
});
|
||||
await expectEvent.inTransaction(tx, this.token, 'Transfer', {
|
||||
from: ZERO_ADDRESS,
|
||||
to: anotherAccount,
|
||||
value: initialSupply,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('withdraw', function () {
|
||||
beforeEach(async function () {
|
||||
await this.underlying.approve(this.token.address, initialSupply, { from: initialHolder });
|
||||
await this.token.depositFor(initialHolder, initialSupply, { from: initialHolder });
|
||||
});
|
||||
|
||||
it('missing balance', async function () {
|
||||
await expectRevert(
|
||||
this.token.withdrawTo(initialHolder, MAX_UINT256, { from: initialHolder }),
|
||||
'ERC20: burn amount exceeds balance',
|
||||
);
|
||||
});
|
||||
|
||||
it('valid', async function () {
|
||||
const value = new BN(42);
|
||||
|
||||
const { tx } = await this.token.withdrawTo(initialHolder, value, { from: initialHolder });
|
||||
await expectEvent.inTransaction(tx, this.underlying, 'Transfer', {
|
||||
from: this.token.address,
|
||||
to: initialHolder,
|
||||
value: value,
|
||||
});
|
||||
await expectEvent.inTransaction(tx, this.token, 'Transfer', {
|
||||
from: initialHolder,
|
||||
to: ZERO_ADDRESS,
|
||||
value: value,
|
||||
});
|
||||
});
|
||||
|
||||
it('entire balance', async function () {
|
||||
const { tx } = await this.token.withdrawTo(initialHolder, initialSupply, { from: initialHolder });
|
||||
await expectEvent.inTransaction(tx, this.underlying, 'Transfer', {
|
||||
from: this.token.address,
|
||||
to: initialHolder,
|
||||
value: initialSupply,
|
||||
});
|
||||
await expectEvent.inTransaction(tx, this.token, 'Transfer', {
|
||||
from: initialHolder,
|
||||
to: ZERO_ADDRESS,
|
||||
value: initialSupply,
|
||||
});
|
||||
});
|
||||
|
||||
it('to other account', async function () {
|
||||
const { tx } = await this.token.withdrawTo(anotherAccount, initialSupply, { from: initialHolder });
|
||||
await expectEvent.inTransaction(tx, this.underlying, 'Transfer', {
|
||||
from: this.token.address,
|
||||
to: anotherAccount,
|
||||
value: initialSupply,
|
||||
});
|
||||
await expectEvent.inTransaction(tx, this.token, 'Transfer', {
|
||||
from: initialHolder,
|
||||
to: ZERO_ADDRESS,
|
||||
value: initialSupply,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('recover', function () {
|
||||
it('nothing to recover', async function () {
|
||||
await this.underlying.approve(this.token.address, initialSupply, { from: initialHolder });
|
||||
await this.token.depositFor(initialHolder, initialSupply, { from: initialHolder });
|
||||
|
||||
const { tx } = await this.token.$_recover(anotherAccount);
|
||||
await expectEvent.inTransaction(tx, this.token, 'Transfer', {
|
||||
from: ZERO_ADDRESS,
|
||||
to: anotherAccount,
|
||||
value: '0',
|
||||
});
|
||||
});
|
||||
|
||||
it('something to recover', async function () {
|
||||
await this.underlying.transfer(this.token.address, initialSupply, { from: initialHolder });
|
||||
|
||||
const { tx } = await this.token.$_recover(anotherAccount);
|
||||
await expectEvent.inTransaction(tx, this.token, 'Transfer', {
|
||||
from: ZERO_ADDRESS,
|
||||
to: anotherAccount,
|
||||
value: initialSupply,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('erc20 behaviour', function () {
|
||||
beforeEach(async function () {
|
||||
await this.underlying.approve(this.token.address, initialSupply, { from: initialHolder });
|
||||
await this.token.depositFor(initialHolder, initialSupply, { from: initialHolder });
|
||||
});
|
||||
|
||||
shouldBehaveLikeERC20('ERC20', initialSupply, initialHolder, recipient, anotherAccount);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,42 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
pragma solidity ^0.8.0;
|
||||
|
||||
import {ERC4626Test} from "erc4626-tests/ERC4626.test.sol";
|
||||
|
||||
import {SafeCast} from "openzeppelin/utils/math/SafeCast.sol";
|
||||
import {ERC20} from "openzeppelin/token/ERC20/ERC20.sol";
|
||||
import {ERC4626} from "openzeppelin/token/ERC20/extensions/ERC4626.sol";
|
||||
|
||||
import {ERC20Mock} from "openzeppelin/mocks/ERC20Mock.sol";
|
||||
import {ERC4626Mock} from "openzeppelin/mocks/ERC4626Mock.sol";
|
||||
import {ERC4626OffsetMock} from "openzeppelin/mocks/token/ERC4626OffsetMock.sol";
|
||||
|
||||
contract ERC4626VaultOffsetMock is ERC4626OffsetMock {
|
||||
constructor(
|
||||
ERC20 underlying_,
|
||||
uint8 offset_
|
||||
) ERC20("My Token Vault", "MTKNV") ERC4626(underlying_) ERC4626OffsetMock(offset_) {}
|
||||
}
|
||||
|
||||
contract ERC4626StdTest is ERC4626Test {
|
||||
ERC20 private _underlying = new ERC20Mock();
|
||||
|
||||
function setUp() public override {
|
||||
_underlying_ = address(_underlying);
|
||||
_vault_ = address(new ERC4626Mock(_underlying_));
|
||||
_delta_ = 0;
|
||||
_vaultMayBeEmpty = true;
|
||||
_unlimitedAmount = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Check the case where calculated `decimals` value overflows the `uint8` type.
|
||||
*/
|
||||
function testFuzzDecimalsOverflow(uint8 offset) public {
|
||||
/// @dev Remember that the `_underlying` exhibits a `decimals` value of 18.
|
||||
offset = uint8(bound(uint256(offset), 238, uint256(type(uint8).max)));
|
||||
ERC4626VaultOffsetMock erc4626VaultOffsetMock = new ERC4626VaultOffsetMock(_underlying, offset);
|
||||
vm.expectRevert();
|
||||
erc4626VaultOffsetMock.decimals();
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
+103
@@ -0,0 +1,103 @@
|
||||
/* eslint-disable */
|
||||
|
||||
const { BN, constants, expectRevert, time } = require('@openzeppelin/test-helpers');
|
||||
const { expect } = require('chai');
|
||||
const { MAX_UINT256 } = constants;
|
||||
|
||||
const { fromRpcSig } = require('ethereumjs-util');
|
||||
const ethSigUtil = require('eth-sig-util');
|
||||
const Wallet = require('ethereumjs-wallet').default;
|
||||
|
||||
const ERC20Permit = artifacts.require('$ERC20Permit');
|
||||
|
||||
const { Permit, getDomain, domainType, domainSeparator } = require('../../../helpers/eip712');
|
||||
const { getChainId } = require('../../../helpers/chainid');
|
||||
|
||||
contract('ERC20Permit', function (accounts) {
|
||||
const [initialHolder, spender] = accounts;
|
||||
|
||||
const name = 'My Token';
|
||||
const symbol = 'MTKN';
|
||||
const version = '1';
|
||||
|
||||
const initialSupply = new BN(100);
|
||||
|
||||
beforeEach(async function () {
|
||||
this.chainId = await getChainId();
|
||||
|
||||
this.token = await ERC20Permit.new(name, symbol, name);
|
||||
await this.token.$_mint(initialHolder, initialSupply);
|
||||
});
|
||||
|
||||
it('initial nonce is 0', async function () {
|
||||
expect(await this.token.nonces(initialHolder)).to.be.bignumber.equal('0');
|
||||
});
|
||||
|
||||
it('domain separator', async function () {
|
||||
expect(await this.token.DOMAIN_SEPARATOR()).to.equal(await getDomain(this.token).then(domainSeparator));
|
||||
});
|
||||
|
||||
describe('permit', function () {
|
||||
const wallet = Wallet.generate();
|
||||
|
||||
const owner = wallet.getAddressString();
|
||||
const value = new BN(42);
|
||||
const nonce = 0;
|
||||
const maxDeadline = MAX_UINT256;
|
||||
|
||||
const buildData = (contract, deadline = maxDeadline) =>
|
||||
getDomain(contract).then(domain => ({
|
||||
primaryType: 'Permit',
|
||||
types: { EIP712Domain: domainType(domain), Permit },
|
||||
domain,
|
||||
message: { owner, spender, value, nonce, deadline },
|
||||
}));
|
||||
|
||||
it('accepts owner signature', async function () {
|
||||
const { v, r, s } = await buildData(this.token)
|
||||
.then(data => ethSigUtil.signTypedMessage(wallet.getPrivateKey(), { data }))
|
||||
.then(fromRpcSig);
|
||||
|
||||
await this.token.permit(owner, spender, value, maxDeadline, v, r, s);
|
||||
|
||||
expect(await this.token.nonces(owner)).to.be.bignumber.equal('1');
|
||||
expect(await this.token.allowance(owner, spender)).to.be.bignumber.equal(value);
|
||||
});
|
||||
|
||||
it('rejects reused signature', async function () {
|
||||
const { v, r, s } = await buildData(this.token)
|
||||
.then(data => ethSigUtil.signTypedMessage(wallet.getPrivateKey(), { data }))
|
||||
.then(fromRpcSig);
|
||||
|
||||
await this.token.permit(owner, spender, value, maxDeadline, v, r, s);
|
||||
|
||||
await expectRevert(
|
||||
this.token.permit(owner, spender, value, maxDeadline, v, r, s),
|
||||
'ERC20Permit: invalid signature',
|
||||
);
|
||||
});
|
||||
|
||||
it('rejects other signature', async function () {
|
||||
const otherWallet = Wallet.generate();
|
||||
|
||||
const { v, r, s } = await buildData(this.token)
|
||||
.then(data => ethSigUtil.signTypedMessage(otherWallet.getPrivateKey(), { data }))
|
||||
.then(fromRpcSig);
|
||||
|
||||
await expectRevert(
|
||||
this.token.permit(owner, spender, value, maxDeadline, v, r, s),
|
||||
'ERC20Permit: invalid signature',
|
||||
);
|
||||
});
|
||||
|
||||
it('rejects expired permit', async function () {
|
||||
const deadline = (await time.latest()) - time.duration.weeks(1);
|
||||
|
||||
const { v, r, s } = await buildData(this.token, deadline)
|
||||
.then(data => ethSigUtil.signTypedMessage(wallet.getPrivateKey(), { data }))
|
||||
.then(fromRpcSig);
|
||||
|
||||
await expectRevert(this.token.permit(owner, spender, value, deadline, v, r, s), 'ERC20Permit: expired deadline');
|
||||
});
|
||||
});
|
||||
});
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
const { BN, constants, expectEvent } = require('@openzeppelin/test-helpers');
|
||||
const { ZERO_ADDRESS } = constants;
|
||||
|
||||
const { expect } = require('chai');
|
||||
|
||||
const ERC20PresetFixedSupply = artifacts.require('ERC20PresetFixedSupply');
|
||||
|
||||
contract('ERC20PresetFixedSupply', function (accounts) {
|
||||
const [deployer, owner] = accounts;
|
||||
|
||||
const name = 'PresetFixedSupply';
|
||||
const symbol = 'PFS';
|
||||
|
||||
const initialSupply = new BN('50000');
|
||||
const amount = new BN('10000');
|
||||
|
||||
before(async function () {
|
||||
this.token = await ERC20PresetFixedSupply.new(name, symbol, initialSupply, owner, { from: deployer });
|
||||
});
|
||||
|
||||
it('deployer has the balance equal to initial supply', async function () {
|
||||
expect(await this.token.balanceOf(owner)).to.be.bignumber.equal(initialSupply);
|
||||
});
|
||||
|
||||
it('total supply is equal to initial supply', async function () {
|
||||
expect(await this.token.totalSupply()).to.be.bignumber.equal(initialSupply);
|
||||
});
|
||||
|
||||
describe('burning', function () {
|
||||
it('holders can burn their tokens', async function () {
|
||||
const remainingBalance = initialSupply.sub(amount);
|
||||
const receipt = await this.token.burn(amount, { from: owner });
|
||||
expectEvent(receipt, 'Transfer', { from: owner, to: ZERO_ADDRESS, value: amount });
|
||||
expect(await this.token.balanceOf(owner)).to.be.bignumber.equal(remainingBalance);
|
||||
});
|
||||
|
||||
it('decrements totalSupply', async function () {
|
||||
const expectedSupply = initialSupply.sub(amount);
|
||||
expect(await this.token.totalSupply()).to.be.bignumber.equal(expectedSupply);
|
||||
});
|
||||
});
|
||||
});
|
||||
+110
@@ -0,0 +1,110 @@
|
||||
const { BN, constants, expectEvent, expectRevert } = require('@openzeppelin/test-helpers');
|
||||
const { ZERO_ADDRESS } = constants;
|
||||
|
||||
const { expect } = require('chai');
|
||||
|
||||
const ERC20PresetMinterPauser = artifacts.require('ERC20PresetMinterPauser');
|
||||
|
||||
contract('ERC20PresetMinterPauser', function (accounts) {
|
||||
const [deployer, other] = accounts;
|
||||
|
||||
const name = 'MinterPauserToken';
|
||||
const symbol = 'DRT';
|
||||
|
||||
const amount = new BN('5000');
|
||||
|
||||
const DEFAULT_ADMIN_ROLE = '0x0000000000000000000000000000000000000000000000000000000000000000';
|
||||
const MINTER_ROLE = web3.utils.soliditySha3('MINTER_ROLE');
|
||||
const PAUSER_ROLE = web3.utils.soliditySha3('PAUSER_ROLE');
|
||||
|
||||
beforeEach(async function () {
|
||||
this.token = await ERC20PresetMinterPauser.new(name, symbol, { from: deployer });
|
||||
});
|
||||
|
||||
it('deployer has the default admin role', async function () {
|
||||
expect(await this.token.getRoleMemberCount(DEFAULT_ADMIN_ROLE)).to.be.bignumber.equal('1');
|
||||
expect(await this.token.getRoleMember(DEFAULT_ADMIN_ROLE, 0)).to.equal(deployer);
|
||||
});
|
||||
|
||||
it('deployer has the minter role', async function () {
|
||||
expect(await this.token.getRoleMemberCount(MINTER_ROLE)).to.be.bignumber.equal('1');
|
||||
expect(await this.token.getRoleMember(MINTER_ROLE, 0)).to.equal(deployer);
|
||||
});
|
||||
|
||||
it('deployer has the pauser role', async function () {
|
||||
expect(await this.token.getRoleMemberCount(PAUSER_ROLE)).to.be.bignumber.equal('1');
|
||||
expect(await this.token.getRoleMember(PAUSER_ROLE, 0)).to.equal(deployer);
|
||||
});
|
||||
|
||||
it('minter and pauser role admin is the default admin', async function () {
|
||||
expect(await this.token.getRoleAdmin(MINTER_ROLE)).to.equal(DEFAULT_ADMIN_ROLE);
|
||||
expect(await this.token.getRoleAdmin(PAUSER_ROLE)).to.equal(DEFAULT_ADMIN_ROLE);
|
||||
});
|
||||
|
||||
describe('minting', function () {
|
||||
it('deployer can mint tokens', async function () {
|
||||
const receipt = await this.token.mint(other, amount, { from: deployer });
|
||||
expectEvent(receipt, 'Transfer', { from: ZERO_ADDRESS, to: other, value: amount });
|
||||
|
||||
expect(await this.token.balanceOf(other)).to.be.bignumber.equal(amount);
|
||||
});
|
||||
|
||||
it('other accounts cannot mint tokens', async function () {
|
||||
await expectRevert(
|
||||
this.token.mint(other, amount, { from: other }),
|
||||
'ERC20PresetMinterPauser: must have minter role to mint',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('pausing', function () {
|
||||
it('deployer can pause', async function () {
|
||||
const receipt = await this.token.pause({ from: deployer });
|
||||
expectEvent(receipt, 'Paused', { account: deployer });
|
||||
|
||||
expect(await this.token.paused()).to.equal(true);
|
||||
});
|
||||
|
||||
it('deployer can unpause', async function () {
|
||||
await this.token.pause({ from: deployer });
|
||||
|
||||
const receipt = await this.token.unpause({ from: deployer });
|
||||
expectEvent(receipt, 'Unpaused', { account: deployer });
|
||||
|
||||
expect(await this.token.paused()).to.equal(false);
|
||||
});
|
||||
|
||||
it('cannot mint while paused', async function () {
|
||||
await this.token.pause({ from: deployer });
|
||||
|
||||
await expectRevert(
|
||||
this.token.mint(other, amount, { from: deployer }),
|
||||
'ERC20Pausable: token transfer while paused',
|
||||
);
|
||||
});
|
||||
|
||||
it('other accounts cannot pause', async function () {
|
||||
await expectRevert(this.token.pause({ from: other }), 'ERC20PresetMinterPauser: must have pauser role to pause');
|
||||
});
|
||||
|
||||
it('other accounts cannot unpause', async function () {
|
||||
await this.token.pause({ from: deployer });
|
||||
|
||||
await expectRevert(
|
||||
this.token.unpause({ from: other }),
|
||||
'ERC20PresetMinterPauser: must have pauser role to unpause',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('burning', function () {
|
||||
it('holders can burn their tokens', async function () {
|
||||
await this.token.mint(other, amount, { from: deployer });
|
||||
|
||||
const receipt = await this.token.burn(amount.subn(1), { from: other });
|
||||
expectEvent(receipt, 'Transfer', { from: other, to: ZERO_ADDRESS, value: amount.subn(1) });
|
||||
|
||||
expect(await this.token.balanceOf(other)).to.be.bignumber.equal('1');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,350 @@
|
||||
const { constants, expectEvent, expectRevert } = require('@openzeppelin/test-helpers');
|
||||
|
||||
const SafeERC20 = artifacts.require('$SafeERC20');
|
||||
const ERC20ReturnFalseMock = artifacts.require('$ERC20ReturnFalseMock');
|
||||
const ERC20ReturnTrueMock = artifacts.require('$ERC20'); // default implementation returns true
|
||||
const ERC20NoReturnMock = artifacts.require('$ERC20NoReturnMock');
|
||||
const ERC20PermitNoRevertMock = artifacts.require('$ERC20PermitNoRevertMock');
|
||||
const ERC20ForceApproveMock = artifacts.require('$ERC20ForceApproveMock');
|
||||
|
||||
const { getDomain, domainType, Permit } = require('../../../helpers/eip712');
|
||||
|
||||
const { fromRpcSig } = require('ethereumjs-util');
|
||||
const ethSigUtil = require('eth-sig-util');
|
||||
const Wallet = require('ethereumjs-wallet').default;
|
||||
|
||||
const name = 'ERC20Mock';
|
||||
const symbol = 'ERC20Mock';
|
||||
|
||||
contract('SafeERC20', function (accounts) {
|
||||
const [hasNoCode] = accounts;
|
||||
|
||||
before(async function () {
|
||||
this.mock = await SafeERC20.new();
|
||||
});
|
||||
|
||||
describe('with address that has no contract code', function () {
|
||||
beforeEach(async function () {
|
||||
this.token = { address: hasNoCode };
|
||||
});
|
||||
|
||||
shouldRevertOnAllCalls(accounts, 'Address: call to non-contract');
|
||||
});
|
||||
|
||||
describe('with token that returns false on all calls', function () {
|
||||
beforeEach(async function () {
|
||||
this.token = await ERC20ReturnFalseMock.new(name, symbol);
|
||||
});
|
||||
|
||||
shouldRevertOnAllCalls(accounts, 'SafeERC20: ERC20 operation did not succeed');
|
||||
});
|
||||
|
||||
describe('with token that returns true on all calls', function () {
|
||||
beforeEach(async function () {
|
||||
this.token = await ERC20ReturnTrueMock.new(name, symbol);
|
||||
});
|
||||
|
||||
shouldOnlyRevertOnErrors(accounts);
|
||||
});
|
||||
|
||||
describe('with token that returns no boolean values', function () {
|
||||
beforeEach(async function () {
|
||||
this.token = await ERC20NoReturnMock.new(name, symbol);
|
||||
});
|
||||
|
||||
shouldOnlyRevertOnErrors(accounts);
|
||||
});
|
||||
|
||||
describe("with token that doesn't revert on invalid permit", function () {
|
||||
const wallet = Wallet.generate();
|
||||
const owner = wallet.getAddressString();
|
||||
const spender = hasNoCode;
|
||||
|
||||
beforeEach(async function () {
|
||||
this.token = await ERC20PermitNoRevertMock.new(name, symbol, name);
|
||||
|
||||
this.data = await getDomain(this.token).then(domain => ({
|
||||
primaryType: 'Permit',
|
||||
types: { EIP712Domain: domainType(domain), Permit },
|
||||
domain,
|
||||
message: { owner, spender, value: '42', nonce: '0', deadline: constants.MAX_UINT256 },
|
||||
}));
|
||||
|
||||
this.signature = fromRpcSig(ethSigUtil.signTypedMessage(wallet.getPrivateKey(), { data: this.data }));
|
||||
});
|
||||
|
||||
it('accepts owner signature', async function () {
|
||||
expect(await this.token.nonces(owner)).to.be.bignumber.equal('0');
|
||||
expect(await this.token.allowance(owner, spender)).to.be.bignumber.equal('0');
|
||||
|
||||
await this.mock.$safePermit(
|
||||
this.token.address,
|
||||
this.data.message.owner,
|
||||
this.data.message.spender,
|
||||
this.data.message.value,
|
||||
this.data.message.deadline,
|
||||
this.signature.v,
|
||||
this.signature.r,
|
||||
this.signature.s,
|
||||
);
|
||||
|
||||
expect(await this.token.nonces(owner)).to.be.bignumber.equal('1');
|
||||
expect(await this.token.allowance(owner, spender)).to.be.bignumber.equal(this.data.message.value);
|
||||
});
|
||||
|
||||
it('revert on reused signature', async function () {
|
||||
expect(await this.token.nonces(owner)).to.be.bignumber.equal('0');
|
||||
// use valid signature and consume nounce
|
||||
await this.mock.$safePermit(
|
||||
this.token.address,
|
||||
this.data.message.owner,
|
||||
this.data.message.spender,
|
||||
this.data.message.value,
|
||||
this.data.message.deadline,
|
||||
this.signature.v,
|
||||
this.signature.r,
|
||||
this.signature.s,
|
||||
);
|
||||
expect(await this.token.nonces(owner)).to.be.bignumber.equal('1');
|
||||
// invalid call does not revert for this token implementation
|
||||
await this.token.permit(
|
||||
this.data.message.owner,
|
||||
this.data.message.spender,
|
||||
this.data.message.value,
|
||||
this.data.message.deadline,
|
||||
this.signature.v,
|
||||
this.signature.r,
|
||||
this.signature.s,
|
||||
);
|
||||
expect(await this.token.nonces(owner)).to.be.bignumber.equal('1');
|
||||
// invalid call revert when called through the SafeERC20 library
|
||||
await expectRevert(
|
||||
this.mock.$safePermit(
|
||||
this.token.address,
|
||||
this.data.message.owner,
|
||||
this.data.message.spender,
|
||||
this.data.message.value,
|
||||
this.data.message.deadline,
|
||||
this.signature.v,
|
||||
this.signature.r,
|
||||
this.signature.s,
|
||||
),
|
||||
'SafeERC20: permit did not succeed',
|
||||
);
|
||||
expect(await this.token.nonces(owner)).to.be.bignumber.equal('1');
|
||||
});
|
||||
|
||||
it('revert on invalid signature', async function () {
|
||||
// signature that is not valid for owner
|
||||
const invalidSignature = {
|
||||
v: 27,
|
||||
r: '0x71753dc5ecb5b4bfc0e3bc530d79ce5988760ed3f3a234c86a5546491f540775',
|
||||
s: '0x0049cedee5aed990aabed5ad6a9f6e3c565b63379894b5fa8b512eb2b79e485d',
|
||||
};
|
||||
|
||||
// invalid call does not revert for this token implementation
|
||||
await this.token.permit(
|
||||
this.data.message.owner,
|
||||
this.data.message.spender,
|
||||
this.data.message.value,
|
||||
this.data.message.deadline,
|
||||
invalidSignature.v,
|
||||
invalidSignature.r,
|
||||
invalidSignature.s,
|
||||
);
|
||||
|
||||
// invalid call revert when called through the SafeERC20 library
|
||||
await expectRevert(
|
||||
this.mock.$safePermit(
|
||||
this.token.address,
|
||||
this.data.message.owner,
|
||||
this.data.message.spender,
|
||||
this.data.message.value,
|
||||
this.data.message.deadline,
|
||||
invalidSignature.v,
|
||||
invalidSignature.r,
|
||||
invalidSignature.s,
|
||||
),
|
||||
'SafeERC20: permit did not succeed',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('with usdt approval beaviour', function () {
|
||||
const spender = hasNoCode;
|
||||
|
||||
beforeEach(async function () {
|
||||
this.token = await ERC20ForceApproveMock.new(name, symbol);
|
||||
});
|
||||
|
||||
describe('with initial approval', function () {
|
||||
beforeEach(async function () {
|
||||
await this.token.$_approve(this.mock.address, spender, 100);
|
||||
});
|
||||
|
||||
it('safeApprove fails to update approval to non-zero', async function () {
|
||||
await expectRevert(
|
||||
this.mock.$safeApprove(this.token.address, spender, 200),
|
||||
'SafeERC20: approve from non-zero to non-zero allowance',
|
||||
);
|
||||
});
|
||||
|
||||
it('safeApprove can update approval to zero', async function () {
|
||||
await this.mock.$safeApprove(this.token.address, spender, 0);
|
||||
});
|
||||
|
||||
it('safeApprove can increase approval', async function () {
|
||||
await expectRevert(this.mock.$safeIncreaseAllowance(this.token.address, spender, 10), 'USDT approval failure');
|
||||
});
|
||||
|
||||
it('safeApprove can decrease approval', async function () {
|
||||
await expectRevert(this.mock.$safeDecreaseAllowance(this.token.address, spender, 10), 'USDT approval failure');
|
||||
});
|
||||
|
||||
it('forceApprove works', async function () {
|
||||
await this.mock.$forceApprove(this.token.address, spender, 200);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function shouldRevertOnAllCalls([receiver, spender], reason) {
|
||||
it('reverts on transfer', async function () {
|
||||
await expectRevert(this.mock.$safeTransfer(this.token.address, receiver, 0), reason);
|
||||
});
|
||||
|
||||
it('reverts on transferFrom', async function () {
|
||||
await expectRevert(this.mock.$safeTransferFrom(this.token.address, this.mock.address, receiver, 0), reason);
|
||||
});
|
||||
|
||||
it('reverts on approve', async function () {
|
||||
await expectRevert(this.mock.$safeApprove(this.token.address, spender, 0), reason);
|
||||
});
|
||||
|
||||
it('reverts on increaseAllowance', async function () {
|
||||
// [TODO] make sure it's reverting for the right reason
|
||||
await expectRevert.unspecified(this.mock.$safeIncreaseAllowance(this.token.address, spender, 0));
|
||||
});
|
||||
|
||||
it('reverts on decreaseAllowance', async function () {
|
||||
// [TODO] make sure it's reverting for the right reason
|
||||
await expectRevert.unspecified(this.mock.$safeDecreaseAllowance(this.token.address, spender, 0));
|
||||
});
|
||||
|
||||
it('reverts on forceApprove', async function () {
|
||||
await expectRevert(this.mock.$forceApprove(this.token.address, spender, 0), reason);
|
||||
});
|
||||
}
|
||||
|
||||
function shouldOnlyRevertOnErrors([owner, receiver, spender]) {
|
||||
describe('transfers', function () {
|
||||
beforeEach(async function () {
|
||||
await this.token.$_mint(owner, 100);
|
||||
await this.token.$_mint(this.mock.address, 100);
|
||||
await this.token.approve(this.mock.address, constants.MAX_UINT256, { from: owner });
|
||||
});
|
||||
|
||||
it("doesn't revert on transfer", async function () {
|
||||
const { tx } = await this.mock.$safeTransfer(this.token.address, receiver, 10);
|
||||
await expectEvent.inTransaction(tx, this.token, 'Transfer', {
|
||||
from: this.mock.address,
|
||||
to: receiver,
|
||||
value: '10',
|
||||
});
|
||||
});
|
||||
|
||||
it("doesn't revert on transferFrom", async function () {
|
||||
const { tx } = await this.mock.$safeTransferFrom(this.token.address, owner, receiver, 10);
|
||||
await expectEvent.inTransaction(tx, this.token, 'Transfer', {
|
||||
from: owner,
|
||||
to: receiver,
|
||||
value: '10',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('approvals', function () {
|
||||
context('with zero allowance', function () {
|
||||
beforeEach(async function () {
|
||||
await this.token.$_approve(this.mock.address, spender, 0);
|
||||
});
|
||||
|
||||
it("doesn't revert when approving a non-zero allowance", async function () {
|
||||
await this.mock.$safeApprove(this.token.address, spender, 100);
|
||||
expect(await this.token.allowance(this.mock.address, spender)).to.be.bignumber.equal('100');
|
||||
});
|
||||
|
||||
it("doesn't revert when approving a zero allowance", async function () {
|
||||
await this.mock.$safeApprove(this.token.address, spender, 0);
|
||||
expect(await this.token.allowance(this.mock.address, spender)).to.be.bignumber.equal('0');
|
||||
});
|
||||
|
||||
it("doesn't revert when force approving a non-zero allowance", async function () {
|
||||
await this.mock.$forceApprove(this.token.address, spender, 100);
|
||||
expect(await this.token.allowance(this.mock.address, spender)).to.be.bignumber.equal('100');
|
||||
});
|
||||
|
||||
it("doesn't revert when force approving a zero allowance", async function () {
|
||||
await this.mock.$forceApprove(this.token.address, spender, 0);
|
||||
expect(await this.token.allowance(this.mock.address, spender)).to.be.bignumber.equal('0');
|
||||
});
|
||||
|
||||
it("doesn't revert when increasing the allowance", async function () {
|
||||
await this.mock.$safeIncreaseAllowance(this.token.address, spender, 10);
|
||||
expect(await this.token.allowance(this.mock.address, spender)).to.be.bignumber.equal('10');
|
||||
});
|
||||
|
||||
it('reverts when decreasing the allowance', async function () {
|
||||
await expectRevert(
|
||||
this.mock.$safeDecreaseAllowance(this.token.address, spender, 10),
|
||||
'SafeERC20: decreased allowance below zero',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
context('with non-zero allowance', function () {
|
||||
beforeEach(async function () {
|
||||
await this.token.$_approve(this.mock.address, spender, 100);
|
||||
});
|
||||
|
||||
it('reverts when approving a non-zero allowance', async function () {
|
||||
await expectRevert(
|
||||
this.mock.$safeApprove(this.token.address, spender, 20),
|
||||
'SafeERC20: approve from non-zero to non-zero allowance',
|
||||
);
|
||||
});
|
||||
|
||||
it("doesn't revert when approving a zero allowance", async function () {
|
||||
await this.mock.$safeApprove(this.token.address, spender, 0);
|
||||
expect(await this.token.allowance(this.mock.address, spender)).to.be.bignumber.equal('0');
|
||||
});
|
||||
|
||||
it("doesn't revert when force approving a non-zero allowance", async function () {
|
||||
await this.mock.$forceApprove(this.token.address, spender, 20);
|
||||
expect(await this.token.allowance(this.mock.address, spender)).to.be.bignumber.equal('20');
|
||||
});
|
||||
|
||||
it("doesn't revert when force approving a zero allowance", async function () {
|
||||
await this.mock.$forceApprove(this.token.address, spender, 0);
|
||||
expect(await this.token.allowance(this.mock.address, spender)).to.be.bignumber.equal('0');
|
||||
});
|
||||
|
||||
it("doesn't revert when increasing the allowance", async function () {
|
||||
await this.mock.$safeIncreaseAllowance(this.token.address, spender, 10);
|
||||
expect(await this.token.allowance(this.mock.address, spender)).to.be.bignumber.equal('110');
|
||||
});
|
||||
|
||||
it("doesn't revert when decreasing the allowance to a positive value", async function () {
|
||||
await this.mock.$safeDecreaseAllowance(this.token.address, spender, 50);
|
||||
expect(await this.token.allowance(this.mock.address, spender)).to.be.bignumber.equal('50');
|
||||
});
|
||||
|
||||
it('reverts when decreasing the allowance to a negative value', async function () {
|
||||
await expectRevert(
|
||||
this.mock.$safeDecreaseAllowance(this.token.address, spender, 200),
|
||||
'SafeERC20: decreased allowance below zero',
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
const { BN, expectRevert, time } = require('@openzeppelin/test-helpers');
|
||||
|
||||
const { expect } = require('chai');
|
||||
|
||||
const ERC20 = artifacts.require('$ERC20');
|
||||
const TokenTimelock = artifacts.require('TokenTimelock');
|
||||
|
||||
contract('TokenTimelock', function (accounts) {
|
||||
const [beneficiary] = accounts;
|
||||
|
||||
const name = 'My Token';
|
||||
const symbol = 'MTKN';
|
||||
|
||||
const amount = new BN(100);
|
||||
|
||||
context('with token', function () {
|
||||
beforeEach(async function () {
|
||||
this.token = await ERC20.new(name, symbol);
|
||||
});
|
||||
|
||||
it('rejects a release time in the past', async function () {
|
||||
const pastReleaseTime = (await time.latest()).sub(time.duration.years(1));
|
||||
await expectRevert(
|
||||
TokenTimelock.new(this.token.address, beneficiary, pastReleaseTime),
|
||||
'TokenTimelock: release time is before current time',
|
||||
);
|
||||
});
|
||||
|
||||
context('once deployed', function () {
|
||||
beforeEach(async function () {
|
||||
this.releaseTime = (await time.latest()).add(time.duration.years(1));
|
||||
this.timelock = await TokenTimelock.new(this.token.address, beneficiary, this.releaseTime);
|
||||
await this.token.$_mint(this.timelock.address, amount);
|
||||
});
|
||||
|
||||
it('can get state', async function () {
|
||||
expect(await this.timelock.token()).to.equal(this.token.address);
|
||||
expect(await this.timelock.beneficiary()).to.equal(beneficiary);
|
||||
expect(await this.timelock.releaseTime()).to.be.bignumber.equal(this.releaseTime);
|
||||
});
|
||||
|
||||
it('cannot be released before time limit', async function () {
|
||||
await expectRevert(this.timelock.release(), 'TokenTimelock: current time is before release time');
|
||||
});
|
||||
|
||||
it('cannot be released just before time limit', async function () {
|
||||
await time.increaseTo(this.releaseTime.sub(time.duration.seconds(3)));
|
||||
await expectRevert(this.timelock.release(), 'TokenTimelock: current time is before release time');
|
||||
});
|
||||
|
||||
it('can be released just after limit', async function () {
|
||||
await time.increaseTo(this.releaseTime.add(time.duration.seconds(1)));
|
||||
await this.timelock.release();
|
||||
expect(await this.token.balanceOf(beneficiary)).to.be.bignumber.equal(amount);
|
||||
});
|
||||
|
||||
it('can be released after time limit', async function () {
|
||||
await time.increaseTo(this.releaseTime.add(time.duration.years(1)));
|
||||
await this.timelock.release();
|
||||
expect(await this.token.balanceOf(beneficiary)).to.be.bignumber.equal(amount);
|
||||
});
|
||||
|
||||
it('cannot be released twice', async function () {
|
||||
await time.increaseTo(this.releaseTime.add(time.duration.years(1)));
|
||||
await this.timelock.release();
|
||||
await expectRevert(this.timelock.release(), 'TokenTimelock: no tokens to release');
|
||||
expect(await this.token.balanceOf(beneficiary)).to.be.bignumber.equal(amount);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user