diff --git a/packages/subgraph/config/hardhat.json b/packages/subgraph/config/hardhat.json new file mode 100644 index 0000000000..d9f3424abb --- /dev/null +++ b/packages/subgraph/config/hardhat.json @@ -0,0 +1 @@ +{"network":"mainnet","testNetwork":"hardhat","hostStartBlock":0,"hostAddress":"0x4374EEcaAD0Dcaa149CfFc160d5a0552B1D092b0","cfaAddress":"0x44BF2a9217A2970A1bCC7529Bf1d40828C594320","idaAddress":"0x0826223156fF61F9abf620D2f58A06177DA664Cc","gdaAddress":"0x1Ad83f2789e013a41eA18819F0e24a4d41477b4c","superTokenFactoryAddress":"0x9b9389FacF9d345676b178EfEe328334cA711A38","resolverV1Address":"0x0e528E13E32cfB153818F1e10c7aff11ec5B46A3","nativeAssetSuperTokenAddress":"0x68bFD28e2AB62C450C563E1687f5f4F6b008149f","constantOutflowNFTAddress":"0x765FEFc8D7b2A6ec1D83626c50781e8cdd7e8284","constantInflowNFTAddress":"0x0983210c9f6f968ACC2010ac4dc56124dcDe2EC2"} \ No newline at end of file diff --git a/packages/subgraph/config/polygon-mainnet.json b/packages/subgraph/config/polygon-mainnet.json new file mode 100644 index 0000000000..8637fdd0b2 --- /dev/null +++ b/packages/subgraph/config/polygon-mainnet.json @@ -0,0 +1 @@ +{"network":"matic","hostStartBlock":11650500,"hostAddress":"0x3E14dC1b13c488a8d5D310918780c983bD5982E7","cfaAddress":"0x6EeE6060f715257b970700bc2656De21dEdF074C","idaAddress":"0xB0aABBA4B2783A72C52956CDEF62d438ecA2d7a1","gdaAddress":"0x961dd5A052741B49B6CBf6759591f9D8576fCFb0","superTokenFactoryAddress":"0x2C90719f25B10Fc5646c82DA3240C76Fa5BcCF34","resolverV1Address":"0x8bDCb5613153f41B2856F71Bd7A7e0432F6dbe58","nativeAssetSuperTokenAddress":"0x3aD736904E9e65189c3000c7DD2c8AC8bB7cD4e3","constantOutflowNFTAddress":"0x554e2bbaCF43FD87417b7201A9F1649a3ED89d68","constantInflowNFTAddress":"0x55909bB8cd8276887Aae35118d60b19755201c68"} \ No newline at end of file diff --git a/packages/subgraph/schema.graphql b/packages/subgraph/schema.graphql index 5820aba5fb..ca3c7ffa44 100644 --- a/packages/subgraph/schema.graphql +++ b/packages/subgraph/schema.graphql @@ -1815,6 +1815,11 @@ type Pool @entity { totalAmountInstantlyDistributedUntilUpdatedAt: BigInt! totalAmountFlowedDistributedUntilUpdatedAt: BigInt! totalAmountDistributedUntilUpdatedAt: BigInt! + totalFlowAdjustmentAmountDistributedUntilUpdatedAt: BigInt! + + perUnitSettledValue: BigInt! + perUnitFlowRate: BigInt! + """ A member is any account which has more than 0 units in the pool. """ @@ -1870,6 +1875,9 @@ type PoolMember @entity { poolTotalAmountDistributedUntilUpdatedAt: BigInt! totalAmountReceivedUntilUpdatedAt: BigInt! + syncedPerUnitSettledValue: BigInt! + syncedPerUnitFlowRate: BigInt! + account: Account! pool: Pool! diff --git a/packages/subgraph/src/mappingHelpers.ts b/packages/subgraph/src/mappingHelpers.ts index 82cf7a0cb5..2f206ffed6 100644 --- a/packages/subgraph/src/mappingHelpers.ts +++ b/packages/subgraph/src/mappingHelpers.ts @@ -36,7 +36,6 @@ import { getPoolDistributorID, getActiveStreamsDelta, getClosedStreamsDelta, - MAX_UINT256, } from "./utils"; import { SuperToken as SuperTokenTemplate } from "../generated/templates"; import { ISuperToken as SuperToken } from "../generated/templates/SuperToken/ISuperToken"; @@ -499,6 +498,11 @@ export function getOrInitPool(event: ethereum.Event, poolId: string): Pool { pool.totalAmountInstantlyDistributedUntilUpdatedAt = BIG_INT_ZERO; pool.totalAmountFlowedDistributedUntilUpdatedAt = BIG_INT_ZERO; pool.totalAmountDistributedUntilUpdatedAt = BIG_INT_ZERO; + pool.totalFlowAdjustmentAmountDistributedUntilUpdatedAt = BIG_INT_ZERO; + + pool.perUnitSettledValue = BIG_INT_ZERO; + pool.perUnitFlowRate = BIG_INT_ZERO; + pool.totalMembers = 0; pool.totalConnectedMembers = 0; pool.totalDisconnectedMembers = 0; @@ -519,9 +523,6 @@ export function updatePoolTotalAmountFlowedAndDistributed( const timeDelta = event.block.timestamp.minus(pool.updatedAtTimestamp); const amountFlowedSinceLastUpdate = pool.flowRate.times(timeDelta); - pool.updatedAtBlockNumber = event.block.number; - pool.updatedAtTimestamp = event.block.timestamp; - pool.totalAmountFlowedDistributedUntilUpdatedAt = pool.totalAmountFlowedDistributedUntilUpdatedAt.plus( amountFlowedSinceLastUpdate @@ -554,13 +555,19 @@ export function getOrInitOrUpdatePoolMember( poolMember.totalAmountClaimed = BIG_INT_ZERO; poolMember.poolTotalAmountDistributedUntilUpdatedAt = BIG_INT_ZERO; poolMember.totalAmountReceivedUntilUpdatedAt = BIG_INT_ZERO; - + + poolMember.syncedPerUnitSettledValue = BIG_INT_ZERO; + poolMember.syncedPerUnitFlowRate = BIG_INT_ZERO; + poolMember.account = poolMemberAddress.toHex(); poolMember.pool = poolAddress.toHex(); } poolMember.updatedAtTimestamp = event.block.timestamp; poolMember.updatedAtBlockNumber = event.block.number; + poolMember.updatedAtTimestamp = event.block.timestamp; + poolMember.updatedAtBlockNumber = event.block.number; + return poolMember; } @@ -1471,33 +1478,80 @@ export function updateAggregateEntitiesTransferData( tokenStatistic.save(); } + +export function particleRTB( + perUnitSettledValue: BigInt, + perUnitFlowRate: BigInt, + currentTimestamp: BigInt, + lastUpdatedTimestamp: BigInt +): BigInt { + const amountFlowedPerUnit = perUnitFlowRate.times(currentTimestamp.minus(lastUpdatedTimestamp)); + return perUnitSettledValue.plus(amountFlowedPerUnit); +} + +export function monetaryUnitPoolMemberRTB(pool: Pool, poolMember: PoolMember, currentTimestamp: BigInt): BigInt { + const poolPerUnitRTB = particleRTB( + pool.perUnitSettledValue, + pool.perUnitFlowRate, + currentTimestamp, + pool.updatedAtTimestamp + ); + const poolMemberPerUnitRTB = particleRTB( + poolMember.syncedPerUnitSettledValue, + poolMember.syncedPerUnitFlowRate, + currentTimestamp, + poolMember.updatedAtTimestamp + ); + + return poolMember.totalAmountReceivedUntilUpdatedAt.plus( + poolPerUnitRTB.minus(poolMemberPerUnitRTB).times(poolMember.units) + ); +} + /** - * Updates `totalAmountReceivedUntilUpdatedAt` and `poolTotalAmountDistributedUntilUpdatedAt` fields - * Requires an explicit save on the PoolMember entity. - * Requires `pool.totalAmountDistributedUntilUpdatedAt` is updated *BEFORE* this function is called. - * Requires that pool.totalUnits and poolMember.units are updated *AFTER* this function is called. - * @param pool the pool entity - * @param poolMember the pool member entity - * @returns the updated pool member entity to be saved + * Updates the pool.perUnitSettledValue to the up to date value based on the current block, + * and updates the updatedAtTimestamp and updatedAtBlockNumber. + * @param pool pool entity + * @param block current block + * @returns updated pool entity */ -export function updatePoolMemberTotalAmountUntilUpdatedAtFields(pool: Pool, poolMember: PoolMember): PoolMember { - let amountReceivedDelta = BIG_INT_ZERO; - // if the pool member has any units, we calculate the delta - // otherwise the delta is going to be 0 - if (!poolMember.units.equals(BIG_INT_ZERO)) { - const distributedAmountDelta = pool.totalAmountDistributedUntilUpdatedAt - .minus(poolMember.poolTotalAmountDistributedUntilUpdatedAt); - - const isSafeToMultiplyWithoutOverflow = MAX_UINT256.div(poolMember.units).gt(distributedAmountDelta); - if (isSafeToMultiplyWithoutOverflow) { - amountReceivedDelta = distributedAmountDelta.times(poolMember.units).div(pool.totalUnits); - } else { - amountReceivedDelta = distributedAmountDelta.div(pool.totalUnits).times(poolMember.units); - } - } - poolMember.totalAmountReceivedUntilUpdatedAt = - poolMember.totalAmountReceivedUntilUpdatedAt.plus(amountReceivedDelta); - poolMember.poolTotalAmountDistributedUntilUpdatedAt = pool.totalAmountDistributedUntilUpdatedAt; +export function settlePoolParticle(pool: Pool, block: ethereum.Block): Pool { + pool.perUnitSettledValue = particleRTB( + pool.perUnitSettledValue, + pool.perUnitFlowRate, + block.timestamp, + pool.updatedAtTimestamp + ); + pool.updatedAtTimestamp = block.timestamp; + pool.updatedAtBlockNumber = block.number; + + return pool; +} + +export function settlePoolMemberParticle(poolMember: PoolMember, block: ethereum.Block): PoolMember { + poolMember.syncedPerUnitSettledValue = particleRTB( + poolMember.syncedPerUnitSettledValue, + poolMember.syncedPerUnitFlowRate, + block.timestamp, + poolMember.updatedAtTimestamp + ); + poolMember.updatedAtTimestamp = block.timestamp; + poolMember.updatedAtBlockNumber = block.number; + + return poolMember; +} + +export function syncPoolMemberParticle(pool: Pool, poolMember: PoolMember): PoolMember { + poolMember.syncedPerUnitSettledValue = pool.perUnitSettledValue; + poolMember.syncedPerUnitFlowRate = pool.perUnitFlowRate; + poolMember.updatedAtTimestamp = pool.updatedAtTimestamp; + poolMember.updatedAtBlockNumber = pool.updatedAtBlockNumber; return poolMember; } + +export function settlePDPoolMemberMU(pool: Pool, poolMember: PoolMember, block: ethereum.Block): void { + pool = settlePoolParticle(pool, block); + poolMember.totalAmountReceivedUntilUpdatedAt = monetaryUnitPoolMemberRTB(pool, poolMember, block.timestamp); + poolMember = syncPoolMemberParticle(pool, poolMember); +} diff --git a/packages/subgraph/src/mappings/gdav1.ts b/packages/subgraph/src/mappings/gdav1.ts index e5a74baf36..19762416f7 100644 --- a/packages/subgraph/src/mappings/gdav1.ts +++ b/packages/subgraph/src/mappings/gdav1.ts @@ -21,10 +21,11 @@ import { getOrInitPoolDistributor, getOrInitOrUpdatePoolMember, getOrInitTokenStatistic, + settlePDPoolMemberMU, + settlePoolParticle, updateATSStreamedAndBalanceUntilUpdatedAt, updateAggregateDistributionAgreementData, updatePoolDistributorTotalAmountFlowedAndDistributed, - updatePoolMemberTotalAmountUntilUpdatedAtFields, updatePoolTotalAmountFlowedAndDistributed, updateSenderATSStreamData, updateTokenStatisticStreamData, @@ -33,6 +34,7 @@ import { import { BIG_INT_ZERO, createEventID, + divideOrZero, initializeEventEntity, membershipWithUnitsExists, } from "../utils"; @@ -98,7 +100,9 @@ export function handlePoolConnectionUpdated( // Update Pool Entity let pool = getOrInitPool(event, event.params.pool.toHex()); + // @note we modify pool and poolMember here in memory, but do not save pool = updatePoolTotalAmountFlowedAndDistributed(event, pool); + settlePDPoolMemberMU(pool, poolMember, event.block); if (poolMember.units.gt(BIG_INT_ZERO)) { if (memberConnectedStatusUpdated) { // disconnected -> connected case @@ -126,9 +130,6 @@ export function handlePoolConnectionUpdated( } } } - - // Update totalAmountDistributedUntilUpdatedAt - poolMember = updatePoolMemberTotalAmountUntilUpdatedAtFields(pool, poolMember); pool.save(); poolMember.save(); @@ -229,7 +230,12 @@ export function handleFlowDistributionUpdated( // Update Pool let pool = getOrInitPool(event, event.params.pool.toHex()); + + // @note that we are duplicating update of updatedAtTimestamp/BlockNumber here + // in the two functions pool = updatePoolTotalAmountFlowedAndDistributed(event, pool); + pool = settlePoolParticle(pool, event.block); + pool.perUnitFlowRate = divideOrZero(event.params.newDistributorToPoolFlowRate, pool.totalUnits); pool.flowRate = event.params.newTotalDistributionFlowRate; pool.adjustmentFlowRate = event.params.adjustmentFlowRate; pool.save(); @@ -312,7 +318,13 @@ export function handleInstantDistributionUpdated( // Update Pool let pool = getOrInitPool(event, event.params.pool.toHex()); + + // @note that we are duplicating update of updatedAtTimestamp/BlockNumber here + // in the two functions pool = updatePoolTotalAmountFlowedAndDistributed(event, pool); + pool = settlePoolParticle(pool, event.block); + // @note a speculations on what needs to be done + pool.perUnitSettledValue = pool.perUnitSettledValue.plus(divideOrZero(event.params.actualAmount, pool.totalUnits)); const previousTotalAmountDistributed = pool.totalAmountDistributedUntilUpdatedAt; pool.totalAmountInstantlyDistributedUntilUpdatedAt = diff --git a/packages/subgraph/src/mappings/superfluidPool.ts b/packages/subgraph/src/mappings/superfluidPool.ts index 15a22f6b8c..12afde5e09 100644 --- a/packages/subgraph/src/mappings/superfluidPool.ts +++ b/packages/subgraph/src/mappings/superfluidPool.ts @@ -9,9 +9,9 @@ import { _createTokenStatisticLogEntity, getOrInitPool, getOrInitOrUpdatePoolMember, + settlePDPoolMemberMU, updateATSStreamedAndBalanceUntilUpdatedAt, updateAggregateDistributionAgreementData, - updatePoolMemberTotalAmountUntilUpdatedAtFields, updatePoolTotalAmountFlowedAndDistributed, updateTokenStatsStreamedUntilUpdatedAt, } from "../mappingHelpers"; @@ -24,14 +24,17 @@ export function handleDistributionClaimed(event: DistributionClaimed): void { // Update Pool let pool = getOrInitPool(event, event.address.toHex()); - pool = updatePoolTotalAmountFlowedAndDistributed(event, pool); - pool.save(); - - // Update PoolMember let poolMember = getOrInitOrUpdatePoolMember(event, event.address, event.params.member); poolMember.totalAmountClaimed = event.params.totalClaimed; - poolMember = updatePoolMemberTotalAmountUntilUpdatedAtFields(pool, poolMember); + // settle pool and pool member + pool = updatePoolTotalAmountFlowedAndDistributed(event, pool); + settlePDPoolMemberMU(pool, poolMember, event.block); + + // Update PoolMember + poolMember.totalAmountClaimed = event.params.totalClaimed; + + pool.save(); poolMember.save(); // Update Token Statistics @@ -51,11 +54,30 @@ export function handleMemberUnitsUpdated(event: MemberUnitsUpdated): void { let pool = getOrInitPool(event, event.address.toHex()); let poolMember = getOrInitOrUpdatePoolMember(event, event.address, event.params.member); - pool = updatePoolTotalAmountFlowedAndDistributed(event, pool); - poolMember = updatePoolMemberTotalAmountUntilUpdatedAtFields(pool, poolMember); - const previousUnits = poolMember.units; const unitsDelta = event.params.newUnits.minus(previousUnits); + const newTotalUnits = pool.totalUnits.plus(unitsDelta); + + pool = updatePoolTotalAmountFlowedAndDistributed(event, pool); + settlePDPoolMemberMU(pool, poolMember, event.block); + + // @note TODO update the pool.perUnitFlowRate + // @note TODO update the poolMember.perUnitFlowRate + const existingPoolFlowRate = pool.perUnitFlowRate.times(pool.totalUnits); + let newPerUnitFlowRate: BigInt; + let remainderRate: BigInt; + + if (!newTotalUnits.equals(BIG_INT_ZERO)) { + newPerUnitFlowRate = existingPoolFlowRate.div(newTotalUnits); + remainderRate = existingPoolFlowRate.minus(newPerUnitFlowRate.times(newTotalUnits)); + } else { + remainderRate = existingPoolFlowRate; + newPerUnitFlowRate = BIG_INT_ZERO; + } + pool.perUnitFlowRate = newPerUnitFlowRate; + pool.totalUnits = newTotalUnits; + + poolMember.syncedPerUnitFlowRate = poolMember.syncedPerUnitFlowRate.plus(remainderRate); poolMember.units = event.params.newUnits; if (poolMember.isConnected) { @@ -63,7 +85,6 @@ export function handleMemberUnitsUpdated(event: MemberUnitsUpdated): void { } else { pool.totalDisconnectedUnits = pool.totalDisconnectedUnits.plus(unitsDelta); } - pool.totalUnits = pool.totalUnits.plus(unitsDelta); // 0 units to > 0 units const didPoolMemberBecomeActive = previousUnits.equals(BIG_INT_ZERO) && event.params.newUnits.gt(BIG_INT_ZERO) diff --git a/packages/subgraph/src/utils.ts b/packages/subgraph/src/utils.ts index a182123f3c..ce7d5c1ed6 100644 --- a/packages/subgraph/src/utils.ts +++ b/packages/subgraph/src/utils.ts @@ -387,3 +387,12 @@ export function createLogID( event.logIndex.toString() ); } + +export function divideOrZero( + numerator: BigInt, + denominator: BigInt +): BigInt { + return denominator.equals(BIG_INT_ZERO) + ? BIG_INT_ZERO + : numerator.div(denominator); +} \ No newline at end of file diff --git a/packages/subgraph/tests/bugs/2024-03-07-pool-member-total-amount-received.test.ts b/packages/subgraph/tests/bugs/2024-03-07-pool-member-total-amount-received.test.ts index c214de0a25..fdd04955af 100644 --- a/packages/subgraph/tests/bugs/2024-03-07-pool-member-total-amount-received.test.ts +++ b/packages/subgraph/tests/bugs/2024-03-07-pool-member-total-amount-received.test.ts @@ -1,176 +1,139 @@ -import { assert, describe, test } from "matchstick-as"; -import { Pool, PoolDistributor, PoolMember } from "../../generated/schema" +import { assert, beforeEach, clearStore, describe, test } from "matchstick-as"; import { Address, BigInt, Bytes } from "@graphprotocol/graph-ts"; -import { FAKE_INITIAL_BALANCE, alice as alice_, bob as bob_, charlie, delta, echo, maticXAddress, superfluidPool } from "../constants"; -import { BIG_INT_ZERO, getPoolMemberID } from "../../src/utils"; -import { handleInstantDistributionUpdated } from "../../src/mappings/gdav1"; -import { createInstantDistributionUpdatedEvent, createMemberUnitsUpdatedEvent } from "../gdav1/gdav1.helper"; -import { mockedGetAppManifest, mockedRealtimeBalanceOf } from "../mockedFunctions"; +import { alice as alice_, bob as bob_, delta, echo, maticXAddress, superfluidPool } from "../constants"; +import { getPoolMemberID } from "../../src/utils"; +import { handleFlowDistributionUpdated, handleInstantDistributionUpdated, handlePoolCreated } from "../../src/mappings/gdav1"; +import { createFlowDistributionUpdatedEvent, createInstantDistributionUpdatedEvent, createMemberUnitsUpdatedEvent, createPoolAndReturnPoolCreatedEvent } from "../gdav1/gdav1.helper"; +import { mockedAppManifestAndRealtimeBalanceOf } from "../mockedFunctions"; import { handleMemberUnitsUpdated } from "../../src/mappings/superfluidPool"; +import { Pool } from "../../generated/schema"; -/** - * Problem description - 1. Create pool - 2. Add member A and update A units to 100 - 3. Distribute 100 tokens - 4. Add member B and update B units to 100 - 4. Distribute 100 tokens - - Expected result: - member A 150 tokens - member B 50 tokens - - Actual result: - member A 100 tokens - member B 50 tokens - */ describe("PoolMember ending up with wrong `totalAmountReceivedUntilUpdatedAt`", () => { - test("create elaborate scenario with 2 distributions and 2 pool members", () => { - return; // ignore test for CI (as the test is failing for now) + beforeEach(() => { + clearStore(); + }); + /** + * Problem description + 1. Create pool + 2. Add member A and update A units to 100 + 3. Distribute 1000 tokens + 4. Add member B and update B units to 100 + 5. Distribute 1000 tokens + + Expected result: + member A 1500 tokens + member B 500 tokens + */ + test("create elaborate scenario with 2 instant distributions and 2 pool members", () => { const superTokenAddress = maticXAddress; - + const poolAdminAndDistributorAddress = Address.fromString(delta); + const poolAddress = Address.fromString(superfluidPool); + // # Arrange State 1 // ## Arrange Pool - const poolAddress = Address.fromString(superfluidPool); - const poolAdminAndDistributorAddress = Address.fromString(delta); - let pool = new Pool(poolAddress.toHexString()); - pool.createdAtTimestamp = BigInt.fromI32(1); - pool.createdAtBlockNumber = BigInt.fromI32(1); - pool.updatedAtTimestamp = BigInt.fromI32(1); - pool.updatedAtBlockNumber = BigInt.fromI32(1); + const poolCreatedEvent = createPoolAndReturnPoolCreatedEvent(poolAdminAndDistributorAddress.toHexString(), superTokenAddress, poolAddress.toHexString()); - pool.totalMembers = 1; - pool.totalConnectedMembers = 1; - pool.totalDisconnectedMembers = 0; - pool.adjustmentFlowRate = BigInt.fromI32(0); - pool.flowRate = BigInt.fromI32(0); - pool.admin = poolAdminAndDistributorAddress.toHexString(); - pool.totalBuffer = BigInt.fromI32(0); - pool.token = superTokenAddress; - pool.totalAmountDistributedUntilUpdatedAt = BigInt.fromI32(0); - pool.totalAmountFlowedDistributedUntilUpdatedAt = BigInt.fromI32(0); - pool.totalAmountInstantlyDistributedUntilUpdatedAt = BigInt.fromI32(0); - pool.totalConnectedUnits = BigInt.fromI32(100); - pool.totalDisconnectedUnits = BigInt.fromI32(0); - pool.totalUnits = BigInt.fromI32(100); - pool.save(); - // --- - // ## Arrange PoolMember 1 const aliceAddress = Address.fromString(alice_); const aliceId = getPoolMemberID(poolAddress, aliceAddress); - const alice = new PoolMember(aliceId) - alice.createdAtTimestamp = BigInt.fromI32(1); - alice.createdAtBlockNumber = BigInt.fromI32(1); - alice.updatedAtTimestamp = BigInt.fromI32(1); - alice.updatedAtBlockNumber = BigInt.fromI32(1); - - alice.account = aliceAddress.toHexString(); - alice.units = BigInt.fromI32(100); - alice.totalAmountReceivedUntilUpdatedAt = BigInt.fromI32(0); - alice.poolTotalAmountDistributedUntilUpdatedAt = BigInt.fromI32(0); - alice.isConnected = true; - alice.totalAmountClaimed = BigInt.fromI32(0); - alice.pool = poolAddress.toHexString(); - alice.save(); - // # --- - - // ## Arrange Distributor - const poolDistributor = new PoolDistributor(poolAdminAndDistributorAddress.toHexString()); - poolDistributor.createdAtTimestamp = BigInt.fromI32(1); - poolDistributor.createdAtBlockNumber = BigInt.fromI32(1); - poolDistributor.updatedAtTimestamp = BigInt.fromI32(1); - poolDistributor.updatedAtBlockNumber = BigInt.fromI32(1); - poolDistributor.account = charlie; - poolDistributor.totalBuffer = BigInt.fromI32(0); - poolDistributor.flowRate = BigInt.fromI32(0); - poolDistributor.pool = poolAddress.toHexString(); - poolDistributor.totalAmountDistributedUntilUpdatedAt = BigInt.fromI32(0); - poolDistributor.totalAmountFlowedDistributedUntilUpdatedAt = BigInt.fromI32(0); - poolDistributor.totalAmountInstantlyDistributedUntilUpdatedAt = BigInt.fromI32(0); - poolDistributor.save(); - // --- + const aliceCreatedEvent = createMemberUnitsUpdatedEvent( + superTokenAddress, + aliceAddress.toHexString(), + BigInt.fromI32(0), // old units + BigInt.fromI32(100) // new units + ); + aliceCreatedEvent.address = poolAddress; + aliceCreatedEvent.block.timestamp = poolCreatedEvent.block.timestamp; - // # First distribution (State 2) - const instantDistributionEvent = createInstantDistributionUpdatedEvent( + mockedAppManifestAndRealtimeBalanceOf(superTokenAddress, aliceAddress.toHexString(), aliceCreatedEvent.block.timestamp); + handleMemberUnitsUpdated(aliceCreatedEvent); + + // # First distribution + const firstDistributionEvent = createInstantDistributionUpdatedEvent( superTokenAddress, poolAddress.toHexString(), poolAdminAndDistributorAddress.toHexString(), echo, - BigInt.fromI32(100), // requested amount - BigInt.fromI32(100), // actual amount + BigInt.fromI32(1000), // requested amount + BigInt.fromI32(1000), // actual amount Bytes.fromHexString("0x") ); - instantDistributionEvent.block.timestamp = BigInt.fromI32(1); - instantDistributionEvent.address = poolAddress; + firstDistributionEvent.block.timestamp = poolCreatedEvent.block.timestamp; + firstDistributionEvent.address = poolAddress; - mockedGetAppManifest(poolAdminAndDistributorAddress.toHexString(), false, false, BIG_INT_ZERO); - mockedRealtimeBalanceOf( - superTokenAddress, - poolAdminAndDistributorAddress.toHexString(), - BigInt.fromI32(1), - FAKE_INITIAL_BALANCE, - BigInt.fromI32(0), - BIG_INT_ZERO - ); - - handleInstantDistributionUpdated(instantDistributionEvent); + mockedAppManifestAndRealtimeBalanceOf(superTokenAddress, poolAdminAndDistributorAddress.toHexString(), firstDistributionEvent.block.timestamp); + handleInstantDistributionUpdated(firstDistributionEvent); assert.fieldEquals( "Pool", poolAddress.toHexString(), "totalAmountDistributedUntilUpdatedAt", + "1000" + ); + assert.fieldEquals( + "Pool", + poolAddress.toHexString(), + "totalUnits", "100" ); + assert.fieldEquals( + "Pool", + poolAddress.toHexString(), + "totalMembers", + "1" + ); assert.fieldEquals( "PoolMember", aliceId, "totalAmountReceivedUntilUpdatedAt", "0" ); - // # --- + assert.fieldEquals( + "PoolMember", + aliceId, + "units", + "100" + ); + // --- - // # Arrange State 3 + // # Arrange State 2 // ## Arrange PoolMember 2 (new member) const bobAddress = Address.fromString(bob_); const bobId = getPoolMemberID(poolAddress, bobAddress); - const bob = new PoolMember(bobId) - bob.createdAtTimestamp = BigInt.fromI32(1); - bob.createdAtBlockNumber = BigInt.fromI32(1); - bob.updatedAtTimestamp = BigInt.fromI32(1); - bob.updatedAtBlockNumber = BigInt.fromI32(1); - - bob.account = bobAddress.toHexString(); - bob.units = BigInt.fromI32(100); - bob.totalAmountReceivedUntilUpdatedAt = BigInt.fromI32(0); - bob.poolTotalAmountDistributedUntilUpdatedAt = BigInt.fromI32(100); - bob.isConnected = true; - bob.totalAmountClaimed = BigInt.fromI32(0); - bob.pool = poolAddress.toHexString(); - bob.save(); - // # --- - - // ## Update Pool for member 2 - pool = Pool.load(poolAddress.toHexString())!; - pool.updatedAtTimestamp = BigInt.fromI32(2); - pool.updatedAtBlockNumber = BigInt.fromI32(2); - pool.totalMembers = 2; - pool.totalConnectedMembers = 2; - pool.totalDisconnectedMembers = 0; - pool.totalConnectedUnits = BigInt.fromI32(2000); - pool.totalUnits = BigInt.fromI32(200); - pool.save(); - // --- + let createBobEvent = createMemberUnitsUpdatedEvent( + superTokenAddress, + bobAddress.toHexString(), + BigInt.fromI32(0), // old units + BigInt.fromI32(100) // new units + ); + createBobEvent.address = poolAddress; + createBobEvent.block.timestamp = BigInt.fromI32(2); + + mockedAppManifestAndRealtimeBalanceOf(superTokenAddress, bobAddress.toHexString(), createBobEvent.block.timestamp); + handleMemberUnitsUpdated(createBobEvent); + + // # Second distribution + const secondDistributionEvent = createInstantDistributionUpdatedEvent( + superTokenAddress, + poolAddress.toHexString(), + poolAdminAndDistributorAddress.toHexString(), + echo, + BigInt.fromI32(1000), // requested amount + BigInt.fromI32(1000), // actual amount + Bytes.fromHexString("0x") + ); + secondDistributionEvent.block.timestamp = poolCreatedEvent.block.timestamp; + secondDistributionEvent.address = poolAddress; + + mockedAppManifestAndRealtimeBalanceOf(superTokenAddress, poolAdminAndDistributorAddress.toHexString(), secondDistributionEvent.block.timestamp); + handleInstantDistributionUpdated(secondDistributionEvent); - // # Second distribution (we can use the first event again) (State 4) - handleInstantDistributionUpdated(instantDistributionEvent); - assert.fieldEquals( "Pool", poolAddress.toHexString(), "totalAmountDistributedUntilUpdatedAt", - "200" + "2000" ); assert.fieldEquals( "Pool", @@ -178,37 +141,248 @@ describe("PoolMember ending up with wrong `totalAmountReceivedUntilUpdatedAt`", "totalUnits", "200" ); - // # --- + assert.fieldEquals( + "PoolMember", + bobId, + "totalAmountReceivedUntilUpdatedAt", + "0" + ); + assert.fieldEquals( + "PoolMember", + bobId, + "units", + "100" + ); + // --- + // Arrange State 3 // # Update PoolMember 2's units to get the `totalAmountReceivedUntilUpdatedAt` - const updateBobUnitsEvent = createMemberUnitsUpdatedEvent( + const updateBobEvent = createMemberUnitsUpdatedEvent( superTokenAddress, bobAddress.toHexString(), BigInt.fromI32(100), // old units BigInt.fromI32(100) // new units ); // Note, the units can stay the same, we just want to trigger an update. - updateBobUnitsEvent.address = poolAddress; - updateBobUnitsEvent.block.timestamp = BigInt.fromI32(2); + updateBobEvent.address = poolAddress; + updateBobEvent.block.timestamp = BigInt.fromI32(3); + + mockedAppManifestAndRealtimeBalanceOf(superTokenAddress, bobAddress.toHexString(), updateBobEvent.block.timestamp); + handleMemberUnitsUpdated(createBobEvent); + + assert.fieldEquals( + "PoolMember", + bobId, + "totalAmountReceivedUntilUpdatedAt", + "500" + ); + assert.fieldEquals( + "PoolMember", + bobId, + "units", + "100" + ); + + // # Update PoolMember 1's units to get the `totalAmountReceivedUntilUpdatedAt` + const updateAliceEvent = createMemberUnitsUpdatedEvent( + superTokenAddress, + aliceAddress.toHexString(), + BigInt.fromI32(100), // old units + BigInt.fromI32(100) // new units + ); + // Note, the units can stay the same, we just want to trigger an update. + updateAliceEvent.address = poolAddress; + updateAliceEvent.block.timestamp = BigInt.fromI32(3); + + mockedAppManifestAndRealtimeBalanceOf(superTokenAddress, aliceAddress.toHexString(), updateAliceEvent.block.timestamp); + handleMemberUnitsUpdated(updateAliceEvent); + + assert.fieldEquals( + "PoolMember", + aliceId, + "totalAmountReceivedUntilUpdatedAt", + "1500" + ); + assert.fieldEquals( + "PoolMember", + aliceId, + "units", + "100" + ); + }); + /** + * Problem description + 1. Create pool + 2. Add member A and update A units to 100 + 3. Flow 1000 tokens (elapse 1 second) + 4. Add member B and update B units to 100 + 5. Flow 1000 tokens (elapse 1 second) + 6. Update flow rate to 2000 tokens + 7. Flow 2000 tokens (elapse 1 second) + + Expected result: + member A 2500 tokens + member B 1500 tokens + + Actual result: + member A 100 tokens + member B 50 tokens + */ + test("create elaborate scenario with 2 flowing distributions and 2 pool members", () => { + const superTokenAddress = maticXAddress; + const poolAdminAndDistributorAddress = Address.fromString(delta); + const poolAddress = Address.fromString(superfluidPool); + + // # Arrange State 1 + // ## Arrange Pool + const poolCreatedEvent = createPoolAndReturnPoolCreatedEvent(poolAdminAndDistributorAddress.toHexString(), superTokenAddress, poolAddress.toHexString()); + assert.stringEquals(poolCreatedEvent.block.timestamp.toString(), BigInt.fromI32(1).toString()); + + handlePoolCreated(poolCreatedEvent); + + const pool = Pool.load(poolAddress.toHexString()); + + if (pool) { + pool.updatedAtTimestamp = poolCreatedEvent.block.timestamp; + } + + // ## Arrange PoolMember 1 + const aliceAddress = Address.fromString(alice_); + const aliceId = getPoolMemberID(poolAddress, aliceAddress); + const aliceCreatedEvent = createMemberUnitsUpdatedEvent( + superTokenAddress, + aliceAddress.toHexString(), + BigInt.fromI32(0), // old units + BigInt.fromI32(100) // new units + ); + aliceCreatedEvent.address = poolAddress; + aliceCreatedEvent.block.timestamp = poolCreatedEvent.block.timestamp; // 1 + + mockedAppManifestAndRealtimeBalanceOf(superTokenAddress, aliceAddress.toHexString(), aliceCreatedEvent.block.timestamp); + handleMemberUnitsUpdated(aliceCreatedEvent); + + // # First flow rate + if (pool) { + pool.updatedAtTimestamp = aliceCreatedEvent.block.timestamp; + } + + const firstFlowRateEvent = createFlowDistributionUpdatedEvent( + superTokenAddress, + poolAddress.toHexString(), + poolAdminAndDistributorAddress.toHexString(), + echo, + BigInt.fromI32(0), // oldFlowRate + BigInt.fromI32(1000), // newDistributorToPoolFlowRate + BigInt.fromI32(1000), // newTotalDistributionFlowRate + poolAdminAndDistributorAddress.toHexString(), // adjustmentFlowRecipient + BigInt.fromI32(0), + Bytes.fromHexString("0x") + ); + firstFlowRateEvent.block.timestamp = poolCreatedEvent.block.timestamp; // 1 + firstFlowRateEvent.address = poolAddress; + + mockedAppManifestAndRealtimeBalanceOf(superTokenAddress, poolAdminAndDistributorAddress.toHexString(), firstFlowRateEvent.block.timestamp); + handleFlowDistributionUpdated(firstFlowRateEvent); + + // # First flow rate + if (pool) { + pool.updatedAtTimestamp = firstFlowRateEvent.block.timestamp; + } + + // TODO: This fails, how has this already flown??? + assert.fieldEquals( + "Pool", + poolAddress.toHexString(), + "totalAmountDistributedUntilUpdatedAt", + "0" // nothing is flowed yet + ); + assert.fieldEquals( + "Pool", + poolAddress.toHexString(), + "totalUnits", + "100" + ); + assert.fieldEquals( + "Pool", + poolAddress.toHexString(), + "totalMembers", + "1" + ); + assert.fieldEquals( + "PoolMember", + aliceId, + "totalAmountReceivedUntilUpdatedAt", + "0" + ); + assert.fieldEquals( + "PoolMember", + aliceId, + "units", + "100" + ); + // --- - mockedGetAppManifest(bobAddress.toHexString(), false, false, BIG_INT_ZERO); - mockedRealtimeBalanceOf( + // # Arrange State 2 + // ## Arrange PoolMember 2 (new member) + const bobAddress = Address.fromString(bob_); + const bobId = getPoolMemberID(poolAddress, bobAddress); + let createBobEvent = createMemberUnitsUpdatedEvent( superTokenAddress, bobAddress.toHexString(), - BigInt.fromI32(2), - FAKE_INITIAL_BALANCE, + BigInt.fromI32(0), // old units + BigInt.fromI32(100) // new units + ); + createBobEvent.address = poolAddress; + createBobEvent.block.timestamp = BigInt.fromI32(2); // Skip 1 second to let it flow to Alice + + mockedAppManifestAndRealtimeBalanceOf(superTokenAddress, bobAddress.toHexString(), createBobEvent.block.timestamp); + handleMemberUnitsUpdated(createBobEvent); + + if (pool) { + pool.updatedAtTimestamp = createBobEvent.block.timestamp; + } + + assert.fieldEquals( + "Pool", + poolAddress.toHexString(), + "totalAmountDistributedUntilUpdatedAt", + "1000" + ); + assert.fieldEquals( + "PoolMember", + bobId, + "totalAmountReceivedUntilUpdatedAt", + "0" // Bob just joined, shouldn't have any received + ); + + // # Second flow rate + const secondFlowRateEvent = createFlowDistributionUpdatedEvent( + superTokenAddress, + poolAddress.toHexString(), + poolAdminAndDistributorAddress.toHexString(), + echo, + BigInt.fromI32(1000), // oldFlowRate + BigInt.fromI32(2000), // newDistributorToPoolFlowRate + BigInt.fromI32(2000), // newTotalDistributionFlowRate + poolAdminAndDistributorAddress.toHexString(), // adjustmentFlowRecipient BigInt.fromI32(0), - BIG_INT_ZERO + Bytes.fromHexString("0x") ); + secondFlowRateEvent.block.timestamp = BigInt.fromI32(3); // One second skipped, 2 seconds flown to Alice, 1 second to Bob + secondFlowRateEvent.address = poolAddress; + + mockedAppManifestAndRealtimeBalanceOf(superTokenAddress, poolAdminAndDistributorAddress.toHexString(), secondFlowRateEvent.block.timestamp); + handleFlowDistributionUpdated(secondFlowRateEvent); - // Act 1 - handleMemberUnitsUpdated(updateBobUnitsEvent); + if (pool) { + pool.updatedAtTimestamp = secondFlowRateEvent.block.timestamp; + } assert.fieldEquals( "Pool", poolAddress.toHexString(), "totalAmountDistributedUntilUpdatedAt", - "200" + "2000" // Only for first flow rate ); assert.fieldEquals( "Pool", @@ -220,38 +394,87 @@ describe("PoolMember ending up with wrong `totalAmountReceivedUntilUpdatedAt`", "PoolMember", bobId, "totalAmountReceivedUntilUpdatedAt", - "50" + "0" // it's 500 if we query on-chain, but 0 here because update member units hasn't been called again since + ); + assert.fieldEquals( + "PoolMember", + bobId, + "units", + "100" + ); + assert.fieldEquals( + "PoolMember", + aliceId, + "totalAmountReceivedUntilUpdatedAt", + "0" // it's 1500 if we query on-chain, but 0 here because update member units hasn't been called again since ); + // --- - // # Update PoolMember 1's units to get the `totalAmountReceivedUntilUpdatedAt` - const updateAliceUnitsEvent = createMemberUnitsUpdatedEvent( + // Arrange State 3 + // # Update PoolMember 2's units to get the `totalAmountReceivedUntilUpdatedAt` + const updateBobEvent = createMemberUnitsUpdatedEvent( superTokenAddress, - aliceAddress.toHexString(), - BigInt.fromI32(10), // old units - BigInt.fromI32(10) // new units + bobAddress.toHexString(), + BigInt.fromI32(100), // old units + BigInt.fromI32(100) // new units ); // Note, the units can stay the same, we just want to trigger an update. - updateAliceUnitsEvent.address = poolAddress; - updateAliceUnitsEvent.block.timestamp = BigInt.fromI32(3); + updateBobEvent.address = poolAddress; + updateBobEvent.block.timestamp = BigInt.fromI32(4); // 4 - 1 = 3 seconds of flow - mockedGetAppManifest(aliceAddress.toHexString(), false, false, BIG_INT_ZERO); - mockedRealtimeBalanceOf( + mockedAppManifestAndRealtimeBalanceOf(superTokenAddress, bobAddress.toHexString(), updateBobEvent.block.timestamp); + handleMemberUnitsUpdated(updateBobEvent); + + if (pool) { + pool.updatedAtTimestamp = updateBobEvent.block.timestamp; + } + assert.fieldEquals( + "Pool", + poolAddress.toHexString(), + "totalAmountDistributedUntilUpdatedAt", + "4000" + ); + assert.fieldEquals( + "PoolMember", + bobId, + "totalAmountReceivedUntilUpdatedAt", + "1500" // 50% of 2000 + ); + assert.fieldEquals( + "PoolMember", + bobId, + "units", + "100" + ); + + // # Update PoolMember 1's units to get the `totalAmountReceivedUntilUpdatedAt` + const updateAliceEvent = createMemberUnitsUpdatedEvent( superTokenAddress, aliceAddress.toHexString(), - BigInt.fromI32(3), - FAKE_INITIAL_BALANCE, - BigInt.fromI32(0), - BIG_INT_ZERO + BigInt.fromI32(100), // old units + BigInt.fromI32(100) // new units ); + // Note, the units can stay the same, we just want to trigger an update. + updateAliceEvent.address = poolAddress; + updateAliceEvent.block.timestamp = updateBobEvent.block.timestamp; // 4 - // Act 2 - handleMemberUnitsUpdated(updateAliceUnitsEvent); + mockedAppManifestAndRealtimeBalanceOf(superTokenAddress, aliceAddress.toHexString(), updateAliceEvent.block.timestamp); + handleMemberUnitsUpdated(updateAliceEvent); + if (pool) { + pool.updatedAtTimestamp = updateAliceEvent.block.timestamp; + } assert.fieldEquals( "PoolMember", aliceId, "totalAmountReceivedUntilUpdatedAt", - "150" // 100 from first + 50 from second + "2500" + ); + assert.fieldEquals( + "PoolMember", + aliceId, + "units", + "100" ); }) }); diff --git a/packages/subgraph/tests/mockedFunctions.ts b/packages/subgraph/tests/mockedFunctions.ts index 88629bdc57..7ef88761b5 100644 --- a/packages/subgraph/tests/mockedFunctions.ts +++ b/packages/subgraph/tests/mockedFunctions.ts @@ -379,3 +379,19 @@ export function mockedApprove( ]) .returns([getETHUnsignedBigInt(expectedValue)]); } + +export function mockedAppManifestAndRealtimeBalanceOf( + tokenAddress: string, + accountAddress: string, + timestamp: BigInt +): void { + mockedGetAppManifest(accountAddress, false, false, BIG_INT_ZERO); + mockedRealtimeBalanceOf( + tokenAddress, + accountAddress, + timestamp, + FAKE_INITIAL_BALANCE, + BIG_INT_ZERO, + BIG_INT_ZERO + ); +}