From 9053c7d1c874d3eccb59f23cf857c4a476c179f5 Mon Sep 17 00:00:00 2001 From: Ariel Melendez Date: Fri, 29 Nov 2024 21:22:45 -0800 Subject: [PATCH 1/4] test(gar): start of basic invariant testing of system after gar tests PE-7211 --- tests/gar.test.mjs | 44 +++++++++++++++++++++++++++++++++++--------- tests/invariants.mjs | 20 ++++++++++++++++++++ 2 files changed, 55 insertions(+), 9 deletions(-) create mode 100644 tests/invariants.mjs diff --git a/tests/gar.test.mjs b/tests/gar.test.mjs index ccc0662b..d6fe926f 100644 --- a/tests/gar.test.mjs +++ b/tests/gar.test.mjs @@ -19,7 +19,7 @@ import { delegateStake, decreaseOperatorStake, } from './helpers.mjs'; -import { describe, it, before } from 'node:test'; +import { describe, it, beforeEach, afterEach } from 'node:test'; import assert from 'node:assert'; import { STUB_TIMESTAMP, @@ -29,6 +29,7 @@ import { INITIAL_OPERATOR_STAKE, INITIAL_DELEGATE_STAKE, } from '../tools/constants.mjs'; +import { assertNoInvariants } from './invariants.mjs'; const delegatorAddress = 'delegator-address-'.padEnd(43, 'x'); @@ -40,15 +41,22 @@ describe('GatewayRegistry', async () => { let sharedMemory = startMemory; // memory we'll use across unique tests; - before(async () => { + beforeEach(async () => { const { memory: joinNetworkMemory } = await joinNetwork({ address: STUB_ADDRESS, - memory: sharedMemory, + memory: startMemory, }); // NOTE: all tests will start with this gateway joined to the network - use `sharedMemory` for the first interaction for each test to avoid having to join the network again sharedMemory = joinNetworkMemory; }); + afterEach(async () => { + await assertNoInvariants({ + timestamp: STUB_TIMESTAMP, + memory: sharedMemory, + }); + }); + describe('Join-Network', () => { it('should allow joining of the network record', async () => { // check the gateway record from contract @@ -210,7 +218,7 @@ describe('GatewayRegistry', async () => { it('should allow joining of the network with an allow list', async () => { const otherGatewayAddress = ''.padEnd(43, '3'); - const updatedMemory = await allowlistJoinTest({ + sharedMemory = await allowlistJoinTest({ gatewayAddress: otherGatewayAddress, tags: [ { name: 'Allow-Delegated-Staking', value: 'allowlist' }, @@ -229,7 +237,7 @@ describe('GatewayRegistry', async () => { }); const delegateItems = await getDelegatesItems({ - memory: updatedMemory, + memory: sharedMemory, gatewayAddress: otherGatewayAddress, }); assert.deepStrictEqual( @@ -244,7 +252,7 @@ describe('GatewayRegistry', async () => { ); const { result: getAllowedDelegatesResult } = await getAllowedDelegates({ - memory: updatedMemory, + memory: sharedMemory, from: STUB_ADDRESS, timestamp: STUB_TIMESTAMP, gatewayAddress: otherGatewayAddress, @@ -342,6 +350,8 @@ describe('GatewayRegistry', async () => { }, ], ); + + sharedMemory = leaveNetworkMemory; }); }); @@ -444,7 +454,7 @@ describe('GatewayRegistry', async () => { } it('should allow updating the gateway settings', async () => { - await updateGatewaySettingsTest({ + sharedMemory = await updateGatewaySettingsTest({ settingsTags: [ { name: 'Label', value: 'new-label' }, { name: 'Note', value: 'new-note' }, @@ -494,7 +504,7 @@ describe('GatewayRegistry', async () => { expectedAllowedDelegates: [STUB_ADDRESS_9], // probs empty }); - await updateGatewaySettingsTest({ + sharedMemory = await updateGatewaySettingsTest({ settingsTags: [ { name: 'Allow-Delegated-Staking', value: 'false' }, { name: 'Allowed-Delegates', value: STUB_ADDRESS_9 }, @@ -573,7 +583,7 @@ describe('GatewayRegistry', async () => { JSON.parse(delegationsResult.Messages[0].Data).items, ); - await updateGatewaySettingsTest({ + sharedMemory = await updateGatewaySettingsTest({ inputMemory: updatedMemory, settingsTags: [{ name: 'Allow-Delegated-Staking', value: 'false' }], expectedUpdatedGatewayProps: { @@ -640,6 +650,7 @@ describe('GatewayRegistry', async () => { sortOrder: 'desc', }, ); + sharedMemory = updatedMemory; }); }); @@ -666,6 +677,7 @@ describe('GatewayRegistry', async () => { ...gatewayBefore, operatorStake: INITIAL_OPERATOR_STAKE + increaseQty, // matches the initial operator stake from the test setup plus the increase }); + sharedMemory = increaseStakeMemory; }); }); @@ -722,6 +734,7 @@ describe('GatewayRegistry', async () => { }, ], ); + sharedMemory = decreaseStakeMemory; }); it('should not allow decreasing the operator stake if below the minimum withdrawal', async () => { @@ -752,6 +765,7 @@ describe('GatewayRegistry', async () => { ), 'Error tag should be present', ); + sharedMemory = decreaseOperatorStakeResult.Memory; }); it('should allow decreasing the operator stake instantly, for a fee', async () => { @@ -832,6 +846,7 @@ describe('GatewayRegistry', async () => { balancesBefore[STUB_ADDRESS] + amountWithdrawn; assert.equal(balancesAfter[PROCESS_ID], expectedProtocolBalance); assert.equal(balancesAfter[STUB_ADDRESS], expectedOperatorBalance); + sharedMemory = decreaseInstantMemory; }); }); @@ -874,6 +889,7 @@ describe('GatewayRegistry', async () => { ], delegateItems, ); + sharedMemory = delegatedStakeMemory; }); }); @@ -949,6 +965,7 @@ describe('GatewayRegistry', async () => { type: 'stake', }, ]); + sharedMemory = decreaseStakeMemory; }); it('should fail to withdraw a delegated stake if below the minimum withdrawal limitation', async () => { @@ -995,6 +1012,7 @@ describe('GatewayRegistry', async () => { memory: decreaseStakeMemory, }); assert.deepStrictEqual(gatewayAfter, gatewayBefore); + sharedMemory = decreaseStakeMemory; }); }); @@ -1039,6 +1057,7 @@ describe('GatewayRegistry', async () => { }); // no changes to the gateway after a withdrawal is cancelled assert.deepStrictEqual(gatewayAfter, gatewayBefore); + sharedMemory = cancelWithdrawalMemory; }); it('should allow cancelling an operator withdrawal', async () => { const decreaseStakeTimestamp = STUB_TIMESTAMP + 1000 * 60 * 15; // 15 minutes after stubbedTimestamp @@ -1084,6 +1103,7 @@ describe('GatewayRegistry', async () => { ...gatewayBefore, operatorStake: INITIAL_OPERATOR_STAKE + decreaseQty, // the decrease was cancelled and returned to the operator }); + sharedMemory = cancelWithdrawalMemory; }); }); @@ -1156,6 +1176,8 @@ describe('GatewayRegistry', async () => { balancesAfter[PROCESS_ID], balancesBefore[PROCESS_ID] + penaltyAmount, ); // original stake + penalty + + sharedMemory = instantWithdrawalMemory; }); }); @@ -1197,6 +1219,7 @@ describe('GatewayRegistry', async () => { fetchedGateways.map((g) => g.gatewayAddress), [STUB_ADDRESS, secondGatewayAddress], ); + sharedMemory = addGatewayMemory2; }); }); @@ -1278,6 +1301,7 @@ describe('GatewayRegistry', async () => { if (!cursor) break; } assert.deepStrictEqual(fetchedDelegations, expectedDelegations); + sharedMemory = decreaseStakeMemory; } it('should paginate active and vaulted stakes by ascending balance correctly', async () => { @@ -1508,6 +1532,7 @@ describe('GatewayRegistry', async () => { redelegationFeeRate: 0, }, ); + sharedMemory = redelegateStakeMemory; }); it("should allow re-delegating stake with a vault and the vault's balance", async () => { @@ -1608,6 +1633,7 @@ describe('GatewayRegistry', async () => { }), [], ); + sharedMemory = redelegateStakeMemory; }); }); }); diff --git a/tests/invariants.mjs b/tests/invariants.mjs new file mode 100644 index 00000000..91700d0a --- /dev/null +++ b/tests/invariants.mjs @@ -0,0 +1,20 @@ +import assert from 'node:assert'; +import { getBalances } from './helpers.mjs'; + +export async function assertNoInvariants({ timestamp, memory }) { + // Assert all balances are >= 0 and all belong to valid addresses + const balances = await getBalances({ + memory, + timestamp, + }); + for (const [address, balance] of Object.entries(balances)) { + assert( + balance >= 0 && balance <= 1_000_000_000_000_000, + `Invariant violated: balance of ${balance} for address ${address}`, + ); + assert( + address.length > 0, + `Invariant violated: address ${address} is empty`, + ); + } +} From 090f720b9aefdf317ec7bee6a552c00f5258d635 Mon Sep 17 00:00:00 2001 From: Ariel Melendez Date: Sat, 30 Nov 2024 20:39:27 -0800 Subject: [PATCH 2/4] test(invariants): assert balance vault invariants PE-7211 --- tests/invariants.mjs | 61 +++++++++++++++++++++++++++++++++++++------- 1 file changed, 52 insertions(+), 9 deletions(-) diff --git a/tests/invariants.mjs b/tests/invariants.mjs index 91700d0a..0c454bcc 100644 --- a/tests/invariants.mjs +++ b/tests/invariants.mjs @@ -1,20 +1,63 @@ import assert from 'node:assert'; -import { getBalances } from './helpers.mjs'; +import { getBalances, getVaults } from './helpers.mjs'; + +function assertValidBalance(balance) { + assert( + balance >= 0 && balance <= 1_000_000_000_000_000, + `Invariant violated: balance ${balance} is invalid`, + ); +} + +function assertValidAddress(address) { + assert(address.length > 0, `Invariant violated: address ${address} is empty`); +} + +function assertValidTimestampsAtTimestamp({ + startTimestamp, + endTimestamp, + timestamp, +}) { + assert( + startTimestamp <= timestamp, + `Invariant violated: startTimestamp ${startTimestamp} is in the future`, + ); + assert( + endTimestamp === null || endTimestamp >= startTimestamp, + `Invariant violated: endTimestamp of ${endTimestamp} for vault ${address}`, + ); +} export async function assertNoInvariants({ timestamp, memory }) { + await assertNoBalanceInvariants({ timestamp, memory }); + await assertNoBalanceVaultInvariants({ timestamp, memory }); +} + +async function assertNoBalanceInvariants({ timestamp, memory }) { // Assert all balances are >= 0 and all belong to valid addresses const balances = await getBalances({ memory, timestamp, }); for (const [address, balance] of Object.entries(balances)) { - assert( - balance >= 0 && balance <= 1_000_000_000_000_000, - `Invariant violated: balance of ${balance} for address ${address}`, - ); - assert( - address.length > 0, - `Invariant violated: address ${address} is empty`, - ); + assertValidBalance(balance); + assertValidAddress(address); + } +} + +async function assertNoBalanceVaultInvariants({ timestamp, memory }) { + const { result } = await getVaults({ + memory, + limit: 1_000_000, // egregiously large limit to make sure we get them all + }); + + for (const vault of JSON.parse(result.Messages?.[0]?.Data).items) { + const { address, balance, startTimestamp, endTimestamp } = vault; + assertValidBalance(balance); + assertValidAddress(address); + assertValidTimestampsAtTimestamp({ + startTimestamp, + endTimestamp, + timestamp, + }); } } From 20476ff1c77d4601747301352ac81d170c573abe Mon Sep 17 00:00:00 2001 From: Ariel Melendez Date: Sat, 30 Nov 2024 20:42:20 -0800 Subject: [PATCH 3/4] test(invariants): adjust invariant values PE-7211 --- tests/invariants.mjs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/invariants.mjs b/tests/invariants.mjs index 0c454bcc..abc4ab9b 100644 --- a/tests/invariants.mjs +++ b/tests/invariants.mjs @@ -3,7 +3,7 @@ import { getBalances, getVaults } from './helpers.mjs'; function assertValidBalance(balance) { assert( - balance >= 0 && balance <= 1_000_000_000_000_000, + balance > 0 && balance <= 1_000_000_000_000_000, `Invariant violated: balance ${balance} is invalid`, ); } @@ -22,7 +22,7 @@ function assertValidTimestampsAtTimestamp({ `Invariant violated: startTimestamp ${startTimestamp} is in the future`, ); assert( - endTimestamp === null || endTimestamp >= startTimestamp, + endTimestamp === null || endTimestamp > startTimestamp, `Invariant violated: endTimestamp of ${endTimestamp} for vault ${address}`, ); } From 0307691269bd9bacaebf2c45d2452d010abe23db Mon Sep 17 00:00:00 2001 From: Ariel Melendez Date: Mon, 2 Dec 2024 10:43:11 -0800 Subject: [PATCH 4/4] test(invariants): more precise balance assertions PE-7211 --- tests/invariants.mjs | 40 +++++++++++++++++++++++++++++++++++++--- 1 file changed, 37 insertions(+), 3 deletions(-) diff --git a/tests/invariants.mjs b/tests/invariants.mjs index abc4ab9b..6aac43b7 100644 --- a/tests/invariants.mjs +++ b/tests/invariants.mjs @@ -1,9 +1,11 @@ import assert from 'node:assert'; import { getBalances, getVaults } from './helpers.mjs'; -function assertValidBalance(balance) { +function assertValidBalance(balance, expectedMin = 1) { assert( - balance > 0 && balance <= 1_000_000_000_000_000, + Number.isInteger(balance) && + balance >= expectedMin && + balance <= 1_000_000_000_000_000, `Invariant violated: balance ${balance} is invalid`, ); } @@ -39,7 +41,7 @@ async function assertNoBalanceInvariants({ timestamp, memory }) { timestamp, }); for (const [address, balance] of Object.entries(balances)) { - assertValidBalance(balance); + assertValidBalance(balance, 0); assertValidAddress(address); } } @@ -61,3 +63,35 @@ async function assertNoBalanceVaultInvariants({ timestamp, memory }) { }); } } + +async function assertNoTotalSupplyInvariants({ timestamp, memory }) { + const supplyResult = await handle({ + Tags: [ + { + name: 'Action', + value: 'Total-Token-Supply', + }, + ], + }); + + // assert no errors + assert.deepEqual(supplyResult.Messages?.[0]?.Error, undefined); + // assert correct tag in message by finding the index of the tag in the message + const notice = supplyResult.Messages?.[0]?.Tags?.find( + (tag) => tag.name === 'Action' && tag.value === 'Total-Token-Supply-Notice', + ); + assert.ok(notice, 'should have a Total-Token-Supply-Notice tag'); + + const supplyData = JSON.parse(supplyResult.Messages?.[0]?.Data); + + assert.ok( + supplyData.total === 1000000000 * 1000000, + 'total supply should be 1 billion IO but was ' + supplyData.total, + ); + assertValidBalance(supplyData.circulating); + assertValidBalance(supplyData.locked, 0); + assertValidBalance(supplyData.staked, 0); + assertValidBalance(supplyData.delegated, 0); + assertValidBalance(supplyData.withdrawn, 0); + assertValidBalance(supplyData.protocolBalance, 0); +}