diff --git a/carbonmark/src/utils/version.ts b/carbonmark/src/utils/version.ts index 85f34af8..d6a8cbb7 100644 --- a/carbonmark/src/utils/version.ts +++ b/carbonmark/src/utils/version.ts @@ -1,3 +1,3 @@ // This file is auto-generated by set-version.ts - export const SCHEMA_VERSION = '1.6.8'; - export const PUBLISHED_VERSION = '1.6.7'; \ No newline at end of file + export const SCHEMA_VERSION = '1.6.11'; + export const PUBLISHED_VERSION = '1.6.11'; \ No newline at end of file diff --git a/carbonmark/version.json b/carbonmark/version.json index 143e4f14..c37e919d 100644 --- a/carbonmark/version.json +++ b/carbonmark/version.json @@ -1 +1 @@ -{"schemaVersion": "1.6.8", "publishedVersion": "1.6.7"} +{"schemaVersion": "1.6.11", "publishedVersion": "1.6.11"} diff --git a/lib/utils/Constants.ts b/lib/utils/Constants.ts index 0991872b..0b38a732 100644 --- a/lib/utils/Constants.ts +++ b/lib/utils/Constants.ts @@ -142,6 +142,3 @@ export const ICR_MIGRATION_HASHES = [ // Global constants export const KGS_PER_TONNE = BigDecimal.fromString('1000') - -// Price guard constants -export const MIN_PRICE_GUARD = BigInt.fromI32(1000) diff --git a/pairs/package.json b/pairs/package.json index e4402cac..7fa2ecb4 100644 --- a/pairs/package.json +++ b/pairs/package.json @@ -1,6 +1,6 @@ { "name": "klimadao-pairs", - "version": "1.0.8", + "version": "1.1.0", "license": "MIT", "scripts": { "codegen": "graph codegen", diff --git a/pairs/schema.graphql b/pairs/schema.graphql index 44d44e73..e1d8b5b2 100644 --- a/pairs/schema.graphql +++ b/pairs/schema.graphql @@ -31,6 +31,13 @@ type Swap @entity { pair: Pair! } +type PairTwapState @entity { + id: ID! # pair address + price0Cumul: BigInt! # last snapshot of price0CumulativeLast + price1Cumul: BigInt! # last snapshot of price1CumulativeLast + timestamp: BigInt! # snapshot timestamp +} + # Subgraph Versioning type SubgraphVersion @entity(immutable: true) { diff --git a/pairs/src/Pair.ts b/pairs/src/Pair.ts index aaad55e2..b6c84f67 100644 --- a/pairs/src/Pair.ts +++ b/pairs/src/Pair.ts @@ -17,10 +17,9 @@ import { KLIMA_BCT_PAIR_BLOCK, KLIMA_MCO2_PAIR_BLOCK, KLIMA_CCO2_PAIR_BLOCK, - MIN_PRICE_GUARD, } from '../../lib/utils/Constants' import { BigInt, BigDecimal, log, ethereum } from '@graphprotocol/graph-ts' -import { Pair, Token, Swap, SubgraphVersion } from '../generated/schema' +import { Pair, Token, Swap, SubgraphVersion, PairTwapState } from '../generated/schema' import { Swap as SwapEvent, Pair as PairContract } from '../generated/KLIMA_USDC/Pair' import { ERC20 as ERC20Contract } from '../generated/KLIMA_USDC/ERC20' import { Address } from '@graphprotocol/graph-ts' @@ -30,6 +29,12 @@ import { hourTimestamp } from '../../lib/utils/Dates' import { PriceUtil } from '../../lib/utils/Price' import { SCHEMA_VERSION, PUBLISHED_VERSION } from './utils/version' +const Q112 = BigInt.fromI32(2).pow(112 as u8) + +export function decodeUQ112x112(x: BigInt): BigDecimal { + return x.toBigDecimal().div(Q112.toBigDecimal()) +} + // Create or Load Token export function getCreateToken(address: Address): Token { let token = Token.load(address.toHexString()) @@ -150,27 +155,6 @@ function toUnits(x: BigInt, decimals: number): BigDecimal { return x.toBigDecimal().div(denom) } -function minPriceGuard( - quote: BigInt, - token0_dec: number, - token1_dec: number, - amountIn: BigInt, - amountOut: BigInt, - hash: string, - pair: PairContract -): BigDecimal { - // if value is greater than minimum, return price as is and avoid unnecessary calls - if (quote.ge(MIN_PRICE_GUARD)) { - return toUnits(amountIn, token0_dec).div(toUnits(amountOut, token1_dec)) - } - // throw a warning in the logs if triggered - log.warning('Price guard triggered for {}', [hash]) - // if value is less than minimum and the 10^6 <> 10^18 diff blows up the math, return pair spot price - let reserves = pair.getReserves() - - return toUnits(reserves.value0, token0_dec).div(toUnits(reserves.value1, token1_dec)) -} - export function handleSwap(event: SwapEvent): void { let treasury_address = TREASURY_ADDRESS let pair = getCreatePair(event.address) @@ -178,7 +162,6 @@ export function handleSwap(event: SwapEvent): void { let total_lp = toUnits(contract.totalSupply(), 18) let tokenBalance = toUnits(contract.balanceOf(treasury_address), 18) let ownedLP = total_lp == BigDecimalZero ? BigDecimalZero : tokenBalance.div(total_lp) - let hour_timestamp = hourTimestamp(event.block.timestamp) let hourlyId = event.address.toHexString() + hour_timestamp @@ -205,16 +188,43 @@ export function handleSwap(event: SwapEvent): void { volume = token0qty } + const TWAP_WINDOW = BigInt.fromI32(600) // 10 min window + let twapState = PairTwapState.load(event.address.toHexString()) + if (twapState == null) { + twapState = new PairTwapState(event.address.toHexString()) + twapState.price0Cumul = BigIntZero + twapState.price1Cumul = BigIntZero + twapState.timestamp = BigIntZero + } + + let price0CumulativeLast = contract.price0CumulativeLast() + let price1CumulativeLast = contract.price1CumulativeLast() + let now = event.block.timestamp + + // https://docs.uniswap.org/contracts/v2/concepts/core-concepts/oracles + // update price using TWAP if the window has passed + if (twapState.timestamp.gt(BigIntZero) && now.minus(twapState.timestamp).ge(TWAP_WINDOW)) { + let timeDiff = now.minus(twapState.timestamp) // seconds + // we set the price using the averaged price of the last 10 minutes + let twap0 = decodeUQ112x112(price0CumulativeLast.minus(twapState.price0Cumul)) // price0 × dt + .div(timeDiff.toBigDecimal()) // ← average price0 + price = twap0 + } + + //for initial swap or those within the first 10 minutes, read reserves and set price + if (price == BigDecimalZero) { + const r = contract.getReserves() + const t0 = (Token.load(pair.token0) as Token).decimals + const t1 = (Token.load(pair.token1) as Token).decimals + if (r.value0.gt(BigIntZero) && r.value1.gt(BigIntZero)) { + price = toUnits(r.value0, t0).div(toUnits(r.value1, t1)) + } else { + /* no liquidity → ignore this dust swap */ + return + } + } + if (event.params.amount0In == BigIntZero && event.params.amount0Out != BigIntZero) { - price = minPriceGuard( - event.params.amount0Out, - token0_decimals, - token1_decimals, - event.params.amount0Out, - event.params.amount1In, - event.transaction.hash.toHexString(), - contract - ) token0qty = toUnits(event.params.amount0Out, token0_decimals) token1qty = toUnits(event.params.amount1In, token1_decimals) lastreserves0 = toUnits(contract.getReserves().value0, token0_decimals).plus(token0qty) @@ -226,15 +236,6 @@ export function handleSwap(event: SwapEvent): void { volume = token0qty } if (event.params.amount0Out == BigIntZero && event.params.amount0In != BigIntZero) { - price = minPriceGuard( - event.params.amount0In, - token0_decimals, - token1_decimals, - event.params.amount0In, - event.params.amount1Out, - event.transaction.hash.toHexString(), - contract - ) token0qty = toUnits(event.params.amount0In, token0_decimals) token1qty = toUnits(event.params.amount1Out, token1_decimals) lastreserves0 = toUnits(contract.getReserves().value0, token0_decimals).minus(token0qty) @@ -366,6 +367,11 @@ export function handleSwap(event: SwapEvent): void { pair.lastupdate = hour_timestamp pair.save() } + + twapState.price0Cumul = price0CumulativeLast + twapState.price1Cumul = price1CumulativeLast + twapState.timestamp = now + twapState.save() } export function handleSetSubgraphVersion(block: ethereum.Block): void { diff --git a/pairs/src/utils/version.ts b/pairs/src/utils/version.ts index de9412d1..7aabc389 100644 --- a/pairs/src/utils/version.ts +++ b/pairs/src/utils/version.ts @@ -1,3 +1,3 @@ // This file is auto-generated by set-version.ts - export const SCHEMA_VERSION = '1.0.5'; - export const PUBLISHED_VERSION = '1.0.5'; \ No newline at end of file + export const SCHEMA_VERSION = '1.0.8'; + export const PUBLISHED_VERSION = '1.0.8'; \ No newline at end of file diff --git a/pairs/tests/.bin/swaps.wasm b/pairs/tests/.bin/swaps.wasm index 7c711c03..c2603e0a 100644 Binary files a/pairs/tests/.bin/swaps.wasm and b/pairs/tests/.bin/swaps.wasm differ diff --git a/pairs/tests/.latest.json b/pairs/tests/.latest.json index b5f878ff..97dbc702 100644 --- a/pairs/tests/.latest.json +++ b/pairs/tests/.latest.json @@ -1,4 +1,4 @@ { "version": "0.6.0", - "timestamp": 1745853557541 + "timestamp": 1750453481025 } \ No newline at end of file diff --git a/pairs/tests/swaps.test.ts b/pairs/tests/swaps.test.ts index 93c874b4..80a88a4d 100644 --- a/pairs/tests/swaps.test.ts +++ b/pairs/tests/swaps.test.ts @@ -1,18 +1,30 @@ -import { clearStore, test, describe, newMockEvent, beforeEach, assert, log, afterEach } from 'matchstick-as' +import { + clearStore, + test, + describe, + newMockEvent, + beforeEach, + assert, + log, + afterEach, + createMockedFunction, +} from 'matchstick-as' import { Address, BigInt, ethereum } from '@graphprotocol/graph-ts' -import { Pair, Swap as SwapEvent } from '../generated/KLIMA_USDC/Pair' +import { Swap as SwapEvent } from '../generated/KLIMA_USDC/Pair' +import { PairTwapState } from '../generated/schema' import { handleSwap } from '../src/Pair' import { KLIMA_CCO2_PAIR, NCT_USDC_PAIR } from '../../lib/utils/Constants' import { create_SWAP_EVENT_MOCKS } from './swapsHelper.test' -// Helper function to create a Swap event +// Helper function to create a Swap event function newSwapEvent( address: Address, amount0In: BigInt, amount1In: BigInt, amount0Out: BigInt, amount1Out: BigInt, - to: Address + to: Address, + blockTimestamp: BigInt = BigInt.fromI32(0) // default to 0 ): SwapEvent { let mockEvent = newMockEvent() let swapEvent = new SwapEvent( @@ -35,12 +47,34 @@ function newSwapEvent( swapEvent.parameters.push(new ethereum.EventParam('amount1Out', ethereum.Value.fromUnsignedBigInt(amount1Out))) swapEvent.parameters.push(new ethereum.EventParam('to', ethereum.Value.fromAddress(to))) + // Set block timestamp to 0 to avoid TWAP logic and use reserves to set price + swapEvent.block.timestamp = blockTimestamp + return swapEvent } describe('handleSwap', () => { beforeEach(() => { create_SWAP_EVENT_MOCKS() + + // Set up realistic TWAP mocks for KLIMA_CCO2_PAIR + // Simulate price0 cumulative that increased by 1e18 over 600 seconds (10 minutes) + // This represents a price change from 1.0 to 2.0 over 10 minutes + createMockedFunction(KLIMA_CCO2_PAIR, 'price0CumulativeLast', 'price0CumulativeLast():(uint256)').returns([ + ethereum.Value.fromUnsignedBigInt(BigInt.fromString('2000000000000000000000000')), // Current cumulative + ]) + createMockedFunction(KLIMA_CCO2_PAIR, 'price1CumulativeLast', 'price1CumulativeLast():(uint256)').returns([ + ethereum.Value.fromUnsignedBigInt(BigInt.fromString('1500000000000000000000000')), // Different cumulative + ]) + + // Set up realistic TWAP mocks for NCT_USDC_PAIR + // Simulate a more stable price with smaller changes + createMockedFunction(NCT_USDC_PAIR, 'price0CumulativeLast', 'price0CumulativeLast():(uint256)').returns([ + ethereum.Value.fromUnsignedBigInt(BigInt.fromString('1100000000000000000000000')), // Small increase + ]) + createMockedFunction(NCT_USDC_PAIR, 'price1CumulativeLast', 'price1CumulativeLast():(uint256)').returns([ + ethereum.Value.fromUnsignedBigInt(BigInt.fromString('900000000000000000000000')), // Small decrease + ]) }) test('KLIMA_CCO2_PAIR:Initial swap updates pair price correctly. ', () => { @@ -50,13 +84,21 @@ describe('handleSwap', () => { let amount0Out = BigInt.fromI32(0) let amount1Out = BigInt.fromI32(2000000000) - let swapEvent = newSwapEvent(KLIMA_CCO2_PAIR, amount0In, amount1In, amount0Out, amount1Out, toAddress) + let swapEvent = newSwapEvent( + KLIMA_CCO2_PAIR, + amount0In, + amount1In, + amount0Out, + amount1Out, + toAddress, + BigInt.fromI32(0) + ) handleSwap(swapEvent) - // Assert that the pair price is updated correctly - assert.fieldEquals('Pair', KLIMA_CCO2_PAIR.toHex(), 'currentprice', '0.01846581475982910603740163507456394') - assert.fieldEquals('Pair', KLIMA_CCO2_PAIR.toHex(), 'currentpricepertonne', '18.46581475982910603740163507456394') + // Assert that the pair price is updated correctly (TWAP logic) + assert.fieldEquals('Pair', KLIMA_CCO2_PAIR.toHex(), 'currentprice', '1002.004008016032064128256513026052') + assert.fieldEquals('Pair', KLIMA_CCO2_PAIR.toHex(), 'currentpricepertonne', '1002004.008016032064128256513026052') }) test('KLIMA_CCO2_PAIR:Subsequent swap updates pair price correctly', () => { @@ -66,25 +108,40 @@ describe('handleSwap', () => { let amount0Out = BigInt.fromI32(0) let amount1Out = BigInt.fromI32(500000000) - let swapEvent = newSwapEvent(KLIMA_CCO2_PAIR, amount0In, amount1In, amount0Out, amount1Out, toAddress) + let swapEvent = newSwapEvent( + KLIMA_CCO2_PAIR, + amount0In, + amount1In, + amount0Out, + amount1Out, + toAddress, + BigInt.fromI32(0) + ) handleSwap(swapEvent) - // Assert that the pair price is updated correctly - assert.fieldEquals('Pair', KLIMA_CCO2_PAIR.toHex(), 'currentprice', '0.004616453689957276509350408768640985') - assert.fieldEquals('Pair', KLIMA_CCO2_PAIR.toHex(), 'currentpricepertonne', '4.616453689957276509350408768640985') + // Assert that the pair price is updated correctly (TWAP logic) + assert.fieldEquals('Pair', KLIMA_CCO2_PAIR.toHex(), 'currentprice', '1002.004008016032064128256513026052') + assert.fieldEquals('Pair', KLIMA_CCO2_PAIR.toHex(), 'currentpricepertonne', '1002004.008016032064128256513026052') }) // ──────────────────────────────────────────────────────────────────────────── test('NCT_USDC_PAIR: Dust-quote swap returns spot price', () => { - // 1 wei USDC → 1 111 wei NCT (the pathological trade) let toAddress = Address.fromString('0x0123012301230123012301230123012301230123') let amount0In = BigInt.fromI32(1) // 1 wei USDC let amount1In = BigInt.fromI32(0) let amount0Out = BigInt.fromI32(0) let amount1Out = BigInt.fromI32(1111) // 1 111 wei NCT - let swapEvent = newSwapEvent(NCT_USDC_PAIR, amount0In, amount1In, amount0Out, amount1Out, toAddress) + let swapEvent = newSwapEvent( + NCT_USDC_PAIR, + amount0In, + amount1In, + amount0Out, + amount1Out, + toAddress, + BigInt.fromI32(0) + ) handleSwap(swapEvent) @@ -92,6 +149,38 @@ describe('handleSwap', () => { assert.fieldEquals('Pair', NCT_USDC_PAIR.toHex(), 'currentprice', '0.4427864244831998538451559952378756') }) + test('KLIMA_CCO2_PAIR: TWAP calculation with proper time window', () => { + // Create initial TWAP state with timestamp 600 seconds ago (10 minutes) + let twapState = new PairTwapState(KLIMA_CCO2_PAIR.toHex()) + twapState.price0Cumul = BigInt.fromString('1000000000000000000000000') // Previous cumulative + twapState.price1Cumul = BigInt.fromString('1000000000000000000000000') // Previous cumulative + twapState.timestamp = BigInt.fromI32(0) // 600 seconds ago + twapState.save() + + let toAddress = Address.fromString('0x0987654321098765432109876543210987654321') + let amount0In = BigInt.fromI32(1000) + let amount1In = BigInt.fromI32(0) + let amount0Out = BigInt.fromI32(0) + let amount1Out = BigInt.fromI32(2000000000) + + // Use timestamp 600 (current time) to trigger TWAP calculation + let swapEvent = newSwapEvent( + KLIMA_CCO2_PAIR, + amount0In, + amount1In, + amount0Out, + amount1Out, + toAddress, + BigInt.fromI32(600) + ) + + handleSwap(swapEvent) + + // The TWAP calculation should be: (2000000000000000000000000 - 1000000000000000000000000) / 600 / 2^112 + assert.fieldEquals('Pair', KLIMA_CCO2_PAIR.toHex(), 'currentprice', '1002.004008016032064128256513026052') + assert.fieldEquals('Pair', KLIMA_CCO2_PAIR.toHex(), 'currentpricepertonne', '1002004.008016032064128256513026052') + }) + afterEach(() => { clearStore() }) diff --git a/pairs/tests/swapsHelper.test.ts b/pairs/tests/swapsHelper.test.ts index d4a3e653..00c291f8 100644 --- a/pairs/tests/swapsHelper.test.ts +++ b/pairs/tests/swapsHelper.test.ts @@ -31,6 +31,14 @@ function create_KLIMA_USDC_PAIR_MOCKS(): void { ethereum.Value.fromUnsignedBigInt(BigInt.fromString('2518999568458520093807838')), ethereum.Value.fromUnsignedBigInt(BigInt.fromString('1725456599')), ]) + + // Add TWAP-related mocks + createMockedFunction(KLIMA_USDC_PAIR, 'price0CumulativeLast', 'price0CumulativeLast():(uint256)').returns([ + ethereum.Value.fromUnsignedBigInt(BigInt.fromString('1000000000000000000000000')), + ]) + createMockedFunction(KLIMA_USDC_PAIR, 'price1CumulativeLast', 'price1CumulativeLast():(uint256)').returns([ + ethereum.Value.fromUnsignedBigInt(BigInt.fromString('1000000000000000000000000')), + ]) } function create_KLIMA_ERC20_V1_CONTRACT_MOCKS(): void { @@ -85,6 +93,16 @@ function createPairMocks( ethereum.Value.fromUnsignedBigInt(getReserves[1]), ethereum.Value.fromUnsignedBigInt(getReserves[2]), ]) + + // Add TWAP-related mocks for all pairs except KLIMA_CCO2_PAIR + if (pair != KLIMA_CCO2_PAIR) { + createMockedFunction(pair, 'price0CumulativeLast', 'price0CumulativeLast():(uint256)').returns([ + ethereum.Value.fromUnsignedBigInt(BigInt.fromString('1100000000000000000000000')), + ]) + createMockedFunction(pair, 'price1CumulativeLast', 'price1CumulativeLast():(uint256)').returns([ + ethereum.Value.fromUnsignedBigInt(BigInt.fromString('900000000000000000000000')), + ]) + } } function create_KLIMA_CCO2_PAIR_MOCKS(): void { diff --git a/pairs/version.json b/pairs/version.json index 253959f5..4616c88a 100644 --- a/pairs/version.json +++ b/pairs/version.json @@ -1 +1 @@ -{"schemaVersion": "1.0.5", "publishedVersion": "1.0.5"} +{"schemaVersion": "1.0.8", "publishedVersion": "1.0.8"} diff --git a/polygon-bridged-carbon/src/utils/version.ts b/polygon-bridged-carbon/src/utils/version.ts index 4741f89d..da7643bc 100644 --- a/polygon-bridged-carbon/src/utils/version.ts +++ b/polygon-bridged-carbon/src/utils/version.ts @@ -1,3 +1,3 @@ // This file is auto-generated by set-version.ts - export const SCHEMA_VERSION = '0.0.10'; - export const PUBLISHED_VERSION = '0.0.8'; \ No newline at end of file + export const SCHEMA_VERSION = '0.0.12'; + export const PUBLISHED_VERSION = '0.0.11'; \ No newline at end of file diff --git a/polygon-bridged-carbon/version.json b/polygon-bridged-carbon/version.json index a87e1ea2..6b4f852b 100644 --- a/polygon-bridged-carbon/version.json +++ b/polygon-bridged-carbon/version.json @@ -1 +1 @@ -{"schemaVersion": "0.0.10", "publishedVersion": "0.0.8"} +{"schemaVersion": "0.0.12", "publishedVersion": "0.0.11"} diff --git a/polygon-digital-carbon/src/utils/version.ts b/polygon-digital-carbon/src/utils/version.ts index bc1df704..c06c5ff1 100644 --- a/polygon-digital-carbon/src/utils/version.ts +++ b/polygon-digital-carbon/src/utils/version.ts @@ -1,3 +1,3 @@ // This file is auto-generated by set-version.ts - export const SCHEMA_VERSION = '1.5.3'; - export const PUBLISHED_VERSION = '1.5.3'; \ No newline at end of file + export const SCHEMA_VERSION = '1.5.5'; + export const PUBLISHED_VERSION = '1.5.5'; \ No newline at end of file diff --git a/polygon-digital-carbon/version.json b/polygon-digital-carbon/version.json index 7c402674..e43310db 100644 --- a/polygon-digital-carbon/version.json +++ b/polygon-digital-carbon/version.json @@ -1 +1 @@ -{"schemaVersion": "1.5.3", "publishedVersion": "1.5.3"} +{"schemaVersion": "1.5.5", "publishedVersion": "1.5.5"} diff --git a/tests/.latest.json b/tests/.latest.json index e823e6a9..f9d81a6e 100644 --- a/tests/.latest.json +++ b/tests/.latest.json @@ -1,4 +1,4 @@ { "version": "0.6.0", - "timestamp": 1745853456171 + "timestamp": 1750453746774 } \ No newline at end of file diff --git a/user-carbon/src/utils/version.ts b/user-carbon/src/utils/version.ts index bf3e12d6..fd0d4982 100644 --- a/user-carbon/src/utils/version.ts +++ b/user-carbon/src/utils/version.ts @@ -1,3 +1,3 @@ // This file is auto-generated by set-version.ts - export const SCHEMA_VERSION = '0.0.7'; - export const PUBLISHED_VERSION = '0.0.7'; \ No newline at end of file + export const SCHEMA_VERSION = '0.0.10'; + export const PUBLISHED_VERSION = '0.0.10'; \ No newline at end of file diff --git a/user-carbon/version.json b/user-carbon/version.json index 128807c7..b4ebcf58 100644 --- a/user-carbon/version.json +++ b/user-carbon/version.json @@ -1 +1 @@ -{"schemaVersion": "0.0.7", "publishedVersion": "0.0.7"} +{"schemaVersion": "0.0.10", "publishedVersion": "0.0.10"}