diff --git a/CHANGELOG.md b/CHANGELOG.md index 5e6dc286c..8d6d8ed1c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ ## vNEXT - Remove `smock` from unit tests: + - IexecEscrow.v8 (#154) - IexecPocoDelegate (#149, #151) - IexecPocoBoost (#148, #150, #153) - Migrate unit test files to Typescript & Hardhat: diff --git a/contracts/tools/testing/IexecEscrowTestContract.sol b/contracts/tools/testing/IexecEscrowTestContract.sol index 961abcf0b..58098036f 100644 --- a/contracts/tools/testing/IexecEscrowTestContract.sol +++ b/contracts/tools/testing/IexecEscrowTestContract.sol @@ -25,4 +25,21 @@ contract IexecEscrowTestContract is IexecEscrow { function seize_(address account, uint256 value, bytes32 ref) external { seize(account, value, ref); } + + // Helper functions used in unit tests. + + function setBalance(address account, uint256 value) external { + m_balances[account] = value; + } + + // TODO remove the following function and inherit `IexecAccessorsDelegate` + // when it is migrated to solidity v8. + + function balanceOf(address account) external view returns (uint256) { + return m_balances[account]; + } + + function frozenOf(address account) external view returns (uint256) { + return m_frozens[account]; + } } diff --git a/deploy/0_deploy.ts b/deploy/0_deploy.ts index f9cd8d2c6..d79e9e6d2 100644 --- a/deploy/0_deploy.ts +++ b/deploy/0_deploy.ts @@ -5,7 +5,6 @@ import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers'; import fs from 'fs'; import hre, { ethers } from 'hardhat'; import path from 'path'; -import { getFunctionSignatures } from '../migrations/utils/getFunctionSignatures'; import { AppRegistry__factory, DatasetRegistry__factory, @@ -40,8 +39,8 @@ import { } from '../typechain'; import { Ownable__factory } from '../typechain/factories/@openzeppelin/contracts/access'; import { FactoryDeployerHelper } from '../utils/FactoryDeployerHelper'; -import { getBaseNameFromContractFactory } from '../utils/deploy-tools'; import { Category } from '../utils/poco-tools'; +import { linkContractToProxy } from '../utils/proxy-tools'; const CONFIG = require('../config/config.json'); /** @@ -231,34 +230,9 @@ async function getOrDeployRlc(token: string, owner: SignerWithAddress) { }); } -/** - * Link a contract to an ERC1538 proxy. - * @param proxy contract to ERC1538 proxy. - * @param contractAddress The contract address to link to the proxy. - * @param contractFactory The contract factory to link to the proxy. - */ -async function linkContractToProxy( - proxy: ERC1538Update, - contractAddress: string, - contractFactory: any, -) { - const contractName = getBaseNameFromContractFactory(contractFactory); - await proxy - .updateContract( - contractAddress, - // TODO: Use contractFactory.interface.functions when moving to ethers@v6 - // https://github.com/ethers-io/ethers.js/issues/1069 - getFunctionSignatures(contractFactory.constructor.abi), - 'Linking ' + contractName, - ) - .then((tx) => tx.wait()) - .catch(() => { - throw new Error(`Failed to link ${contractName}`); - }); -} - // TODO [optional]: Use hardhat-deploy to save addresses automatically // https://github.com/wighawag/hardhat-deploy/tree/master#hardhat-deploy-in-a-nutshell +// TODO remove this. /** * Save addresses of deployed contracts (since hardhat does not do it for us). * @param contractName contract name to deploy diff --git a/test/byContract/IexecPocoBoost/IexecEscrow.v8.test.ts b/test/byContract/IexecPocoBoost/IexecEscrow.v8.test.ts index 41529a691..03b172819 100644 --- a/test/byContract/IexecPocoBoost/IexecEscrow.v8.test.ts +++ b/test/byContract/IexecPocoBoost/IexecEscrow.v8.test.ts @@ -1,60 +1,51 @@ -import { MockContract, smock } from '@defi-wonderland/smock'; +// SPDX-FileCopyrightText: 2023-2024 IEXEC BLOCKCHAIN TECH +// SPDX-License-Identifier: Apache-2.0 + +import { loadFixture } from '@nomicfoundation/hardhat-network-helpers'; import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers'; import { constants } from 'ethers'; -import { ethers, expect } from 'hardhat'; +import { expect } from 'hardhat'; import { IexecEscrowTestContract, IexecEscrowTestContract__factory } from '../../../typechain'; +import { getIexecAccounts } from '../../../utils/poco-tools'; + +const userBalance = 1000; +const amount = 3; +const ref = constants.HashZero; // TODO remove this and use HashZero -const BALANCES = 'm_balances'; -const FROZENS = 'm_frozens'; +let iexecEscrow: IexecEscrowTestContract; +let user: SignerWithAddress; describe('IexecEscrow.v8', function () { - // Using 0 causes an error when checking initial balance. - const initialEscrowBalance = 10; - const initialUserBalance = 20; - const initialUserFrozen = 30; - const amount = 3; - const ref = constants.HashZero; - - let deployer: SignerWithAddress; - let user: SignerWithAddress; - let iexecEscrow: MockContract; - - beforeEach(async function () { - // Get wallets. - [deployer, user] = await ethers.getSigners(); - // Deploy the contract to be tested as a mock. - iexecEscrow = (await smock - .mock('IexecEscrowTestContract') - .then((instance) => instance.deploy()) - .then((contract) => contract.deployed())) as MockContract; - // Set initial state of contract. - await iexecEscrow.setVariables({ - [BALANCES]: { - [iexecEscrow.address]: initialEscrowBalance, - [user.address]: initialUserBalance, - }, - [FROZENS]: { - [user.address]: initialUserFrozen, - }, - }); + beforeEach('Deploy', async () => { + // Initialize test environment + await loadFixture(initFixture); }); + async function initFixture() { + user = (await getIexecAccounts()).anyone; + // Deploy test contract to make internal escrow functions accessible. + iexecEscrow = await new IexecEscrowTestContract__factory() + .connect(user) // Anyone works. + .deploy() + .then((contract) => contract.deployed()); + // Deposit some funds in the user's account. + await iexecEscrow.setBalance(user.address, userBalance).then((tx) => tx.wait()); + } + describe('Lock', function () { it('Should lock funds', async function () { - // Check balances before the operation. - await checkInitialBalancesAndFrozens(); - // Run operation. + const frozenBefore = (await iexecEscrow.frozenOf(user.address)).toNumber(); await expect(iexecEscrow.lock_(user.address, amount)) + .to.changeTokenBalances( + iexecEscrow, + [iexecEscrow.address, user.address], + [amount, -amount], + ) .to.emit(iexecEscrow, 'Transfer') .withArgs(user.address, iexecEscrow.address, amount) .to.emit(iexecEscrow, 'Lock') .withArgs(user.address, amount); - // Check balances after the operation. - await checkBalancesAndFrozens( - initialEscrowBalance + amount, - initialUserBalance - amount, - initialUserFrozen + amount, - ); + expect(await iexecEscrow.frozenOf(user.address)).to.equal(frozenBefore + amount); }); it('Should not lock funds for empty address', async function () { @@ -64,28 +55,29 @@ describe('IexecEscrow.v8', function () { }); it('Should not lock funds when insufficient balance', async function () { - await expect( - iexecEscrow.lock_(user.address, initialUserBalance + 1), - ).to.be.revertedWith('IexecEscrow: Transfer amount exceeds balance'); + await expect(iexecEscrow.lock_(user.address, userBalance + 1)).to.be.revertedWith( + 'IexecEscrow: Transfer amount exceeds balance', + ); }); }); describe('Unlock', function () { it('Should unlock funds', async function () { - // Check balances before the operation. - await checkInitialBalancesAndFrozens(); - // Run operation. + // Lock some user funds to be able to unlock. + await iexecEscrow.lock_(user.address, userBalance).then((tx) => tx.wait()); + + const frozenBefore = (await iexecEscrow.frozenOf(user.address)).toNumber(); await expect(iexecEscrow.unlock_(user.address, amount)) + .to.changeTokenBalances( + iexecEscrow, + [iexecEscrow.address, user.address], + [-amount, amount], + ) .to.emit(iexecEscrow, 'Transfer') .withArgs(iexecEscrow.address, user.address, amount) .to.emit(iexecEscrow, 'Unlock') .withArgs(user.address, amount); - // Check balances after the operation. - await checkBalancesAndFrozens( - initialEscrowBalance - amount, - initialUserBalance + amount, - initialUserFrozen - amount, - ); + expect(await iexecEscrow.frozenOf(user.address)).to.equal(frozenBefore - amount); }); it('Should not unlock funds for empty address', async function () { @@ -95,28 +87,27 @@ describe('IexecEscrow.v8', function () { }); it('Should not unlock funds when insufficient balance', async function () { - await expect( - iexecEscrow.unlock_(user.address, initialUserFrozen + 1), - ).to.be.revertedWith('IexecEscrow: Transfer amount exceeds balance'); + await expect(iexecEscrow.unlock_(user.address, amount)).to.be.revertedWith( + 'IexecEscrow: Transfer amount exceeds balance', + ); }); }); describe('Reward', function () { it('Should reward', async function () { - // Check balances before the operation. - await checkInitialBalancesAndFrozens(); - // Run operation. + // Fund iexecEscrow so it can reward the user. + await iexecEscrow.setBalance(iexecEscrow.address, amount).then((tx) => tx.wait()); + await expect(iexecEscrow.reward_(user.address, amount, ref)) + .to.changeTokenBalances( + iexecEscrow, + [iexecEscrow.address, user.address], + [-amount, amount], + ) .to.emit(iexecEscrow, 'Transfer') .withArgs(iexecEscrow.address, user.address, amount) .to.emit(iexecEscrow, 'Reward') .withArgs(user.address, amount, ref); - // Check balances after the operation. - await checkBalancesAndFrozens( - initialEscrowBalance - amount, - initialUserBalance + amount, - initialUserFrozen, - ); }); it('Should not reward empty address', async function () { @@ -126,26 +117,23 @@ describe('IexecEscrow.v8', function () { }); it('Should not reward when insufficient balance', async function () { - await expect( - iexecEscrow.reward_(user.address, initialEscrowBalance + 1, ref), - ).to.be.revertedWith('IexecEscrow: Transfer amount exceeds balance'); + await expect(iexecEscrow.reward_(user.address, amount, ref)).to.be.revertedWith( + 'IexecEscrow: Transfer amount exceeds balance', + ); }); }); describe('Seize', function () { it('Should seize funds', async function () { - // Check balances before the operation. - await checkInitialBalancesAndFrozens(); - // Run operation. + // Lock some user funds to be able to seize. + await iexecEscrow.lock_(user.address, userBalance).then((tx) => tx.wait()); + + const frozenBefore = (await iexecEscrow.frozenOf(user.address)).toNumber(); await expect(iexecEscrow.seize_(user.address, amount, ref)) + .to.changeTokenBalances(iexecEscrow, [iexecEscrow.address, user.address], [0, 0]) .to.emit(iexecEscrow, 'Seize') .withArgs(user.address, amount, ref); - // Check balances after the operation. - await checkBalancesAndFrozens( - initialEscrowBalance, - initialUserBalance, - initialUserFrozen - amount, - ); + expect(await iexecEscrow.frozenOf(user.address)).to.equal(frozenBefore - amount); }); it('Should not seize funds for empty address', async function () { @@ -155,25 +143,9 @@ describe('IexecEscrow.v8', function () { }); it('Should not seize funds when insufficient balance', async function () { - await expect( - iexecEscrow.seize_(user.address, initialUserFrozen + 1, ref), - ).to.be.revertedWithPanic(0x11); + await expect(iexecEscrow.seize_(user.address, amount, ref)).to.be.revertedWithPanic( + 0x11, + ); }); }); - - async function checkInitialBalancesAndFrozens() { - checkBalancesAndFrozens(initialEscrowBalance, initialUserBalance, initialUserFrozen); - } - - async function checkBalancesAndFrozens( - escrowBalance: number, - userBalance: number, - userFrozen: number, - ) { - expect(await iexecEscrow.getVariable(BALANCES, [iexecEscrow.address])).to.be.equal( - escrowBalance, - ); - expect(await iexecEscrow.getVariable(BALANCES, [user.address])).to.be.equal(userBalance); - expect(await iexecEscrow.getVariable(FROZENS, [user.address])).to.be.equal(userFrozen); - } }); diff --git a/utils/deploy-tools.ts b/utils/deploy-tools.ts index 45e500e2a..02fdb7fcd 100644 --- a/utils/deploy-tools.ts +++ b/utils/deploy-tools.ts @@ -6,7 +6,7 @@ import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers'; import { deployments } from 'hardhat'; /** - * Deploy a contract. + * Deploy a contract and save its deployment. * @param contractFactory The contract to deploy * @param deployer The signer to deploy the contract * @param constructorArgs Arguments passed to the contract constructor at deployment diff --git a/utils/proxy-tools.ts b/utils/proxy-tools.ts new file mode 100644 index 000000000..53e31c9a6 --- /dev/null +++ b/utils/proxy-tools.ts @@ -0,0 +1,32 @@ +// SPDX-FileCopyrightText: 2024 IEXEC BLOCKCHAIN TECH +// SPDX-License-Identifier: Apache-2.0 + +import { getFunctionSignatures } from '../migrations/utils/getFunctionSignatures'; +import { ERC1538Update } from '../typechain'; +import { getBaseNameFromContractFactory } from '../utils/deploy-tools'; + +/** + * Link a contract to an ERC1538 proxy. + * @param proxy contract to ERC1538 proxy. + * @param contractAddress The contract address to link to the proxy. + * @param contractFactory The contract factory to link to the proxy. + */ +export async function linkContractToProxy( + proxy: ERC1538Update, + contractAddress: string, + contractFactory: any, +) { + const contractName = getBaseNameFromContractFactory(contractFactory); + await proxy + .updateContract( + contractAddress, + // TODO: Use contractFactory.interface.functions when moving to ethers@v6 + // https://github.com/ethers-io/ethers.js/issues/1069 + getFunctionSignatures(contractFactory.constructor.abi), + 'Linking ' + contractName, + ) + .then((tx) => tx.wait()) + .catch(() => { + throw new Error(`Failed to link ${contractName}`); + }); +}