diff --git a/test/000_fullchain-boost.test.ts b/test/000_fullchain-boost.test.ts index 186cc9e6..c5f8871c 100644 --- a/test/000_fullchain-boost.test.ts +++ b/test/000_fullchain-boost.test.ts @@ -38,7 +38,7 @@ import { } from '../utils/poco-tools'; import { IexecWrapper } from './utils/IexecWrapper'; import { loadHardhatFixtureDeployment } from './utils/hardhat-fixture-deployer'; -import { randomAddress } from './utils/utils'; +import { expectProxyBalanceToEqualAllFrozen, randomAddress } from './utils/utils'; const taskIndex = 0n; const volume = taskIndex + 1n; @@ -109,6 +109,10 @@ describe('IexecPocoBoostFacet (IT)', function () { await iexecWrapper.setTeeBroker('0x0000000000000000000000000000000000000000'); }); + afterEach('Check escrow invariant', async () => { + await expectProxyBalanceToEqualAllFrozen(proxyAddress); + }); + async function initFixture() { const accounts = await getIexecAccounts(); ({ diff --git a/test/000_fullchain.test.ts b/test/000_fullchain.test.ts index ddda44e8..3d91b38a 100644 --- a/test/000_fullchain.test.ts +++ b/test/000_fullchain.test.ts @@ -16,7 +16,7 @@ import { } from '../utils/poco-tools'; import { IexecWrapper } from './utils/IexecWrapper'; import { loadHardhatFixtureDeployment } from './utils/hardhat-fixture-deployer'; -import { randomAddress } from './utils/utils'; +import { expectProxyBalanceToEqualAllFrozen, randomAddress } from './utils/utils'; // +---------+-------------+-------------+-------------+----------+-----+---------------------------------------------+ // | | Sponsorship | Replication | Beneficiary | Callback | BoT | Type | @@ -67,6 +67,10 @@ describe('Integration tests', function () { await loadFixture(initFixture); }); + afterEach('Check escrow invariant', async () => { + await expectProxyBalanceToEqualAllFrozen(proxyAddress); + }); + async function initFixture() { const accounts = await getIexecAccounts(); ({ diff --git a/test/200_fullchain-bot.test.ts b/test/200_fullchain-bot.test.ts index 9012ac82..ff86e2ee 100644 --- a/test/200_fullchain-bot.test.ts +++ b/test/200_fullchain-bot.test.ts @@ -10,6 +10,7 @@ import { OrdersActors, OrdersAssets, OrdersPrices, buildOrders } from '../utils/ import { TaskStatusEnum, buildUtf8ResultAndDigest, getIexecAccounts } from '../utils/poco-tools'; import { IexecWrapper } from './utils/IexecWrapper'; import { loadHardhatFixtureDeployment } from './utils/hardhat-fixture-deployer'; +import { expectProxyBalanceToEqualAllFrozen } from './utils/utils'; const appPrice = 1000n; const datasetPrice = 1_000_000n; @@ -43,6 +44,10 @@ describe('Integration tests', function () { await loadFixture(initFixture); }); + afterEach('Check escrow invariant', async () => { + await expectProxyBalanceToEqualAllFrozen(proxyAddress); + }); + async function initFixture() { const accounts = await getIexecAccounts(); ({ diff --git a/test/201_fullchain-multi-orders.test.ts b/test/201_fullchain-multi-orders.test.ts index 4a0c5a64..efe56663 100644 --- a/test/201_fullchain-multi-orders.test.ts +++ b/test/201_fullchain-multi-orders.test.ts @@ -23,6 +23,7 @@ import { import { maxBigInt, minBigInt } from '../utils/tools'; import { IexecWrapper } from './utils/IexecWrapper'; import { loadHardhatFixtureDeployment } from './utils/hardhat-fixture-deployer'; +import { expectProxyBalanceToEqualAllFrozen } from './utils/utils'; const appPrice = 1000n; const datasetPrice = 1_000_000n; @@ -47,6 +48,10 @@ describe('Integration tests', function () { await loadFixture(initFixture); }); + afterEach('Check escrow invariant', async () => { + await expectProxyBalanceToEqualAllFrozen(proxyAddress); + }); + async function initFixture() { const accounts = await getIexecAccounts(); ({ requester, appProvider, datasetProvider, scheduler, anyone, worker1 } = accounts); diff --git a/test/300_fullchain-reopen.test.ts b/test/300_fullchain-reopen.test.ts index 960e916d..2505a438 100644 --- a/test/300_fullchain-reopen.test.ts +++ b/test/300_fullchain-reopen.test.ts @@ -9,6 +9,7 @@ import { ZeroAddress } from 'ethers'; import { IexecInterface, IexecInterface__factory } from '../typechain'; import { OrdersActors, OrdersAssets, OrdersPrices, buildOrders } from '../utils/createOrders'; import { loadHardhatFixtureDeployment } from './utils/hardhat-fixture-deployer'; +import { expectProxyBalanceToEqualAllFrozen } from './utils/utils'; import { TAG_STANDARD } from '../utils/constants'; import { @@ -52,6 +53,10 @@ describe('Integration tests', function () { await loadFixture(initFixture); }); + afterEach('Check escrow invariant', async () => { + await expectProxyBalanceToEqualAllFrozen(proxyAddress); + }); + async function initFixture() { const accounts = await getIexecAccounts(); ({ diff --git a/test/byContract/IexecERC20/IexecERC20.test.ts b/test/byContract/IexecERC20/IexecERC20.test.ts index ef69d0ba..ee696eb0 100644 --- a/test/byContract/IexecERC20/IexecERC20.test.ts +++ b/test/byContract/IexecERC20/IexecERC20.test.ts @@ -15,7 +15,7 @@ import { import { getIexecAccounts } from '../../../utils/poco-tools'; import { IexecWrapper } from '../../utils/IexecWrapper'; import { loadHardhatFixtureDeployment } from '../../utils/hardhat-fixture-deployer'; -import { setZeroAddressBalance } from '../../utils/utils'; +import { expectProxyBalanceToEqualAllFrozen, setZeroAddressBalance } from '../../utils/utils'; const value = 100n; @@ -33,6 +33,10 @@ describe('ERC20', async () => { await loadFixture(initFixture); }); + afterEach('Check escrow invariant', async () => { + await expectProxyBalanceToEqualAllFrozen(proxyAddress); + }); + async function initFixture() { const accounts = await getIexecAccounts(); // Setup current test accounts from some arbitrary iExec accounts diff --git a/test/byContract/IexecEscrow/IexecEscrow.test.ts b/test/byContract/IexecEscrow/IexecEscrow.test.ts index 626135fe..c193eb49 100644 --- a/test/byContract/IexecEscrow/IexecEscrow.test.ts +++ b/test/byContract/IexecEscrow/IexecEscrow.test.ts @@ -9,7 +9,7 @@ import { ethers } from 'hardhat'; import { IexecInterface, IexecInterface__factory, RLC, RLC__factory } from '../../../typechain'; import { getIexecAccounts } from '../../../utils/poco-tools'; import { loadHardhatFixtureDeployment } from '../../utils/hardhat-fixture-deployer'; -import { setZeroAddressBalance } from '../../utils/utils'; +import { expectProxyBalanceToEqualAllFrozen, setZeroAddressBalance } from '../../utils/utils'; const amount = ethers.parseUnits('100', 9); @@ -24,6 +24,10 @@ describe('IexecEscrow', () => { await loadFixture(initFixture); }); + afterEach('Check escrow invariant', async () => { + await expectProxyBalanceToEqualAllFrozen(proxyAddress); + }); + async function initFixture() { const accounts = await getIexecAccounts(); ({ diff --git a/test/byContract/IexecPoco/IexecPoco2-claim.test.ts b/test/byContract/IexecPoco/IexecPoco2-claim.test.ts index 5955529e..f606d13e 100644 --- a/test/byContract/IexecPoco/IexecPoco2-claim.test.ts +++ b/test/byContract/IexecPoco/IexecPoco2-claim.test.ts @@ -17,6 +17,7 @@ import { } from '../../../utils/poco-tools'; import { IexecWrapper } from '../../utils/IexecWrapper'; import { loadHardhatFixtureDeployment } from '../../utils/hardhat-fixture-deployer'; +import { expectProxyBalanceToEqualAllFrozen } from '../../utils/utils'; import * as constants from './../../../utils/constants'; const categoryTime = 300n; @@ -46,6 +47,10 @@ describe('IexecPoco2#claim', async () => { await loadFixture(initFixture); }); + afterEach('Check escrow invariant', async () => { + await expectProxyBalanceToEqualAllFrozen(proxyAddress); + }); + async function initFixture() { const accounts = await getIexecAccounts(); ({ iexecAdmin, requester, sponsor, scheduler, worker1, worker2, anyone } = accounts); diff --git a/test/byContract/IexecPoco/IexecPoco2-contribute-and-finalize.test.ts b/test/byContract/IexecPoco/IexecPoco2-contribute-and-finalize.test.ts index 6a07801e..668e707b 100644 --- a/test/byContract/IexecPoco/IexecPoco2-contribute-and-finalize.test.ts +++ b/test/byContract/IexecPoco/IexecPoco2-contribute-and-finalize.test.ts @@ -22,6 +22,7 @@ import { } from '../../../utils/poco-tools'; import { IexecWrapper } from '../../utils/IexecWrapper'; import { loadHardhatFixtureDeployment } from '../../utils/hardhat-fixture-deployer'; +import { expectProxyBalanceToEqualAllFrozen } from '../../utils/utils'; const appPrice = 1000n; const datasetPrice = 1_000_000n; @@ -60,6 +61,10 @@ describe('IexecPoco2#contributeAndFinalize', () => { await loadFixture(initFixture); }); + afterEach('Check escrow invariant', async () => { + await expectProxyBalanceToEqualAllFrozen(proxyAddress); + }); + async function initFixture() { const accounts = await getIexecAccounts(); ({ requester, appProvider, datasetProvider, scheduler, sms, enclave, worker, anyone } = diff --git a/test/byContract/IexecPoco/IexecPoco2-finalize.test.ts b/test/byContract/IexecPoco/IexecPoco2-finalize.test.ts index bfa46d02..f8802599 100644 --- a/test/byContract/IexecPoco/IexecPoco2-finalize.test.ts +++ b/test/byContract/IexecPoco/IexecPoco2-finalize.test.ts @@ -24,6 +24,7 @@ import { import { getPocoStorageSlotLocation } from '../../../utils/proxy-tools'; import { IexecWrapper } from '../../utils/IexecWrapper'; import { loadHardhatFixtureDeployment } from '../../utils/hardhat-fixture-deployer'; +import { expectProxyBalanceToEqualAllFrozen } from '../../utils/utils'; const { results, resultDigest } = buildUtf8ResultAndDigest('result'); const hexResults = ethers.hexlify(results); @@ -63,6 +64,10 @@ describe('IexecPoco2#finalize', async () => { await loadFixture(initFixture); }); + afterEach('Check escrow invariant', async () => { + await expectProxyBalanceToEqualAllFrozen(proxyAddress); + }); + async function initFixture() { const accounts = await getIexecAccounts(); ({ diff --git a/test/byContract/IexecPocoBoost/IexecPocoBoost.test.ts b/test/byContract/IexecPocoBoost/IexecPocoBoost.test.ts index dbdb68ad..ceb66b72 100644 --- a/test/byContract/IexecPocoBoost/IexecPocoBoost.test.ts +++ b/test/byContract/IexecPocoBoost/IexecPocoBoost.test.ts @@ -59,7 +59,7 @@ import { } from '../../../utils/poco-tools'; import { IexecWrapper } from '../../utils/IexecWrapper'; import { loadHardhatFixtureDeployment } from '../../utils/hardhat-fixture-deployer'; -import { randomAddress } from '../../utils/utils'; +import { expectProxyBalanceToEqualAllFrozen, randomAddress } from '../../utils/utils'; const taskIndex = 0n; const volume = taskIndex + 1n; @@ -108,6 +108,10 @@ describe('IexecPocoBoost', function () { await loadFixture(initFixture); }); + afterEach('Check escrow invariant', async () => { + await expectProxyBalanceToEqualAllFrozen(proxyAddress); + }); + async function initFixture() { const accounts = await getIexecAccounts(); ({ diff --git a/test/utils/utils.ts b/test/utils/utils.ts index c46ba500..90562a35 100644 --- a/test/utils/utils.ts +++ b/test/utils/utils.ts @@ -1,6 +1,7 @@ +import { expect } from 'chai'; import { ZeroAddress } from 'ethers'; import { ethers } from 'hardhat'; -import { IexecLibOrders_v5 } from '../../typechain'; +import { IexecInterface__factory, IexecLibOrders_v5 } from '../../typechain'; export async function hashDomain(domain: IexecLibOrders_v5.EIP712DomainStructOutput) { return ethers.TypedDataEncoder.hashDomain({ @@ -30,3 +31,31 @@ export async function setZeroAddressBalance() { export function randomAddress() { return ethers.Wallet.createRandom().address; } + +/** + * Assert the escrow invariant: the sRLC held by the proxy is exactly the sum of + * every account's frozen sRLC. + * + * `EscrowLib.seize` and `EscrowLib.rewardAndLock` are the only escrow operations + * that write `m_frozens` without an sRLC transfer, so they preserve this + * invariant only when they are paired on the same value. Per-account balance and + * frozen deltas cannot detect such a mismatch: a seize without its + * `rewardAndLock` leaves the proxy holding sRLC no account can claim, and the + * opposite leaves frozen values that a later unlock cannot transfer. + * + * The invariant assumes no account sRLC-transfers to the proxy outside of the + * escrow (`transfer(proxyAddress, ...)` or `depositFor(proxyAddress, ...)`), + * which no production code path does. + */ +export async function expectProxyBalanceToEqualAllFrozen(proxyAddress: string) { + const iexecPoco = IexecInterface__factory.connect(proxyAddress, ethers.provider); + const accounts = [ + proxyAddress, // The proxy itself never freezes, but a non null value would break the sum. + await iexecPoco.kitty_address(), + ...(await ethers.getSigners()).map((signer) => signer.address), + ]; + const totalFrozen = await Promise.all( + accounts.map((account) => iexecPoco.frozenOf(account)), + ).then((frozens) => frozens.reduce((total, frozen) => total + frozen, 0n)); + expect(await iexecPoco.balanceOf(proxyAddress)).to.equal(totalFrozen); +}