From 793602451a177da5b5624c36310b43946191c656 Mon Sep 17 00:00:00 2001 From: Gerhard Steenkamp Date: Thu, 18 Sep 2025 13:38:46 +0200 Subject: [PATCH 1/8] derive custom bond key from currency Signed-off-by: Gerhard Steenkamp --- packages/managed-oracle-v2/schema.graphql | 6 +++++- .../src/mappings/managedOracleV2.ts | 7 ++++++- .../src/utils/helpers/managedOracleV2.ts | 15 +++++++++++++++ 3 files changed, 26 insertions(+), 2 deletions(-) diff --git a/packages/managed-oracle-v2/schema.graphql b/packages/managed-oracle-v2/schema.graphql index 8f9cd42..fcee21f 100644 --- a/packages/managed-oracle-v2/schema.graphql +++ b/packages/managed-oracle-v2/schema.graphql @@ -82,9 +82,11 @@ type OptimisticPriceRequest @entity { } type CustomBond @entity { - "ID is managedRequestId, ie. the hash of requester, identifier, ancillaryData" + "ID is the hash of requester, identifier, ancillaryData, currency" id: ID! + managedRequestId: String! + currency: Bytes! requester: Bytes! @@ -100,6 +102,8 @@ type CustomLiveness @entity { "ID is managedRequestId, ie. the hash of requester, identifier, ancillaryData" id: ID! + managedRequestId: String! + requester: Bytes! identifier: String! diff --git a/packages/managed-oracle-v2/src/mappings/managedOracleV2.ts b/packages/managed-oracle-v2/src/mappings/managedOracleV2.ts index f820b76..995ba4c 100644 --- a/packages/managed-oracle-v2/src/mappings/managedOracleV2.ts +++ b/packages/managed-oracle-v2/src/mappings/managedOracleV2.ts @@ -1,15 +1,19 @@ import { log } from "@graphprotocol/graph-ts"; import { CustomBond, CustomLiveness } from "../../generated/schema"; import { CustomBondSet, CustomLivenessSet } from "../../generated/ManagedOracleV2/ManagedOracleV2"; +import { createCustomBondIdFromEvent } from "../utils/helpers/managedOracleV2"; export function handleCustomBondSet(event: CustomBondSet): void { const managedRequestId = event.params.managedRequestId.toHexString(); log.debug("Custom Bond set event. Loading entity with managedRequestId, {}", [managedRequestId]); - let entity = CustomBond.load(managedRequestId); + const id = createCustomBondIdFromEvent(event); + + let entity = CustomBond.load(id); if (entity == null) { entity = new CustomBond(managedRequestId); + entity.managedRequestId = managedRequestId; entity.requester = event.params.requester; entity.identifier = event.params.identifier.toString(); entity.ancillaryData = event.params.ancillaryData.toHex(); @@ -28,6 +32,7 @@ export function handleCustomLivenessSet(event: CustomLivenessSet): void { if (entity == null) { entity = new CustomLiveness(managedRequestId); + entity.managedRequestId = managedRequestId; entity.requester = event.params.requester; entity.identifier = event.params.identifier.toString(); entity.ancillaryData = event.params.ancillaryData.toHex(); diff --git a/packages/managed-oracle-v2/src/utils/helpers/managedOracleV2.ts b/packages/managed-oracle-v2/src/utils/helpers/managedOracleV2.ts index f352971..6369996 100644 --- a/packages/managed-oracle-v2/src/utils/helpers/managedOracleV2.ts +++ b/packages/managed-oracle-v2/src/utils/helpers/managedOracleV2.ts @@ -1,6 +1,21 @@ import { Address, ByteArray, Bytes, crypto } from "@graphprotocol/graph-ts"; +import { CustomBondSet } from "../../../generated/ManagedOracleV2/ManagedOracleV2"; export function getManagedRequestId(requester: Address, identifier: Bytes, ancillaryData: Bytes): ByteArray { let packed = requester.concat(identifier).concat(ancillaryData); return crypto.keccak256(packed); } + +export function createCustomBondId(requester: Bytes, identifier: Bytes, ancillaryData: Bytes, currency: Bytes): string { + let packed = requester.concat(identifier).concat(ancillaryData).concat(currency); + return crypto.keccak256(packed).toHexString(); +} + +export function createCustomBondIdFromEvent(event: CustomBondSet): string { + return createCustomBondId( + event.params.requester, + event.params.identifier, + event.params.ancillaryData, + event.params.currency + ); +} From c82c69b8da20b3399084ea5ca4283e4b3b8b6c8f Mon Sep 17 00:00:00 2001 From: Gerhard Steenkamp Date: Thu, 18 Sep 2025 13:56:47 +0200 Subject: [PATCH 2/8] fixup Signed-off-by: Gerhard Steenkamp --- .../src/mappings/optimisticOracleV2.ts | 28 ++++++++++++++----- 1 file changed, 21 insertions(+), 7 deletions(-) diff --git a/packages/managed-oracle-v2/src/mappings/optimisticOracleV2.ts b/packages/managed-oracle-v2/src/mappings/optimisticOracleV2.ts index 8f945fb..06eaaca 100644 --- a/packages/managed-oracle-v2/src/mappings/optimisticOracleV2.ts +++ b/packages/managed-oracle-v2/src/mappings/optimisticOracleV2.ts @@ -12,15 +12,21 @@ import { getManagedRequestId, getOrCreateOptimisticPriceRequest } from "../utils import { CustomBond, CustomLiveness } from "../../generated/schema"; import { Address, BigInt, Bytes, dataSource, log } from "@graphprotocol/graph-ts"; +import { createCustomBondId } from "../utils/helpers/managedOracleV2"; let network = dataSource.network(); let isMainnet = network == "mainnet"; let isGoerli = network == "goerli"; -function getCustomBond(requester: Address, identifier: Bytes, ancillaryData: Bytes): CustomBond | null { - const managedRequestId = getManagedRequestId(requester, identifier, ancillaryData).toHexString(); - let customBondEntity = CustomBond.load(managedRequestId); +function getCustomBond( + requester: Address, + identifier: Bytes, + ancillaryData: Bytes, + currency: Bytes +): CustomBond | null { + const id = createCustomBondId(requester, identifier, ancillaryData, currency); + let customBondEntity = CustomBond.load(id); return customBondEntity ? customBondEntity : null; } @@ -118,7 +124,12 @@ export function handleOptimisticRequestPrice(event: RequestPrice): void { } // Look up custom bond and liveness values that may have been set before the request - let customBond = getCustomBond(event.params.requester, event.params.identifier, event.params.ancillaryData); + let customBond = getCustomBond( + event.params.requester, + event.params.identifier, + event.params.ancillaryData, + event.params.currency + ); if (customBond !== null) { const bond = customBond.customBond; const currency = customBond.currency; @@ -128,7 +139,6 @@ export function handleOptimisticRequestPrice(event: RequestPrice): void { requestId, ]); request.bond = bond; - request.currency = currency; } let customLiveness = getCustomLiveness(event.params.requester, event.params.identifier, event.params.ancillaryData); @@ -187,7 +197,12 @@ export function handleOptimisticProposePrice(event: ProposePrice): void { ); // Look up custom bond and liveness values that may have been set before the request - let customBond = getCustomBond(event.params.requester, event.params.identifier, event.params.ancillaryData); + let customBond = getCustomBond( + event.params.requester, + event.params.identifier, + event.params.ancillaryData, + event.params.currency + ); if (customBond !== null) { const bond = customBond.customBond; const currency = customBond.currency; @@ -197,7 +212,6 @@ export function handleOptimisticProposePrice(event: ProposePrice): void { requestId, ]); request.bond = bond; - request.currency = currency; } let customLiveness = getCustomLiveness(event.params.requester, event.params.identifier, event.params.ancillaryData); From 2dc4766c86dc4f33cf4f42a679fa1ac1b9865ba9 Mon Sep 17 00:00:00 2001 From: Gerhard Steenkamp Date: Thu, 18 Sep 2025 14:08:02 +0200 Subject: [PATCH 3/8] fixup Signed-off-by: Gerhard Steenkamp --- packages/managed-oracle-v2/src/mappings/managedOracleV2.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/managed-oracle-v2/src/mappings/managedOracleV2.ts b/packages/managed-oracle-v2/src/mappings/managedOracleV2.ts index 995ba4c..92895d5 100644 --- a/packages/managed-oracle-v2/src/mappings/managedOracleV2.ts +++ b/packages/managed-oracle-v2/src/mappings/managedOracleV2.ts @@ -12,7 +12,7 @@ export function handleCustomBondSet(event: CustomBondSet): void { let entity = CustomBond.load(id); if (entity == null) { - entity = new CustomBond(managedRequestId); + entity = new CustomBond(id); entity.managedRequestId = managedRequestId; entity.requester = event.params.requester; entity.identifier = event.params.identifier.toString(); From e6963b49c7e22bf4af8ec7461efffb34d8c4aa79 Mon Sep 17 00:00:00 2001 From: Gerhard Steenkamp Date: Thu, 18 Sep 2025 14:09:18 +0200 Subject: [PATCH 4/8] add test case Signed-off-by: Gerhard Steenkamp --- .../managed-oracle-v2/managedOracle.test.ts | 401 ++++++++++++------ 1 file changed, 267 insertions(+), 134 deletions(-) diff --git a/packages/managed-oracle-v2/tests/managed-oracle-v2/managedOracle.test.ts b/packages/managed-oracle-v2/tests/managed-oracle-v2/managedOracle.test.ts index 738db80..5019071 100644 --- a/packages/managed-oracle-v2/tests/managed-oracle-v2/managedOracle.test.ts +++ b/packages/managed-oracle-v2/tests/managed-oracle-v2/managedOracle.test.ts @@ -1,6 +1,7 @@ -import { describe, test, clearStore, afterAll, assert, log, afterEach } from "matchstick-as/assembly/index"; +import { describe, test, clearStore, assert, log, afterEach } from "matchstick-as/assembly/index"; import { handleCustomLivenessSet, handleCustomBondSet } from "../../src/mappings/managedOracleV2"; import { handleOptimisticProposePrice, handleOptimisticRequestPrice } from "../../src/mappings/optimisticOracleV2"; +import { createCustomBondIdFromEvent } from "../../src/utils/helpers/managedOracleV2"; import { createCustomLivenessSetEvent, createCustomBondSetEvent, @@ -15,35 +16,25 @@ import { BigInt, Bytes, Address } from "@graphprotocol/graph-ts"; // Tests structure (matchstick-as >=0.5.0) // https://thegraph.com/docs/en/developer/matchstick/#tests-structure-0-5-0 -namespace Constants { - export const managedRequestId = "0x8aed060a05dfbb279705824d8b544fc58a63ebc4a1c26380cbd90297c0a7e33c"; - export const requester = "0x9A8f92a830A5cB89a3816e3D267CB7791c16b04D"; - export const identifierHex = "0x00000000000000000000000000000000005945535f4f525f4e4f5f5155455259"; // "YES_OR_NO_QUERY" - export const identifierString = "YES_OR_NO_QUERY"; - export const ancillaryData = "0x5945535f4f525f4e4f5f5155455259"; - export const currency = "0x9b4A302A548c7e313c2b74C461db7b84d3074A84"; - export const customBond = 2000000; - export const customLiveness = 1757286231; - export const reward = 1000000; - export const finalFee = 500000; - export const timestamp = 1757284669; - export const customLiveness_2 = 123456; - export const customBond_2 = 3000000; - export const currency_2 = "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48"; -} - describe("Managed OOv2", () => { afterEach(() => { clearStore(); }); test("handleCustomLivenessSet creates CustomLiveness entity correctly", () => { + // Test variables + const managedRequestId = "0x8aed060a05dfbb279705824d8b544fc58a63ebc4a1c26380cbd90297c0a7e33c"; + const requester = "0x9A8f92a830A5cB89a3816e3D267CB7791c16b04D"; + const identifierHex = "0x00000000000000000000000000000000005945535f4f525f4e4f5f5155455259"; // "YES_OR_NO_QUERY" + const ancillaryData = "0x5945535f4f525f4e4f5f5155455259"; + const customLiveness = 1757286231; + const customLivenessEvent = createCustomLivenessSetEvent( - Constants.managedRequestId, - Constants.requester, - Constants.identifierHex, - Constants.ancillaryData, - Constants.customLiveness + managedRequestId, + requester, + identifierHex, + ancillaryData, + customLiveness ); handleCustomLivenessSet(customLivenessEvent); @@ -60,18 +51,18 @@ describe("Managed OOv2", () => { assert.addressEquals( Address.fromBytes(customLivenessEntity.requester), - Address.fromString(Constants.requester), + Address.fromString(requester), "Requester should match" ); assert.stringEquals(customLivenessEntity.identifier, "YES_OR_NO_QUERY", "Identifier should match"); assert.bytesEquals( Bytes.fromHexString(customLivenessEntity.ancillaryData), - Bytes.fromHexString(Constants.ancillaryData), + Bytes.fromHexString(ancillaryData), "Ancillary data should match" ); assert.bigIntEquals( customLivenessEntity.customLiveness, - BigInt.fromI32(Constants.customLiveness), + BigInt.fromI32(customLiveness), "Custom liveness should match" ); @@ -83,20 +74,30 @@ describe("Managed OOv2", () => { }); test("handleCustomBondSet creates CustomBond entity correctly", () => { + // Test variables + const managedRequestId = "0x8aed060a05dfbb279705824d8b544fc58a63ebc4a1c26380cbd90297c0a7e33c"; + const requester = "0x9A8f92a830A5cB89a3816e3D267CB7791c16b04D"; + const identifierHex = "0x00000000000000000000000000000000005945535f4f525f4e4f5f5155455259"; // "YES_OR_NO_QUERY" + const identifierString = "YES_OR_NO_QUERY"; + const ancillaryData = "0x5945535f4f525f4e4f5f5155455259"; + const currency = "0x9b4A302A548c7e313c2b74C461db7b84d3074A84"; + const customBond = 2000000; + const customBondEvent = createCustomBondSetEvent( - Constants.managedRequestId, - Constants.requester, - Constants.identifierHex, - Constants.ancillaryData, - Constants.currency, - Constants.customBond + managedRequestId, + requester, + identifierHex, + ancillaryData, + currency, + customBond ); handleCustomBondSet(customBondEvent); - const managedRequestIdActual = customBondEvent.params.managedRequestId.toHexString(); + // Generate the same ID that the mapping function uses + const customBondId = createCustomBondIdFromEvent(customBondEvent); - const customBondEntity = CustomBond.load(managedRequestIdActual); + const customBondEntity = CustomBond.load(customBondId); assert.assertTrue(customBondEntity !== null, "CustomBond entity should be created"); @@ -106,16 +107,16 @@ describe("Managed OOv2", () => { assert.addressEquals( Address.fromBytes(customBondEntity.requester), - Address.fromString(Constants.requester), + Address.fromString(requester), "Requester should match" ); - assert.stringEquals(customBondEntity.identifier, Constants.identifierString, "Identifier should match"); + assert.stringEquals(customBondEntity.identifier, identifierString, "Identifier should match"); assert.bytesEquals( Bytes.fromHexString(customBondEntity.ancillaryData), - Bytes.fromHexString(Constants.ancillaryData), + Bytes.fromHexString(ancillaryData), "Ancillary data should match" ); - assert.bigIntEquals(customBondEntity.customBond, BigInt.fromI32(Constants.customBond), "Custom bond should match"); + assert.bigIntEquals(customBondEntity.customBond, BigInt.fromI32(customBond), "Custom bond should match"); log.info("Created CustomBond entity: {}", [customBondEntity.id]); log.info("Requester: {}", [customBondEntity.requester.toHexString()]); @@ -125,51 +126,54 @@ describe("Managed OOv2", () => { }); test("Custom bond and liveness are applied to RequestPrice entity at REQUEST time", () => { - mockGetState( - Constants.requester, - Constants.identifierHex, - Constants.timestamp, - Constants.ancillaryData, - State.Requested - ); + // Test variables + const managedRequestId = "0x8aed060a05dfbb279705824d8b544fc58a63ebc4a1c26380cbd90297c0a7e33c"; + const requester = "0x9A8f92a830A5cB89a3816e3D267CB7791c16b04D"; + const identifierHex = "0x00000000000000000000000000000000005945535f4f525f4e4f5f5155455259"; // "YES_OR_NO_QUERY" + const identifierString = "YES_OR_NO_QUERY"; + const ancillaryData = "0x5945535f4f525f4e4f5f5155455259"; + const currency = "0x9b4A302A548c7e313c2b74C461db7b84d3074A84"; + const customBond = 2000000; + const customLiveness = 1757286231; + const reward = 1000000; + const finalFee = 500000; + const timestamp = 1757284669; + + mockGetState(requester, identifierHex, timestamp, ancillaryData, State.Requested); // Step 1: Set custom liveness const customLivenessEvent = createCustomLivenessSetEvent( - Constants.managedRequestId, - Constants.requester, - Constants.identifierHex, - Constants.ancillaryData, - Constants.customLiveness + managedRequestId, + requester, + identifierHex, + ancillaryData, + customLiveness ); handleCustomLivenessSet(customLivenessEvent); // Step 2: Set custom bond const customBondEvent = createCustomBondSetEvent( - Constants.managedRequestId, - Constants.requester, - Constants.identifierHex, - Constants.ancillaryData, - Constants.currency, - Constants.customBond + managedRequestId, + requester, + identifierHex, + ancillaryData, + currency, + customBond ); handleCustomBondSet(customBondEvent); // Step 3: Create RequestPrice event const requestPriceEvent = createRequestPriceEvent( - Constants.requester, - Constants.identifierHex, - Constants.timestamp, - Constants.ancillaryData, - Constants.currency, - Constants.reward, - Constants.finalFee + requester, + identifierHex, + timestamp, + ancillaryData, + currency, + reward, + finalFee ); handleOptimisticRequestPrice(requestPriceEvent); - const requestId = Constants.identifierString - .concat("-") - .concat(Constants.timestamp.toString()) - .concat("-") - .concat(Constants.ancillaryData); + const requestId = identifierString.concat("-").concat(timestamp.toString()).concat("-").concat(ancillaryData); const priceRequestEntity = OptimisticPriceRequest.load(requestId); @@ -179,111 +183,118 @@ describe("Managed OOv2", () => { return; } - assert.stringEquals(priceRequestEntity.identifier, Constants.identifierString, "Identifier should match"); - assert.bigIntEquals(priceRequestEntity.time, BigInt.fromI32(Constants.timestamp), "Timestamp should match"); + assert.stringEquals(priceRequestEntity.identifier, identifierString, "Identifier should match"); + assert.bigIntEquals(priceRequestEntity.time, BigInt.fromI32(timestamp), "Timestamp should match"); assert.bytesEquals( Bytes.fromHexString(priceRequestEntity.ancillaryData), - Bytes.fromHexString(Constants.ancillaryData), + Bytes.fromHexString(ancillaryData), "Ancillary data should match" ); assert.addressEquals( Address.fromBytes(priceRequestEntity.requester), - Address.fromString(Constants.requester), + Address.fromString(requester), "Requester should match" ); assert.addressEquals( Address.fromBytes(priceRequestEntity.currency), - Address.fromString(Constants.currency), + Address.fromString(currency), "Currency should match" ); - assert.bigIntEquals(priceRequestEntity.reward, BigInt.fromI32(Constants.reward), "Reward should match"); - assert.bigIntEquals(priceRequestEntity.finalFee, BigInt.fromI32(Constants.finalFee), "Final fee should match"); + assert.bigIntEquals(priceRequestEntity.reward, BigInt.fromI32(reward), "Reward should match"); + assert.bigIntEquals(priceRequestEntity.finalFee, BigInt.fromI32(finalFee), "Final fee should match"); // Assert custom values are applied assert.bigIntEquals( priceRequestEntity.customLiveness!, - BigInt.fromI32(Constants.customLiveness), + BigInt.fromI32(customLiveness), "Custom liveness should be applied to RequestPrice" ); - assert.bigIntEquals( - priceRequestEntity.bond!, - BigInt.fromI32(Constants.customBond), - "Custom bond should be applied to RequestPrice" - ); + + // Check if bond is set + assert.assertTrue(priceRequestEntity.bond !== null, "Bond should not be null - custom bond should be applied"); + if (priceRequestEntity.bond !== null) { + assert.bigIntEquals( + priceRequestEntity.bond!, + BigInt.fromI32(customBond), + "Custom bond should be applied to RequestPrice" + ); + } log.info("Created OptimisticPriceRequest entity: {}", [priceRequestEntity.id]); log.info("Custom Liveness: {}", [priceRequestEntity.customLiveness!.toString()]); - log.info("Custom Bond: {}", [priceRequestEntity.bond!.toString()]); + if (priceRequestEntity.bond !== null) { + log.info("Custom Bond: {}", [priceRequestEntity.bond!.toString()]); + } log.info("Custom Bond Currency: {}", [priceRequestEntity.currency!.toHexString()]); log.info("State: {}", [priceRequestEntity.state!]); }); test("Custom bond and liveness are applied to RequestPrice entity at PROPOSE time", () => { - mockGetState( - Constants.requester, - Constants.identifierHex, - Constants.timestamp, - Constants.ancillaryData, - State.Requested - ); + // Test variables + const managedRequestId = "0x8aed060a05dfbb279705824d8b544fc58a63ebc4a1c26380cbd90297c0a7e33c"; + const requester = "0x9A8f92a830A5cB89a3816e3D267CB7791c16b04D"; + const identifierHex = "0x00000000000000000000000000000000005945535f4f525f4e4f5f5155455259"; // "YES_OR_NO_QUERY" + const identifierString = "YES_OR_NO_QUERY"; + const ancillaryData = "0x5945535f4f525f4e4f5f5155455259"; + const currency = "0x9b4A302A548c7e313c2b74C461db7b84d3074A84"; + const customBond = 2000000; + const customLiveness = 1757286231; + const reward = 1000000; + const finalFee = 500000; + const timestamp = 1757284669; + const customLiveness_2 = 123456; + const customBond_2 = 3000000; + const currency_2 = "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48"; + + mockGetState(requester, identifierHex, timestamp, ancillaryData, State.Requested); // Step 1: Create RequestPrice event const requestPriceEvent = createRequestPriceEvent( - Constants.requester, - Constants.identifierHex, - Constants.timestamp, - Constants.ancillaryData, - Constants.currency, - Constants.reward, - Constants.finalFee + requester, + identifierHex, + timestamp, + ancillaryData, + currency, + reward, + finalFee ); handleOptimisticRequestPrice(requestPriceEvent); // Step 2: Set custom liveness const customLivenessEvent = createCustomLivenessSetEvent( - Constants.managedRequestId, - Constants.requester, - Constants.identifierHex, - Constants.ancillaryData, - Constants.customLiveness + managedRequestId, + requester, + identifierHex, + ancillaryData, + customLiveness ); handleCustomLivenessSet(customLivenessEvent); // Step 3: Set custom bond const customBondEvent = createCustomBondSetEvent( - Constants.managedRequestId, - Constants.requester, - Constants.identifierHex, - Constants.ancillaryData, - Constants.currency_2, - Constants.customBond_2 + managedRequestId, + requester, + identifierHex, + ancillaryData, + currency_2, + customBond_2 ); handleCustomBondSet(customBondEvent); - mockGetState( - Constants.requester, - Constants.identifierHex, - Constants.timestamp, - Constants.ancillaryData, - State.Proposed - ); + mockGetState(requester, identifierHex, timestamp, ancillaryData, State.Proposed); // Step 4: Create ProposePrice event const proposePriceEvent = createProposePriceEvent( - Constants.requester, - Constants.requester, - Constants.identifierHex, - Constants.timestamp, - Constants.ancillaryData, + requester, + requester, + identifierHex, + timestamp, + ancillaryData, 1, - Constants.timestamp + 3600, - Constants.currency + timestamp + 3600, + currency_2 // Use currency_2 to match the custom bond currency ); handleOptimisticProposePrice(proposePriceEvent); - const requestId = Constants.identifierString - .concat("-") - .concat(Constants.timestamp.toString()) - .concat("-") - .concat(Constants.ancillaryData); + const requestId = identifierString.concat("-").concat(timestamp.toString()).concat("-").concat(ancillaryData); const priceRequestEntity = OptimisticPriceRequest.load(requestId); @@ -296,26 +307,148 @@ describe("Managed OOv2", () => { // Assert custom values are applied assert.addressEquals( Address.fromBytes(priceRequestEntity.currency), - Address.fromString(Constants.currency_2), + Address.fromString(currency_2), "Currency should match" ); - assert.bigIntEquals( - priceRequestEntity.bond!, - BigInt.fromI32(Constants.customBond_2), - "Custom bond should be applied to RequestPrice" - ); + // Check if bond is set + assert.assertTrue(priceRequestEntity.bond !== null, "Bond should not be null - custom bond should be applied"); + if (priceRequestEntity.bond !== null) { + assert.bigIntEquals( + priceRequestEntity.bond!, + BigInt.fromI32(customBond_2), + "Custom bond should be applied to RequestPrice" + ); + } assert.bigIntEquals( priceRequestEntity.customLiveness!, - BigInt.fromI32(Constants.customLiveness), + BigInt.fromI32(customLiveness), "Custom liveness should be applied to RequestPrice" ); log.info("Created OptimisticPriceRequest entity: {}", [priceRequestEntity.id]); log.info("Custom Liveness: {}", [priceRequestEntity.customLiveness!.toString()]); log.info("State: {}", [priceRequestEntity.state!]); - log.info("Custom Bond: {}", [priceRequestEntity.bond!.toString()]); + if (priceRequestEntity.bond !== null) { + log.info("Custom Bond: {}", [priceRequestEntity.bond!.toString()]); + } log.info("Custom Bond Currency: {}", [priceRequestEntity.currency!.toHexString()]); }); + + test("Custom bond currency matching - only matching currency bond is applied (no currency changes)", () => { + // Test variables + const managedRequestId = "0x8aed060a05dfbb279705824d8b544fc58a63ebc4a1c26380cbd90297c0a7e33c"; + const requester = "0x9A8f92a830A5cB89a3816e3D267CB7791c16b04D"; + const identifierHex = "0x00000000000000000000000000000000005945535f4f525f4e4f5f5155455259"; // "YES_OR_NO_QUERY" + const identifierString = "YES_OR_NO_QUERY"; + const ancillaryData = "0x5945535f4f525f4e4f5f5155455259"; + + // Two different currencies and bond amounts + const currencyA = "0x9b4A302A548c7e313c2b74C461db7b84d3074A84"; // Token A + const bondAmountA = 2000000; // Bond amount A for Token A + + const currencyB = "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48"; // Token B + const bondAmountB = 3000000; // Bond amount B for Token B + + const reward = 1000000; + const finalFee = 500000; + const timestamp = 1757284669; + + mockGetState(requester, identifierHex, timestamp, ancillaryData, State.Requested); + + // Step 1: Set custom bond for Token A with bond amount A + const customBondEventA = createCustomBondSetEvent( + managedRequestId, + requester, + identifierHex, + ancillaryData, + currencyA, + bondAmountA + ); + handleCustomBondSet(customBondEventA); + + // Step 2: Set custom bond for Token B with bond amount B + const customBondEventC = createCustomBondSetEvent( + managedRequestId, + requester, + identifierHex, + ancillaryData, + currencyB, + bondAmountB + ); + handleCustomBondSet(customBondEventC); + + // Step 3: Create RequestPrice event with Token B as currency + // This should only apply the custom bond for Token B (bond amount B) + const requestPriceEvent = createRequestPriceEvent( + requester, + identifierHex, + timestamp, + ancillaryData, + currencyB, // Using Token B as currency + reward, + finalFee + ); + handleOptimisticRequestPrice(requestPriceEvent); + + const requestId = identifierString.concat("-").concat(timestamp.toString()).concat("-").concat(ancillaryData); + + const priceRequestEntity = OptimisticPriceRequest.load(requestId); + + assert.assertTrue(priceRequestEntity !== null, "OptimisticPriceRequest entity should be created"); + + if (priceRequestEntity === null) { + return; + } + + // Verify the request was created with correct basic parameters + assert.stringEquals(priceRequestEntity.identifier, identifierString, "Identifier should match"); + assert.bigIntEquals(priceRequestEntity.time, BigInt.fromI32(timestamp), "Timestamp should match"); + assert.bytesEquals( + Bytes.fromHexString(priceRequestEntity.ancillaryData), + Bytes.fromHexString(ancillaryData), + "Ancillary data should match" + ); + assert.addressEquals( + Address.fromBytes(priceRequestEntity.requester), + Address.fromString(requester), + "Requester should match" + ); + assert.addressEquals( + Address.fromBytes(priceRequestEntity.currency), + Address.fromString(currencyB), + "Currency should be Token C" + ); + assert.bigIntEquals(priceRequestEntity.reward, BigInt.fromI32(reward), "Reward should match"); + assert.bigIntEquals(priceRequestEntity.finalFee, BigInt.fromI32(finalFee), "Final fee should match"); + + // Check if bond is null first + assert.assertTrue(priceRequestEntity.bond !== null, "Bond should not be null - custom bond should be applied"); + + if (priceRequestEntity.bond === null) { + return; + } + + // CRITICAL ASSERTION: Only the custom bond for Token B (bond amount B) should be applied + // The custom bond for Token A (bond amount A) should NOT be applied + assert.bigIntEquals( + priceRequestEntity.bond!, + BigInt.fromI32(bondAmountB), // Should be bond amount B (for Token B) + "Custom bond should be bond amount B (for Token B), not bond amount A (for Token A)" + ); + + // Verify that the bond amount is NOT the amount set for Token A + assert.assertTrue( + !priceRequestEntity.bond!.equals(BigInt.fromI32(bondAmountA)), + "Custom bond should NOT be bond amount A (for Token A)" + ); + + log.info("Created OptimisticPriceRequest entity: {}", [priceRequestEntity.id]); + if (priceRequestEntity.bond !== null) { + log.info("Applied Custom Bond: {} (should be bond amount B for Token B)", [priceRequestEntity.bond!.toString()]); + } + log.info("Currency: {} (should be Token B)", [priceRequestEntity.currency!.toHexString()]); + log.info("State: {}", [priceRequestEntity.state!]); + }); }); From 0502fd4efae181fc47347c0465f329374f8cd5e2 Mon Sep 17 00:00:00 2001 From: Gerhard Steenkamp Date: Thu, 18 Sep 2025 14:13:37 +0200 Subject: [PATCH 5/8] add some comments Signed-off-by: Gerhard Steenkamp --- .../src/mappings/managedOracleV2.ts | 8 ++++++++ .../src/mappings/optimisticOracleV2.ts | 18 ++++++++++++++++++ .../src/utils/helpers/managedOracleV2.ts | 15 +++++++++++++++ 3 files changed, 41 insertions(+) diff --git a/packages/managed-oracle-v2/src/mappings/managedOracleV2.ts b/packages/managed-oracle-v2/src/mappings/managedOracleV2.ts index 92895d5..758aaa2 100644 --- a/packages/managed-oracle-v2/src/mappings/managedOracleV2.ts +++ b/packages/managed-oracle-v2/src/mappings/managedOracleV2.ts @@ -3,10 +3,18 @@ import { CustomBond, CustomLiveness } from "../../generated/schema"; import { CustomBondSet, CustomLivenessSet } from "../../generated/ManagedOracleV2/ManagedOracleV2"; import { createCustomBondIdFromEvent } from "../utils/helpers/managedOracleV2"; +/** + * Handles CustomBondSet events from the ManagedOracleV2 contract. + * + * Creates or updates a CustomBond entity with a unique ID that includes the currency. + * This ensures that custom bonds are tied to specific currencies and can only be + * found when the request currency matches the custom bond currency. + */ export function handleCustomBondSet(event: CustomBondSet): void { const managedRequestId = event.params.managedRequestId.toHexString(); log.debug("Custom Bond set event. Loading entity with managedRequestId, {}", [managedRequestId]); + // Generate unique ID that includes currency - this ensures currency matching const id = createCustomBondIdFromEvent(event); let entity = CustomBond.load(id); diff --git a/packages/managed-oracle-v2/src/mappings/optimisticOracleV2.ts b/packages/managed-oracle-v2/src/mappings/optimisticOracleV2.ts index 06eaaca..1141817 100644 --- a/packages/managed-oracle-v2/src/mappings/optimisticOracleV2.ts +++ b/packages/managed-oracle-v2/src/mappings/optimisticOracleV2.ts @@ -19,6 +19,16 @@ let network = dataSource.network(); let isMainnet = network == "mainnet"; let isGoerli = network == "goerli"; +/** + * Retrieves a custom bond entity if one exists for the given parameters. + * + * IMPORTANT: Custom bonds are stored with a unique ID that includes the currency. + * This means we can only find custom bonds that match the EXACT currency used in the request. + * If a custom bond was set for a different currency, it will NOT be found here. + * + * This ensures that custom bonds are only applied when the currency matches, + * preventing incorrect bond amounts from being applied to requests with different currencies. + */ function getCustomBond( requester: Address, identifier: Bytes, @@ -124,6 +134,8 @@ export function handleOptimisticRequestPrice(event: RequestPrice): void { } // Look up custom bond and liveness values that may have been set before the request + // Custom bonds are stored with a unique ID that includes the currency, so we only find + // custom bonds that match the exact currency used in this request let customBond = getCustomBond( event.params.requester, event.params.identifier, @@ -138,6 +150,8 @@ export function handleOptimisticRequestPrice(event: RequestPrice): void { currency.toHexString(), requestId, ]); + // Apply the custom bond amount - the currency is guaranteed to match since + // the custom bond ID includes the currency and we looked it up using the request's currency request.bond = bond; } @@ -197,6 +211,8 @@ export function handleOptimisticProposePrice(event: ProposePrice): void { ); // Look up custom bond and liveness values that may have been set before the request + // Custom bonds are stored with a unique ID that includes the currency, so we only find + // custom bonds that match the exact currency used in this request let customBond = getCustomBond( event.params.requester, event.params.identifier, @@ -211,6 +227,8 @@ export function handleOptimisticProposePrice(event: ProposePrice): void { currency.toHexString(), requestId, ]); + // Apply the custom bond amount - the currency is guaranteed to match since + // the custom bond ID includes the currency and we looked it up using the request's currency request.bond = bond; } diff --git a/packages/managed-oracle-v2/src/utils/helpers/managedOracleV2.ts b/packages/managed-oracle-v2/src/utils/helpers/managedOracleV2.ts index 6369996..8fc0b9e 100644 --- a/packages/managed-oracle-v2/src/utils/helpers/managedOracleV2.ts +++ b/packages/managed-oracle-v2/src/utils/helpers/managedOracleV2.ts @@ -6,11 +6,26 @@ export function getManagedRequestId(requester: Address, identifier: Bytes, ancil return crypto.keccak256(packed); } +/** + * Creates a unique ID for a custom bond entity. + * + * Including the currency in the ID ensures that custom bonds are tied to specific currencies. + * This means that custom bonds set for different currencies will have different IDs, + * even if all other parameters (requester, identifier, ancillary data) are the same. + * + * This is crucial for the currency matching logic - we can only find custom bonds + * that match the exact currency used in a request. + */ export function createCustomBondId(requester: Bytes, identifier: Bytes, ancillaryData: Bytes, currency: Bytes): string { let packed = requester.concat(identifier).concat(ancillaryData).concat(currency); return crypto.keccak256(packed).toHexString(); } +/** + * Creates a custom bond ID from a CustomBondSet event. + * This is a convenience function that extracts the parameters from the event + * and passes them to createCustomBondId. + */ export function createCustomBondIdFromEvent(event: CustomBondSet): string { return createCustomBondId( event.params.requester, From 94556f169fe209b8b5fb3f078765853d44568253 Mon Sep 17 00:00:00 2001 From: Gerhard Steenkamp Date: Thu, 18 Sep 2025 14:21:10 +0200 Subject: [PATCH 6/8] fixup Signed-off-by: Gerhard Steenkamp --- .../managed-oracle-v2/managedOracle.test.ts | 23 ++++++++----------- 1 file changed, 10 insertions(+), 13 deletions(-) diff --git a/packages/managed-oracle-v2/tests/managed-oracle-v2/managedOracle.test.ts b/packages/managed-oracle-v2/tests/managed-oracle-v2/managedOracle.test.ts index 5019071..1b46b17 100644 --- a/packages/managed-oracle-v2/tests/managed-oracle-v2/managedOracle.test.ts +++ b/packages/managed-oracle-v2/tests/managed-oracle-v2/managedOracle.test.ts @@ -229,7 +229,7 @@ describe("Managed OOv2", () => { log.info("State: {}", [priceRequestEntity.state!]); }); - test("Custom bond and liveness are applied to RequestPrice entity at PROPOSE time", () => { + test("Custom bond currency mismatch - bond not applied when currencies don't match", () => { // Test variables const managedRequestId = "0x8aed060a05dfbb279705824d8b544fc58a63ebc4a1c26380cbd90297c0a7e33c"; const requester = "0x9A8f92a830A5cB89a3816e3D267CB7791c16b04D"; @@ -290,7 +290,7 @@ describe("Managed OOv2", () => { ancillaryData, 1, timestamp + 3600, - currency_2 // Use currency_2 to match the custom bond currency + currency // Use same currency as original request ); handleOptimisticProposePrice(proposePriceEvent); @@ -307,19 +307,16 @@ describe("Managed OOv2", () => { // Assert custom values are applied assert.addressEquals( Address.fromBytes(priceRequestEntity.currency), - Address.fromString(currency_2), - "Currency should match" + Address.fromString(currency), + "Currency should match original request currency" ); - // Check if bond is set - assert.assertTrue(priceRequestEntity.bond !== null, "Bond should not be null - custom bond should be applied"); - if (priceRequestEntity.bond !== null) { - assert.bigIntEquals( - priceRequestEntity.bond!, - BigInt.fromI32(customBond_2), - "Custom bond should be applied to RequestPrice" - ); - } + // Check if bond is set - since custom bond was set for different currency, + // it should NOT be applied (currency mismatch) + assert.assertTrue( + priceRequestEntity.bond === null, + "Bond should be null - custom bond should NOT be applied due to currency mismatch" + ); assert.bigIntEquals( priceRequestEntity.customLiveness!, From de9748c0bbe3566d01a200b2f7fa052c3cb72ea4 Mon Sep 17 00:00:00 2001 From: Gerhard Steenkamp Date: Thu, 18 Sep 2025 14:27:14 +0200 Subject: [PATCH 7/8] update readme Signed-off-by: Gerhard Steenkamp --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index abce00c..6d1c060 100644 --- a/README.md +++ b/README.md @@ -146,10 +146,10 @@ This subgraph indexes events and function calls by the "Managed Optimistic Oracl - Amoy - TheGraph: - - Goldsky: + - Goldsky: - Polygon - TheGraph: - - Goldsky: + - Goldsky: ## Financial Contract Events From e1f62f55de5918dd7fe530bc2f1a12bad350334f Mon Sep 17 00:00:00 2001 From: Gerhard Steenkamp Date: Thu, 18 Sep 2025 14:43:30 +0200 Subject: [PATCH 8/8] remove redundant field Signed-off-by: Gerhard Steenkamp --- packages/managed-oracle-v2/schema.graphql | 2 -- packages/managed-oracle-v2/src/mappings/managedOracleV2.ts | 1 - 2 files changed, 3 deletions(-) diff --git a/packages/managed-oracle-v2/schema.graphql b/packages/managed-oracle-v2/schema.graphql index fcee21f..536150e 100644 --- a/packages/managed-oracle-v2/schema.graphql +++ b/packages/managed-oracle-v2/schema.graphql @@ -102,8 +102,6 @@ type CustomLiveness @entity { "ID is managedRequestId, ie. the hash of requester, identifier, ancillaryData" id: ID! - managedRequestId: String! - requester: Bytes! identifier: String! diff --git a/packages/managed-oracle-v2/src/mappings/managedOracleV2.ts b/packages/managed-oracle-v2/src/mappings/managedOracleV2.ts index 758aaa2..9ee7c5c 100644 --- a/packages/managed-oracle-v2/src/mappings/managedOracleV2.ts +++ b/packages/managed-oracle-v2/src/mappings/managedOracleV2.ts @@ -40,7 +40,6 @@ export function handleCustomLivenessSet(event: CustomLivenessSet): void { if (entity == null) { entity = new CustomLiveness(managedRequestId); - entity.managedRequestId = managedRequestId; entity.requester = event.params.requester; entity.identifier = event.params.identifier.toString(); entity.ancillaryData = event.params.ancillaryData.toHex();