|
|
|
const { ethers } = require('hardhat');
|
|
|
|
const { expect } = require('chai');
|
|
|
|
const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers');
|
|
|
|
|
|
|
|
const name = 'My Token';
|
|
|
|
const symbol = 'MTKN';
|
|
|
|
const initialSupply = 100n;
|
|
|
|
const loanValue = 10_000_000_000_000n;
|
|
|
|
|
|
|
|
async function fixture() {
|
|
|
|
const [holder, other] = await ethers.getSigners();
|
|
|
|
|
|
|
|
const token = await ethers.deployContract('$ERC20FlashMintMock', [name, symbol]);
|
|
|
|
await token.$_mint(holder, initialSupply);
|
|
|
|
|
|
|
|
return { holder, other, token };
|
|
|
|
}
|
|
|
|
|
|
|
|
describe('ERC20FlashMint', function () {
|
|
|
|
beforeEach(async function () {
|
|
|
|
Object.assign(this, await loadFixture(fixture));
|
|
|
|
});
|
|
|
|
|
|
|
|
describe('maxFlashLoan', function () {
|
|
|
|
it('token match', async function () {
|
|
|
|
expect(await this.token.maxFlashLoan(this.token)).to.equal(ethers.MaxUint256 - initialSupply);
|
|
|
|
});
|
|
|
|
|
|
|
|
it('token mismatch', async function () {
|
|
|
|
expect(await this.token.maxFlashLoan(ethers.ZeroAddress)).to.equal(0n);
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
describe('flashFee', function () {
|
|
|
|
it('token match', async function () {
|
|
|
|
expect(await this.token.flashFee(this.token, loanValue)).to.equal(0n);
|
|
|
|
});
|
|
|
|
|
|
|
|
it('token mismatch', async function () {
|
|
|
|
await expect(this.token.flashFee(ethers.ZeroAddress, loanValue))
|
|
|
|
.to.be.revertedWithCustomError(this.token, 'ERC3156UnsupportedToken')
|
|
|
|
.withArgs(ethers.ZeroAddress);
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
describe('flashFeeReceiver', function () {
|
|
|
|
it('default receiver', async function () {
|
|
|
|
expect(await this.token.$_flashFeeReceiver()).to.equal(ethers.ZeroAddress);
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
describe('flashLoan', function () {
|
|
|
|
it('success', async function () {
|
|
|
|
const receiver = await ethers.deployContract('ERC3156FlashBorrowerMock', [true, true]);
|
|
|
|
|
|
|
|
const tx = await this.token.flashLoan(receiver, this.token, loanValue, '0x');
|
|
|
|
await expect(tx)
|
|
|
|
.to.emit(this.token, 'Transfer')
|
|
|
|
.withArgs(ethers.ZeroAddress, receiver, loanValue)
|
|
|
|
.to.emit(this.token, 'Transfer')
|
|
|
|
.withArgs(receiver, ethers.ZeroAddress, loanValue)
|
|
|
|
.to.emit(receiver, 'BalanceOf')
|
|
|
|
.withArgs(this.token, receiver, loanValue)
|
|
|
|
.to.emit(receiver, 'TotalSupply')
|
|
|
|
.withArgs(this.token, initialSupply + loanValue);
|
|
|
|
await expect(tx).to.changeTokenBalance(this.token, receiver, 0);
|
|
|
|
|
|
|
|
expect(await this.token.totalSupply()).to.equal(initialSupply);
|
|
|
|
expect(await this.token.allowance(receiver, this.token)).to.equal(0n);
|
|
|
|
});
|
|
|
|
|
|
|
|
it('missing return value', async function () {
|
|
|
|
const receiver = await ethers.deployContract('ERC3156FlashBorrowerMock', [false, true]);
|
|
|
|
await expect(this.token.flashLoan(receiver, this.token, loanValue, '0x'))
|
|
|
|
.to.be.revertedWithCustomError(this.token, 'ERC3156InvalidReceiver')
|
|
|
|
.withArgs(receiver);
|
|
|
|
});
|
|
|
|
|
|
|
|
it('missing approval', async function () {
|
|
|
|
const receiver = await ethers.deployContract('ERC3156FlashBorrowerMock', [true, false]);
|
|
|
|
await expect(this.token.flashLoan(receiver, this.token, loanValue, '0x'))
|
|
|
|
.to.be.revertedWithCustomError(this.token, 'ERC20InsufficientAllowance')
|
|
|
|
.withArgs(this.token, 0, loanValue);
|
|
|
|
});
|
|
|
|
|
|
|
|
it('unavailable funds', async function () {
|
|
|
|
const receiver = await ethers.deployContract('ERC3156FlashBorrowerMock', [true, true]);
|
|
|
|
const data = this.token.interface.encodeFunctionData('transfer', [this.other.address, 10]);
|
|
|
|
await expect(this.token.flashLoan(receiver, this.token, loanValue, data))
|
|
|
|
.to.be.revertedWithCustomError(this.token, 'ERC20InsufficientBalance')
|
|
|
|
.withArgs(receiver, loanValue - 10n, loanValue);
|
|
|
|
});
|
|
|
|
|
|
|
|
it('more than maxFlashLoan', async function () {
|
|
|
|
const receiver = await ethers.deployContract('ERC3156FlashBorrowerMock', [true, true]);
|
|
|
|
const data = this.token.interface.encodeFunctionData('transfer', [this.other.address, 10]);
|
|
|
|
await expect(this.token.flashLoan(receiver, this.token, ethers.MaxUint256, data))
|
|
|
|
.to.be.revertedWithCustomError(this.token, 'ERC3156ExceededMaxLoan')
|
|
|
|
.withArgs(ethers.MaxUint256 - initialSupply);
|
|
|
|
});
|
|
|
|
|
|
|
|
describe('custom flash fee & custom fee receiver', function () {
|
|
|
|
const receiverInitialBalance = 200_000n;
|
|
|
|
const flashFee = 5_000n;
|
|
|
|
|
|
|
|
beforeEach('init receiver balance & set flash fee', async function () {
|
|
|
|
this.receiver = await ethers.deployContract('ERC3156FlashBorrowerMock', [true, true]);
|
|
|
|
|
|
|
|
const tx = await this.token.$_mint(this.receiver, receiverInitialBalance);
|
|
|
|
await expect(tx)
|
|
|
|
.to.emit(this.token, 'Transfer')
|
|
|
|
.withArgs(ethers.ZeroAddress, this.receiver, receiverInitialBalance);
|
|
|
|
await expect(tx).to.changeTokenBalance(this.token, this.receiver, receiverInitialBalance);
|
|
|
|
|
|
|
|
await this.token.setFlashFee(flashFee);
|
|
|
|
expect(await this.token.flashFee(this.token, loanValue)).to.equal(flashFee);
|
|
|
|
});
|
|
|
|
|
|
|
|
it('default flash fee receiver', async function () {
|
|
|
|
const tx = await this.token.flashLoan(this.receiver, this.token, loanValue, '0x');
|
|
|
|
await expect(tx)
|
|
|
|
.to.emit(this.token, 'Transfer')
|
|
|
|
.withArgs(ethers.ZeroAddress, this.receiver, loanValue)
|
|
|
|
.to.emit(this.token, 'Transfer')
|
|
|
|
.withArgs(this.receiver, ethers.ZeroAddress, loanValue + flashFee)
|
|
|
|
.to.emit(this.receiver, 'BalanceOf')
|
|
|
|
.withArgs(this.token, this.receiver, receiverInitialBalance + loanValue)
|
|
|
|
.to.emit(this.receiver, 'TotalSupply')
|
|
|
|
.withArgs(this.token, initialSupply + receiverInitialBalance + loanValue);
|
|
|
|
await expect(tx).to.changeTokenBalances(this.token, [this.receiver, ethers.ZeroAddress], [-flashFee, 0]);
|
|
|
|
|
|
|
|
expect(await this.token.totalSupply()).to.equal(initialSupply + receiverInitialBalance - flashFee);
|
|
|
|
expect(await this.token.allowance(this.receiver, this.token)).to.equal(0n);
|
|
|
|
});
|
|
|
|
|
|
|
|
it('custom flash fee receiver', async function () {
|
|
|
|
const flashFeeReceiverAddress = this.other;
|
|
|
|
await this.token.setFlashFeeReceiver(flashFeeReceiverAddress);
|
|
|
|
expect(await this.token.$_flashFeeReceiver()).to.equal(flashFeeReceiverAddress);
|
|
|
|
|
|
|
|
const tx = await this.token.flashLoan(this.receiver, this.token, loanValue, '0x');
|
|
|
|
await expect(tx)
|
|
|
|
.to.emit(this.token, 'Transfer')
|
|
|
|
.withArgs(ethers.ZeroAddress, this.receiver, loanValue)
|
|
|
|
.to.emit(this.token, 'Transfer')
|
|
|
|
.withArgs(this.receiver, ethers.ZeroAddress, loanValue)
|
|
|
|
.to.emit(this.token, 'Transfer')
|
|
|
|
.withArgs(this.receiver, flashFeeReceiverAddress, flashFee)
|
|
|
|
.to.emit(this.receiver, 'BalanceOf')
|
|
|
|
.withArgs(this.token, this.receiver, receiverInitialBalance + loanValue)
|
|
|
|
.to.emit(this.receiver, 'TotalSupply')
|
|
|
|
.withArgs(this.token, initialSupply + receiverInitialBalance + loanValue);
|
|
|
|
await expect(tx).to.changeTokenBalances(
|
|
|
|
this.token,
|
|
|
|
[this.receiver, flashFeeReceiverAddress],
|
|
|
|
[-flashFee, flashFee],
|
|
|
|
);
|
|
|
|
|
|
|
|
expect(await this.token.totalSupply()).to.equal(initialSupply + receiverInitialBalance);
|
|
|
|
expect(await this.token.allowance(this.receiver, flashFeeReceiverAddress)).to.equal(0n);
|
|
|
|
});
|
|
|
|
});
|
|
|
|
});
|
|
|
|
});
|