Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

## vNEXT
- Migrate unit test files to Typescript & Hardhat:
- IexecEscrowToken (#141)
- IexecRelay (#140)
- IexecPoco1 (#136, #137)
- IexecPoco2
Expand Down
377 changes: 377 additions & 0 deletions test/byContract/IexecEscrow/IexecEscrowToken.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,377 @@
// SPDX-FileCopyrightText: 2024 IEXEC BLOCKCHAIN TECH <contact@iex.ec>
// SPDX-License-Identifier: Apache-2.0

import { AddressZero } from '@ethersproject/constants';
import { loadFixture } from '@nomicfoundation/hardhat-network-helpers';
import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers';
import { BigNumber } from 'ethers';
import { ethers, expect } from 'hardhat';
import { loadHardhatFixtureDeployment } from '../../../scripts/hardhat-fixture-deployer';
import {
IexecInterfaceToken,
IexecInterfaceToken__factory,
RLC,
RLC__factory,
} from '../../../typechain';
import { getIexecAccounts } from '../../../utils/poco-tools';

const amount = ethers.utils.parseUnits(BigNumber.from(100).toString(), 9);

describe('IexecEscrowToken', () => {
let proxyAddress: string;
let [iexecPoco, , iexecPocoAsAccountA, iexecPocoAsAdmin]: IexecInterfaceToken[] = [];
let [iexecAdmin, accountA, accountB, accountC, anyone]: SignerWithAddress[] = [];
let [rlcInstance, rlcInstanceAsAccountA]: RLC[] = [];

beforeEach('Deploy', async () => {
proxyAddress = await loadHardhatFixtureDeployment();
await loadFixture(initFixture);
});

async function initFixture() {
const accounts = await getIexecAccounts();
({
iexecAdmin,
anyone: accountA,
requester: accountB,
sponsor: accountC,
anyone,
} = accounts);

iexecPoco = IexecInterfaceToken__factory.connect(proxyAddress, anyone);
iexecPocoAsAccountA = iexecPoco.connect(accountA);
iexecPocoAsAdmin = iexecPoco.connect(iexecAdmin);
rlcInstance = RLC__factory.connect(await iexecPoco.token(), anyone);
rlcInstanceAsAccountA = rlcInstance.connect(accountA);
await rlcInstance
.connect(iexecAdmin)
.transfer(accountA.address, amount.mul(10)) //enough to cover tests.
.then((tx) => tx.wait());
}

describe('Receive and Fallback', () => {
it('Should revert on receive', async () => {
await expect(
accountA.sendTransaction({
to: iexecPoco.address,
value: amount,
}),
).to.be.revertedWith('fallback-disabled');
});
it('Should revert on fallback', async () => {
const randomData = ethers.utils.hexlify(
ethers.utils.toUtf8Bytes((Math.random() * 0xfffff).toString(16)),
);
await expect(
accountA.sendTransaction({
to: iexecPoco.address,
value: amount,
data: randomData,
}),
).to.be.revertedWith('fallback-disabled');
});
});

describe('Deposit', () => {
it('Should deposit tokens', async () => {
await rlcInstanceAsAccountA.approve(iexecPoco.address, amount).then((tx) => tx.wait());
const initialTotalSupply = await iexecPoco.totalSupply();

expect(await iexecPocoAsAccountA.callStatic.deposit(amount)).to.be.true;
await expect(iexecPocoAsAccountA.deposit(amount))
.to.changeTokenBalances(rlcInstance, [accountA, iexecPoco], [-amount, amount])
.to.emit(rlcInstance, 'Transfer')
.withArgs(accountA.address, iexecPoco, amount)
.to.changeTokenBalances(iexecPoco, [accountA], [amount])
.to.emit(iexecPoco, 'Transfer')
.withArgs(AddressZero, accountA.address, amount);
expect(await iexecPoco.totalSupply()).to.equal(initialTotalSupply.add(amount));
});
it('Should deposit 0 token', async () => {
expect(await iexecPocoAsAccountA.callStatic.deposit(0)).to.be.true;
await expect(iexecPocoAsAccountA.deposit(0))
.to.changeTokenBalances(rlcInstance, [accountA, iexecPoco], [-0, 0])
.to.emit(rlcInstance, 'Transfer')
.withArgs(accountA.address, iexecPoco, 0)
.to.changeTokenBalances(iexecPoco, [accountA], [0])
.to.emit(iexecPoco, 'Transfer')
.withArgs(AddressZero, accountA.address, 0);
});
it('Should not deposit tokens when caller is address 0', async () => {
const addressZeroSigner = await ethers.getImpersonatedSigner(AddressZero);
await rlcInstance
.connect(iexecAdmin)
.transfer(addressZeroSigner.address, amount)
.then((tx) => tx.wait());
// send some gas token
iexecAdmin
.sendTransaction({
to: addressZeroSigner.address,
value: ethers.utils.parseUnits(BigNumber.from(100_000).toString(), 9),
})
.then((tx) => tx.wait());

await rlcInstance
.connect(addressZeroSigner)
.approve(iexecPoco.address, amount)
.then((tx) => tx.wait());

await expect(iexecPoco.connect(addressZeroSigner).deposit(amount)).to.be.revertedWith(
'ERC20: mint to the zero address',
);
});
});

describe('Deposit for', () => {
it('Should deposit tokens for another account', async () => {
await rlcInstanceAsAccountA.approve(iexecPoco.address, amount).then((tx) => tx.wait());
const initialTotalSupply = await iexecPoco.totalSupply();

const depositForParams = {
amount: amount,
target: accountB.address,
};
const depositForArgs = Object.values(depositForParams) as [BigNumber, string];

expect(await iexecPocoAsAccountA.callStatic.depositFor(...depositForArgs)).to.be.true;
await expect(iexecPocoAsAccountA.depositFor(...depositForArgs))
.to.changeTokenBalances(
rlcInstance,
[accountA, iexecPoco],
[-depositForParams.amount, depositForParams.amount],
)
.to.emit(rlcInstance, 'Transfer')
.withArgs(accountA.address, iexecPoco, depositForParams.amount)
.to.changeTokenBalances(
iexecPoco,
[depositForParams.target],
[depositForParams.amount],
)
.to.emit(iexecPoco, 'Transfer')
.withArgs(AddressZero, depositForParams.target, depositForParams.amount);
expect(await iexecPoco.totalSupply()).to.equal(
initialTotalSupply.add(depositForParams.amount),
);
});
it('Should not deposit tokens for zero address', async () => {
await rlcInstanceAsAccountA.approve(iexecPoco.address, amount).then((tx) => tx.wait());
await expect(iexecPocoAsAccountA.depositFor(amount, AddressZero)).to.be.revertedWith(
'ERC20: mint to the zero address',
);
});
});

describe('Deposit for array', () => {
it('Should deposit tokens for multiple accounts', async () => {
const depositForArrayParams = {
amounts: [amount, amount.mul(2)],
targets: [accountB.address, accountC.address],
};
const depositForArrayArgs = Object.values(depositForArrayParams) as [
BigNumber[],
string[],
];
const depositTotalAmount = getTotalAmount(depositForArrayParams.amounts);
const initialTotalSupply = await iexecPoco.totalSupply();

await rlcInstanceAsAccountA
.approve(iexecPoco.address, depositTotalAmount)
.then((tx) => tx.wait());
expect(await iexecPocoAsAccountA.callStatic.depositForArray(...depositForArrayArgs)).to
.be.true;
await expect(iexecPocoAsAccountA.depositForArray(...depositForArrayArgs))
.to.changeTokenBalances(
rlcInstance,
[accountA, iexecPoco],
[-depositTotalAmount, depositTotalAmount],
)
.to.emit(rlcInstance, 'Transfer')
.withArgs(accountA.address, iexecPoco, depositTotalAmount)
.to.changeTokenBalances(
iexecPoco,
[...depositForArrayParams.targets],
[...depositForArrayParams.amounts],
)
.to.emit(iexecPoco, 'Transfer')
.withArgs(
AddressZero,
depositForArrayParams.targets[0],
depositForArrayParams.amounts[0],
)
.to.emit(iexecPoco, 'Transfer')
.withArgs(
AddressZero,
depositForArrayParams.targets[1],
depositForArrayParams.amounts[1],
);
expect(await iexecPoco.totalSupply()).to.equal(
initialTotalSupply.add(depositTotalAmount),
);
});
it('Should not deposit tokens for multiple accounts with mismatched array lengths', async () => {
const depositForArrayParams = {
amounts: [amount.mul(2), amount, amount.div(2)],
targets: [accountB.address, accountC.address],
};
const depositForArrayArgs = Object.values(depositForArrayParams) as [
BigNumber[],
string[],
];
const depositTotalAmount = getTotalAmount(depositForArrayParams.amounts);

await rlcInstanceAsAccountA
.approve(iexecPoco.address, depositTotalAmount)
.then((tx) => tx.wait());
await expect(
iexecPocoAsAccountA.depositForArray(...depositForArrayArgs),
).to.be.revertedWith('invalid-array-length');
});
it('Should not deposit tokens for multiple accounts with address zero in target', async () => {
const depositForArrayParams = {
amounts: [amount, amount.mul(2)],
targets: [AddressZero, accountB.address],
};
const depositForArrayArgs = Object.values(depositForArrayParams) as [
BigNumber[],
string[],
];
const depositTotalAmount = getTotalAmount(depositForArrayParams.amounts);

await rlcInstanceAsAccountA
.approve(iexecPoco.address, depositTotalAmount)
.then((tx) => tx.wait());
await expect(
iexecPocoAsAccountA.depositForArray(...depositForArrayArgs),
).to.be.revertedWith('ERC20: mint to the zero address');
});
});

describe('Withdraw', () => {
it('Should withdraw tokens', async () => {
await rlcInstanceAsAccountA.approve(iexecPoco.address, amount).then((tx) => tx.wait());
await iexecPocoAsAccountA.deposit(amount).then((tx) => tx.wait());
const initialTotalSupply = await iexecPoco.totalSupply();

expect(await iexecPocoAsAccountA.callStatic.withdraw(amount)).to.be.true;
await expect(iexecPocoAsAccountA.withdraw(amount))
.to.changeTokenBalances(iexecPoco, [accountA], [-amount])
.to.emit(iexecPoco, 'Transfer')
.withArgs(accountA.address, AddressZero, amount)
.to.changeTokenBalances(rlcInstance, [iexecPoco, accountA], [-amount, amount])
.to.emit(rlcInstance, 'Transfer')
.withArgs(iexecPoco.address, accountA.address, amount);
expect(await iexecPoco.totalSupply()).to.equal(initialTotalSupply.sub(amount));
});
it('Should withdraw zero token', async () => {
expect(await iexecPocoAsAccountA.callStatic.withdraw(0)).to.be.true;
await expect(iexecPocoAsAccountA.withdraw(0))
.to.changeTokenBalances(iexecPoco, [accountA], [-0])
.to.emit(iexecPoco, 'Transfer')
.withArgs(accountA.address, AddressZero, 0)
.to.changeTokenBalances(rlcInstance, [iexecPoco, accountA], [-0, 0])
.to.emit(rlcInstance, 'Transfer')
.withArgs(iexecPoco.address, accountA.address, 0);
});
it('Should not withdraw native tokens with empty balance', async () => {
await expect(iexecPocoAsAccountA.withdraw(amount)).to.be.revertedWithoutReason();
});
it('Should not withdraw tokens with insufficient balance', async () => {
await rlcInstanceAsAccountA.approve(iexecPoco.address, amount).then((tx) => tx.wait());
await iexecPocoAsAccountA.deposit(amount).then((tx) => tx.wait());

await expect(iexecPocoAsAccountA.withdraw(amount.add(1))).to.be.revertedWithoutReason();
});
});

describe('Withdraw to', () => {
it('Should withdraw to another address', async () => {
await rlcInstanceAsAccountA.approve(iexecPoco.address, amount).then((tx) => tx.wait());
await iexecPocoAsAccountA.deposit(amount).then((tx) => tx.wait());
const initialTotalSupply = await iexecPoco.totalSupply();

const withdrawToParams = {
amount: amount,
target: accountB.address,
};
const withdrawToArgs = Object.values(withdrawToParams) as [BigNumber, string];

expect(await iexecPocoAsAccountA.callStatic.withdrawTo(...withdrawToArgs)).to.be.true;
await expect(iexecPocoAsAccountA.withdrawTo(...withdrawToArgs))
.to.changeTokenBalances(iexecPoco, [accountA], [-amount])
.to.emit(iexecPoco, 'Transfer')
.withArgs(accountA.address, AddressZero, withdrawToParams.amount)
.to.changeTokenBalances(
rlcInstance,
[iexecPoco, withdrawToParams.target],
[-withdrawToParams.amount, withdrawToParams.amount],
)
.to.emit(rlcInstance, 'Transfer')
.withArgs(iexecPoco.address, withdrawToParams.target, withdrawToParams.amount);
expect(await iexecPoco.totalSupply()).to.equal(
initialTotalSupply.sub(withdrawToParams.amount),
);
});
it('Should withdraw to another address with zero token', async () => {
const withdrawToParams = {
amount: 0,
target: accountB.address,
};
const withdrawToArgs = Object.values(withdrawToParams) as [BigNumber, string];

expect(await iexecPocoAsAccountA.callStatic.withdrawTo(...withdrawToArgs)).to.be.true;
await expect(iexecPocoAsAccountA.withdrawTo(...withdrawToArgs))
.to.changeTokenBalances(iexecPoco, [accountA], [-0])
.to.emit(iexecPoco, 'Transfer')
.withArgs(accountA.address, AddressZero, 0)
.to.changeTokenBalances(rlcInstance, [iexecPoco, accountB], [-0, 0])
.to.emit(rlcInstance, 'Transfer')
.withArgs(iexecPoco.address, withdrawToParams.target, 0);
});
it('Should not withdraw to another address with empty balance', async () => {
await expect(
iexecPocoAsAccountA.withdrawTo(amount, accountB.address),
).to.be.revertedWithoutReason();
});
it('Should not withdraw to another address with insufficient balance', async () => {
await rlcInstanceAsAccountA.approve(iexecPoco.address, amount).then((tx) => tx.wait());
await iexecPocoAsAccountA.deposit(amount).then((tx) => tx.wait());

await expect(
iexecPocoAsAccountA.withdrawTo(amount.add(1), accountB.address),
).to.be.revertedWithoutReason();
});
});

describe('Recover', () => {
it('Should recover from balance deviation', async () => {
await rlcInstance.connect(iexecAdmin).transfer(proxyAddress, amount); // Simulate deviation

const initTotalSupply = await iexecPoco.totalSupply();
const expectedDelta = amount;

await expect(iexecPocoAsAdmin.recover())
.to.changeTokenBalances(iexecPoco, [iexecAdmin], [expectedDelta])
.to.emit(iexecPoco, 'Transfer')
.withArgs(AddressZero, iexecAdmin.address, expectedDelta);
expect(await iexecPoco.totalSupply()).to.equal(initTotalSupply.add(expectedDelta));
});
it('Should recover 0 token when balance matches total supply', async () => {
const initialSupply = await iexecPoco.totalSupply();

await expect(iexecPocoAsAdmin.recover())
.to.changeTokenBalances(iexecPoco, [iexecAdmin], [0])
.to.emit(iexecPoco, 'Transfer')
.withArgs(AddressZero, iexecAdmin.address, 0);
expect(await iexecPoco.totalSupply()).to.equal(initialSupply);
});
it('Should not recover token when caller is not the owner', async () => {
await expect(iexecPocoAsAccountA.recover()).to.be.revertedWith(
'Ownable: caller is not the owner',
);
});
});
});

function getTotalAmount(amounts: BigNumber[]) {
return amounts.reduce((a, b) => a.add(b), BigNumber.from(0));
}