diff --git a/generated/schema.ts b/generated/schema.ts index 1387ec0..b2cd9a1 100644 --- a/generated/schema.ts +++ b/generated/schema.ts @@ -1131,9 +1131,9 @@ export class MarketState extends Entity { } export class Position extends Entity { - constructor(id: Bytes) { + constructor(id: string) { super(); - this.set("id", Value.fromBytes(id)); + this.set("id", Value.fromString(id)); } save(): void { @@ -1141,34 +1141,32 @@ export class Position extends Entity { assert(id != null, "Cannot save Position entity without an ID"); if (id) { assert( - id.kind == ValueKind.BYTES, - `Entities of type Position must have an ID of type Bytes but the id '${id.displayData()}' is of type ${id.displayKind()}`, + id.kind == ValueKind.STRING, + `Entities of type Position must have an ID of type String but the id '${id.displayData()}' is of type ${id.displayKind()}`, ); - store.set("Position", id.toBytes().toHexString(), this); + store.set("Position", id.toString(), this); } } - static loadInBlock(id: Bytes): Position | null { - return changetype( - store.get_in_block("Position", id.toHexString()), - ); + static loadInBlock(id: string): Position | null { + return changetype(store.get_in_block("Position", id)); } - static load(id: Bytes): Position | null { - return changetype(store.get("Position", id.toHexString())); + static load(id: string): Position | null { + return changetype(store.get("Position", id)); } - get id(): Bytes { + get id(): string { let value = this.get("id"); if (!value || value.kind == ValueKind.NULL) { throw new Error("Cannot return null for a required field."); } else { - return value.toBytes(); + return value.toString(); } } - set id(value: Bytes) { - this.set("id", Value.fromBytes(value)); + set id(value: string) { + this.set("id", Value.fromString(value)); } get positionId(): string { @@ -1406,27 +1404,19 @@ export class Position extends Entity { } get builds(): BuildLoader { - return new BuildLoader( - "Position", - this.get("id")!.toBytes().toHexString(), - "builds", - ); + return new BuildLoader("Position", this.get("id")!.toString(), "builds"); } get liquidates(): LiquidateLoader { return new LiquidateLoader( "Position", - this.get("id")!.toBytes().toHexString(), + this.get("id")!.toString(), "liquidates", ); } get unwinds(): UnwindLoader { - return new UnwindLoader( - "Position", - this.get("id")!.toBytes().toHexString(), - "unwinds", - ); + return new UnwindLoader("Position", this.get("id")!.toString(), "unwinds"); } } @@ -1559,9 +1549,9 @@ export class Transaction extends Entity { } export class Build extends Entity { - constructor(id: Bytes) { + constructor(id: string) { super(); - this.set("id", Value.fromBytes(id)); + this.set("id", Value.fromString(id)); } save(): void { @@ -1569,34 +1559,32 @@ export class Build extends Entity { assert(id != null, "Cannot save Build entity without an ID"); if (id) { assert( - id.kind == ValueKind.BYTES, - `Entities of type Build must have an ID of type Bytes but the id '${id.displayData()}' is of type ${id.displayKind()}`, + id.kind == ValueKind.STRING, + `Entities of type Build must have an ID of type String but the id '${id.displayData()}' is of type ${id.displayKind()}`, ); - store.set("Build", id.toBytes().toHexString(), this); + store.set("Build", id.toString(), this); } } - static loadInBlock(id: Bytes): Build | null { - return changetype( - store.get_in_block("Build", id.toHexString()), - ); + static loadInBlock(id: string): Build | null { + return changetype(store.get_in_block("Build", id)); } - static load(id: Bytes): Build | null { - return changetype(store.get("Build", id.toHexString())); + static load(id: string): Build | null { + return changetype(store.get("Build", id)); } - get id(): Bytes { + get id(): string { let value = this.get("id"); if (!value || value.kind == ValueKind.NULL) { throw new Error("Cannot return null for a required field."); } else { - return value.toBytes(); + return value.toString(); } } - set id(value: Bytes) { - this.set("id", Value.fromBytes(value)); + set id(value: string) { + this.set("id", Value.fromString(value)); } get owner(): Bytes { @@ -1612,56 +1600,17 @@ export class Build extends Entity { this.set("owner", Value.fromBytes(value)); } - get position(): Bytes { + get position(): string { let value = this.get("position"); if (!value || value.kind == ValueKind.NULL) { throw new Error("Cannot return null for a required field."); } else { - return value.toBytes(); - } - } - - set position(value: Bytes) { - this.set("position", Value.fromBytes(value)); - } - - get currentOi(): BigInt { - let value = this.get("currentOi"); - if (!value || value.kind == ValueKind.NULL) { - throw new Error("Cannot return null for a required field."); - } else { - return value.toBigInt(); - } - } - - set currentOi(value: BigInt) { - this.set("currentOi", Value.fromBigInt(value)); - } - - get currentDebt(): BigInt { - let value = this.get("currentDebt"); - if (!value || value.kind == ValueKind.NULL) { - throw new Error("Cannot return null for a required field."); - } else { - return value.toBigInt(); - } - } - - set currentDebt(value: BigInt) { - this.set("currentDebt", Value.fromBigInt(value)); - } - - get isLong(): boolean { - let value = this.get("isLong"); - if (!value || value.kind == ValueKind.NULL) { - return false; - } else { - return value.toBoolean(); + return value.toString(); } } - set isLong(value: boolean) { - this.set("isLong", Value.fromBoolean(value)); + set position(value: string) { + this.set("position", Value.fromString(value)); } get price(): BigInt { @@ -1690,32 +1639,6 @@ export class Build extends Entity { this.set("feeAmount", Value.fromBigInt(value)); } - get collateral(): BigInt { - let value = this.get("collateral"); - if (!value || value.kind == ValueKind.NULL) { - throw new Error("Cannot return null for a required field."); - } else { - return value.toBigInt(); - } - } - - set collateral(value: BigInt) { - this.set("collateral", Value.fromBigInt(value)); - } - - get value(): BigInt { - let value = this.get("value"); - if (!value || value.kind == ValueKind.NULL) { - throw new Error("Cannot return null for a required field."); - } else { - return value.toBigInt(); - } - } - - set value(value: BigInt) { - this.set("value", Value.fromBigInt(value)); - } - get timestamp(): BigInt { let value = this.get("timestamp"); if (!value || value.kind == ValueKind.NULL) { @@ -1744,9 +1667,9 @@ export class Build extends Entity { } export class Unwind extends Entity { - constructor(id: Bytes) { + constructor(id: string) { super(); - this.set("id", Value.fromBytes(id)); + this.set("id", Value.fromString(id)); } save(): void { @@ -1754,34 +1677,32 @@ export class Unwind extends Entity { assert(id != null, "Cannot save Unwind entity without an ID"); if (id) { assert( - id.kind == ValueKind.BYTES, - `Entities of type Unwind must have an ID of type Bytes but the id '${id.displayData()}' is of type ${id.displayKind()}`, + id.kind == ValueKind.STRING, + `Entities of type Unwind must have an ID of type String but the id '${id.displayData()}' is of type ${id.displayKind()}`, ); - store.set("Unwind", id.toBytes().toHexString(), this); + store.set("Unwind", id.toString(), this); } } - static loadInBlock(id: Bytes): Unwind | null { - return changetype( - store.get_in_block("Unwind", id.toHexString()), - ); + static loadInBlock(id: string): Unwind | null { + return changetype(store.get_in_block("Unwind", id)); } - static load(id: Bytes): Unwind | null { - return changetype(store.get("Unwind", id.toHexString())); + static load(id: string): Unwind | null { + return changetype(store.get("Unwind", id)); } - get id(): Bytes { + get id(): string { let value = this.get("id"); if (!value || value.kind == ValueKind.NULL) { throw new Error("Cannot return null for a required field."); } else { - return value.toBytes(); + return value.toString(); } } - set id(value: Bytes) { - this.set("id", Value.fromBytes(value)); + set id(value: string) { + this.set("id", Value.fromString(value)); } get owner(): Bytes { @@ -1797,17 +1718,17 @@ export class Unwind extends Entity { this.set("owner", Value.fromBytes(value)); } - get position(): Bytes { + get position(): string { let value = this.get("position"); if (!value || value.kind == ValueKind.NULL) { throw new Error("Cannot return null for a required field."); } else { - return value.toBytes(); + return value.toString(); } } - set position(value: Bytes) { - this.set("position", Value.fromBytes(value)); + set position(value: string) { + this.set("position", Value.fromString(value)); } get unwindNumber(): BigInt { @@ -1823,21 +1744,8 @@ export class Unwind extends Entity { this.set("unwindNumber", Value.fromBigInt(value)); } - get currentOi(): BigInt { - let value = this.get("currentOi"); - if (!value || value.kind == ValueKind.NULL) { - throw new Error("Cannot return null for a required field."); - } else { - return value.toBigInt(); - } - } - - set currentOi(value: BigInt) { - this.set("currentOi", Value.fromBigInt(value)); - } - - get currentDebt(): BigInt { - let value = this.get("currentDebt"); + get oiUnwound(): BigInt { + let value = this.get("oiUnwound"); if (!value || value.kind == ValueKind.NULL) { throw new Error("Cannot return null for a required field."); } else { @@ -1845,21 +1753,8 @@ export class Unwind extends Entity { } } - set currentDebt(value: BigInt) { - this.set("currentDebt", Value.fromBigInt(value)); - } - - get isLong(): boolean { - let value = this.get("isLong"); - if (!value || value.kind == ValueKind.NULL) { - return false; - } else { - return value.toBoolean(); - } - } - - set isLong(value: boolean) { - this.set("isLong", Value.fromBoolean(value)); + set oiUnwound(value: BigInt) { + this.set("oiUnwound", Value.fromBigInt(value)); } get price(): BigInt { @@ -1992,32 +1887,6 @@ export class Unwind extends Entity { this.set("mint", Value.fromBigInt(value)); } - get collateral(): BigInt { - let value = this.get("collateral"); - if (!value || value.kind == ValueKind.NULL) { - throw new Error("Cannot return null for a required field."); - } else { - return value.toBigInt(); - } - } - - set collateral(value: BigInt) { - this.set("collateral", Value.fromBigInt(value)); - } - - get value(): BigInt { - let value = this.get("value"); - if (!value || value.kind == ValueKind.NULL) { - throw new Error("Cannot return null for a required field."); - } else { - return value.toBigInt(); - } - } - - set value(value: BigInt) { - this.set("value", Value.fromBigInt(value)); - } - get timestamp(): BigInt { let value = this.get("timestamp"); if (!value || value.kind == ValueKind.NULL) { @@ -2046,9 +1915,9 @@ export class Unwind extends Entity { } export class Liquidate extends Entity { - constructor(id: Bytes) { + constructor(id: string) { super(); - this.set("id", Value.fromBytes(id)); + this.set("id", Value.fromString(id)); } save(): void { @@ -2056,36 +1925,32 @@ export class Liquidate extends Entity { assert(id != null, "Cannot save Liquidate entity without an ID"); if (id) { assert( - id.kind == ValueKind.BYTES, - `Entities of type Liquidate must have an ID of type Bytes but the id '${id.displayData()}' is of type ${id.displayKind()}`, + id.kind == ValueKind.STRING, + `Entities of type Liquidate must have an ID of type String but the id '${id.displayData()}' is of type ${id.displayKind()}`, ); - store.set("Liquidate", id.toBytes().toHexString(), this); + store.set("Liquidate", id.toString(), this); } } - static loadInBlock(id: Bytes): Liquidate | null { - return changetype( - store.get_in_block("Liquidate", id.toHexString()), - ); + static loadInBlock(id: string): Liquidate | null { + return changetype(store.get_in_block("Liquidate", id)); } - static load(id: Bytes): Liquidate | null { - return changetype( - store.get("Liquidate", id.toHexString()), - ); + static load(id: string): Liquidate | null { + return changetype(store.get("Liquidate", id)); } - get id(): Bytes { + get id(): string { let value = this.get("id"); if (!value || value.kind == ValueKind.NULL) { throw new Error("Cannot return null for a required field."); } else { - return value.toBytes(); + return value.toString(); } } - set id(value: Bytes) { - this.set("id", Value.fromBytes(value)); + set id(value: string) { + this.set("id", Value.fromString(value)); } get owner(): Bytes { @@ -2114,17 +1979,17 @@ export class Liquidate extends Entity { this.set("sender", Value.fromBytes(value)); } - get position(): Bytes { + get position(): string { let value = this.get("position"); if (!value || value.kind == ValueKind.NULL) { throw new Error("Cannot return null for a required field."); } else { - return value.toBytes(); + return value.toString(); } } - set position(value: Bytes) { - this.set("position", Value.fromBytes(value)); + set position(value: string) { + this.set("position", Value.fromString(value)); } get fractionOfPosition(): BigInt { @@ -2166,45 +2031,6 @@ export class Liquidate extends Entity { this.set("size", Value.fromBigInt(value)); } - get currentOi(): BigInt { - let value = this.get("currentOi"); - if (!value || value.kind == ValueKind.NULL) { - throw new Error("Cannot return null for a required field."); - } else { - return value.toBigInt(); - } - } - - set currentOi(value: BigInt) { - this.set("currentOi", Value.fromBigInt(value)); - } - - get currentDebt(): BigInt { - let value = this.get("currentDebt"); - if (!value || value.kind == ValueKind.NULL) { - throw new Error("Cannot return null for a required field."); - } else { - return value.toBigInt(); - } - } - - set currentDebt(value: BigInt) { - this.set("currentDebt", Value.fromBigInt(value)); - } - - get isLong(): boolean { - let value = this.get("isLong"); - if (!value || value.kind == ValueKind.NULL) { - return false; - } else { - return value.toBoolean(); - } - } - - set isLong(value: boolean) { - this.set("isLong", Value.fromBoolean(value)); - } - get price(): BigInt { let value = this.get("price"); if (!value || value.kind == ValueKind.NULL) { @@ -2231,32 +2057,6 @@ export class Liquidate extends Entity { this.set("mint", Value.fromBigInt(value)); } - get collateral(): BigInt { - let value = this.get("collateral"); - if (!value || value.kind == ValueKind.NULL) { - throw new Error("Cannot return null for a required field."); - } else { - return value.toBigInt(); - } - } - - set collateral(value: BigInt) { - this.set("collateral", Value.fromBigInt(value)); - } - - get value(): BigInt { - let value = this.get("value"); - if (!value || value.kind == ValueKind.NULL) { - throw new Error("Cannot return null for a required field."); - } else { - return value.toBigInt(); - } - } - - set value(value: BigInt) { - this.set("value", Value.fromBigInt(value)); - } - get timestamp(): BigInt { let value = this.get("timestamp"); if (!value || value.kind == ValueKind.NULL) { diff --git a/schema.graphql b/schema.graphql index 8ab7397..1d62f8a 100644 --- a/schema.graphql +++ b/schema.graphql @@ -143,7 +143,7 @@ type MarketState @entity { type Position @entity { # market contract concat'd with position id - id: Bytes! + id: ID! # position id positionId: String! # owner of position @@ -206,25 +206,15 @@ type Transaction @entity { type Build @entity { # market contract concat'd with position id - id: Bytes! + id: ID! # account performing action owner: Account! # position entitie position: Position! - # current position oi - currentOi: BigInt! - # current position debt - currentDebt: BigInt! - # position side taken - isLong: Boolean! # price at time of build price: BigInt! # Fee amount of this build feeAmount: BigInt! - # current collateral backing position - collateral: BigInt! - # current value of position - value: BigInt! # timestamp of transaction timestamp: BigInt! # transaction the position was built in @@ -233,19 +223,15 @@ type Build @entity { type Unwind @entity { # market contract concat'd with position id concat'd with unwind number - id: Bytes! + id: ID! # account performing action owner: Account! # position id position: Position! # unwind number unwindNumber: BigInt! - # current position oi - currentOi: BigInt! - # current position debt - currentDebt: BigInt! - # position side taken - isLong: Boolean! + # amount of oi unwound + oiUnwound: BigInt! # price at time of unwind price: BigInt! # fraction of unwind @@ -266,10 +252,6 @@ type Unwind @entity { volume: BigInt! # total ovl minted/burned in this unwind mint: BigInt! - # current collateral backing position - collateral: BigInt! - # current value of position - value: BigInt! # timestamp of transaction timestamp: BigInt! # transaction the position was unwinded in @@ -278,7 +260,7 @@ type Unwind @entity { type Liquidate @entity { # market contract concat'd with position id - id: Bytes! + id: ID! # owner of the position owner: Account! # address performing the transaction @@ -291,20 +273,10 @@ type Liquidate @entity { fundingPayment: BigInt! # liquidate size = initialCollateral * fractionOfPosition size: BigInt! - # current position oi - currentOi: BigInt! - # current position debt - currentDebt: BigInt! - # position side taken - isLong: Boolean! # price at time of liquidate price: BigInt! # total ovl minted/burned in this transaction mint: BigInt! - # current collateral backing position - collateral: BigInt! - # current value of position - value: BigInt! # timestamp of transaction timestamp: BigInt! # transaction the position was liquidated in diff --git a/src/__test__/unit/market.test.ts b/src/__test__/unit/market.test.ts index 963a31c..51f4f98 100644 --- a/src/__test__/unit/market.test.ts +++ b/src/__test__/unit/market.test.ts @@ -98,13 +98,10 @@ describe("Market events", () => { }) test("creates Build entity with correct attributes", () => { - const marketId = market.concatI32(positionId.toI32()).toHexString() + const marketId = market.toHexString().concat('-').concat(positionId.toHexString()) assert.entityCount("Build", 1) assert.fieldEquals("Build", marketId, "owner", sender.toHexString()) - assert.fieldEquals("Build", marketId, "currentOi", oi.toString()) - assert.fieldEquals("Build", marketId, "currentDebt", debt.toString()) - assert.fieldEquals("Build", marketId, "isLong", isLong.toString()) assert.fieldEquals("Build", marketId, "price", price.toString()) }) @@ -245,13 +242,11 @@ describe("Market events", () => { }) test("creates Unwind entity with correct attributes", () => { - const unwindId = market.concatI32(positionId.toI32()).concatI32(0).toHexString() + const unwindId = market.toHexString().concat('-').concat(positionId.toHexString()).concat('-').concat('0') assert.entityCount("Unwind", 1) assert.fieldEquals("Unwind", unwindId, "owner", sender.toHexString()) - assert.fieldEquals("Unwind", unwindId, "currentOi", oi.toString()) - assert.fieldEquals("Unwind", unwindId, "currentDebt", debt.toString()) - assert.fieldEquals("Unwind", unwindId, "isLong", isLong.toString()) + assert.fieldEquals("Unwind", unwindId, "oiUnwound", oi.minus(oiAfterUnwind).toString()) assert.fieldEquals("Unwind", unwindId, "price", price.toString()) }) }) @@ -272,14 +267,11 @@ describe("Market events", () => { }) test("creates Liquidate entity with correct attributes", () => { - const liquidateId = market.concatI32(positionId.toI32()).toHexString(); + const liquidateId = market.toHexString().concat('-').concat(positionId.toHexString()) assert.entityCount("Liquidate", 1) assert.fieldEquals("Liquidate", liquidateId, "owner", sender.toHexString()) assert.fieldEquals("Liquidate", liquidateId, "sender", zeroAddress.toHexString()) - assert.fieldEquals("Liquidate", liquidateId, "currentOi", oi.toString()) - assert.fieldEquals("Liquidate", liquidateId, "currentDebt", debt.toString()) - assert.fieldEquals("Liquidate", liquidateId, "isLong", isLong.toString()) assert.fieldEquals("Liquidate", liquidateId, "price", price.toString()) }) }) @@ -300,29 +292,24 @@ describe("Market events", () => { }) test("creates an Unwound and withdraws Position collateral", () => { - const _positionId = market.concatI32(positionId.toI32()) - const unwindId = _positionId.concatI32(0).toHexString() + const _positionId = market.toHexString().concat('-').concat(positionId.toHexString()) + const unwindId = _positionId.concat('-0') assert.entityCount("Unwind", 1) - assert.fieldEquals("Unwind", unwindId, "position", _positionId.toHexString()) + assert.fieldEquals("Unwind", unwindId, "position", _positionId) assert.fieldEquals("Unwind", unwindId, "owner", sender.toHexString()) assert.fieldEquals("Unwind", unwindId, "size", collateral.toString()) assert.fieldEquals("Unwind", unwindId, "transferAmount", collateral.toString()) - assert.fieldEquals("Unwind", unwindId, "currentOi", oi.toString()) - assert.fieldEquals("Unwind", unwindId, "currentDebt", debt.toString()) - assert.fieldEquals("Unwind", unwindId, "isLong", isLong.toString()) + assert.fieldEquals("Unwind", unwindId, "oiUnwound", oi.toString()) assert.fieldEquals("Unwind", unwindId, "price", "0") assert.fieldEquals("Unwind", unwindId, "fraction", ONE_18DEC_BI.toString()) assert.fieldEquals("Unwind", unwindId, "fractionOfPosition", ONE_18DEC_BI.toString()) - assert.fieldEquals("Unwind", unwindId, "volume", "0") assert.fieldEquals("Unwind", unwindId, "mint", "0") assert.fieldEquals("Unwind", unwindId, "fundingPayment", "0") - assert.fieldEquals("Unwind", unwindId, "collateral", "0") - assert.fieldEquals("Unwind", unwindId, "value", "0") - assert.fieldEquals("Position", _positionId.toHexString(), "currentOi", "0") - assert.fieldEquals("Position", _positionId.toHexString(), "currentDebt", "0") - assert.fieldEquals("Position", _positionId.toHexString(), "fractionUnwound", ONE_18DEC_BI.toString()) + assert.fieldEquals("Position", _positionId, "currentOi", "0") + assert.fieldEquals("Position", _positionId, "currentDebt", "0") + assert.fieldEquals("Position", _positionId, "fractionUnwound", ONE_18DEC_BI.toString()) }) }) diff --git a/src/mapping.ts b/src/mapping.ts index d5f4bb8..08fb025 100644 --- a/src/mapping.ts +++ b/src/mapping.ts @@ -33,19 +33,23 @@ export function handleMarketDeployed(event: MarketDeployed): void { // load factory let factory = loadFactory(Address.fromString(FACTORY_ADDRESS)) + // adding a new market to the count factory.marketCount = factory.marketCount.plus(ONE_BI) + // vars that will be used to populate the information for a new market let marketAddress = event.params.market let feedAddress = event.params.feed let marketContract = OverlayV1Market.bind(event.params.market) let market = new Market(marketAddress) as Market let marketState = updateMarketState(market.id) + // basic info about the market market.feedAddress = feedAddress.toHexString() market.factory = factory.id market.createdAtTimestamp = event.block.timestamp market.createdAtBlockNumber = event.block.number + // all params market.k = marketContract.params(integer.fromNumber(0)) market.lmbda = marketContract.params(integer.fromNumber(1)) market.delta = marketContract.params(integer.fromNumber(2)) @@ -84,74 +88,121 @@ export function handleMarketDeployed(event: MarketDeployed): void { } export function handleBuild(event: BuildEvent): void { + // Load the market entity using the market address from the event let market = loadMarket(event, event.address) - let marketState = updateMarketState(market.id) + // Load the account entity corresponding to the sender of the transaction let sender = loadAccount(event.params.sender) + // Convert market ID and sender ID to Address type for further usage let marketAddress = Address.fromBytes(market.id) let senderAddress = Address.fromBytes(sender.id) + // Retrieve the position ID from the event and create a unique ID for the position entity let positionId = event.params.positionId - let id = market.id.concatI32(positionId.toI32()) + let id = market.id.toHexString().concat('-').concat(positionId.toHexString()) let position = new Position(id) as Position + // Initialize transfer fee amount to zero let transferFeeAmount = ZERO_BI + // How much OVL user sent to the market (initialCollateral + transferFee) + let userTransferAmount = ZERO_BI + + // Retrieve the transaction receipt and load the factory entity associated with the market let receipt = event.receipt let factory = Factory.load(market.factory) + // This block of code is to get `transferFeeAmount` from the logs + // Build transactions generate 3 (sometimes 4) relevant logs: + // 1. Build: The main event log indicating the build action. + // 2. (Optional) Approve: This is emitted only for the first Build in the market. + // 3. Transfer from user to smart contract: The transfer of collateral from the user to the market contract. + // 4. Transfer from smart contract to fee recipient: The transfer of the fee amount from the market contract to the fee recipient. + + // All code below is to find the second Transfer function (from the smart contract to the fee recipient) and extract the `transferFeeAmount` value. + if (receipt && factory) { - const logLength = receipt.logs.length - log.warning("handleBuild: START: tx: {}, transactionLogIndex: {}, logIndex: {}, transaction.index: {}, logs length: {}, logs[0].logIndex: {}, logs[0].transactionIndex: {}, logs[0].transactionLogIndex: {}", [ - event.transaction.hash.toHexString(), - event.transactionLogIndex.toI32().toString(), - event.logIndex.toI32().toString(), - event.transaction.index.toI32().toString(), - logLength.toString(), - receipt.logs[0].logIndex.toI32().toString(), - receipt.logs[0].transactionIndex.toI32().toString(), - receipt.logs[0].transactionLogIndex.toI32().toString(), - ]) + // Initialize the index for the Build event log let buildIndex = 0 + + // Iterate over the logs to find the log corresponding to the Build event for (let i = 0; i < receipt.logs.length; i++) { if (receipt.logs[i].logIndex.toI32() === event.logIndex.toI32()) { buildIndex = i break } } + + // Check if the next log is not an ERC20 transfer event, adjust the index if necessary + // The first Build event on each market emits an Approve event after the Build event, + // which might shift the position of the subsequent Transfer logs. if (receipt.logs[buildIndex + 1].topics[0].notEqual(TRANSFER_SIG)) { buildIndex += 1 } + + // Calculate the index for the transfer event log from the smart contract to the fee recipient let index = +buildIndex + 2 - log.warning("handleBuild: indexes: tx {}, buildIndex: {}, index: {}", [ - event.transaction.hash.toHexString(), - buildIndex.toString(), - index.toString() - ]) + + // Further adjust the index if the next log is not an ERC20 transfer event + // This adjustment is a precautionary step, though it is rare for this to happen. if (receipt.logs[buildIndex + 1].topics[0].notEqual(TRANSFER_SIG)) { + log.warning("IT HAPPENS", []) index += 1 } - const _topic0 = receipt.logs[index].topics[0] - const _address = receipt.logs[index].address - // find the log that matches the ERC20 transfer to the owner + + // Extract the first topic (event signature) and the address of the log at the calculated index + const _topic0Fee = receipt.logs[index].topics[0] + const _addressFee = receipt.logs[index].address + + // Verify that the log is an ERC20 Transfer event and matches the OVL token address if ( - _topic0.equals(TRANSFER_SIG) && - _address.toHexString() == OVL_ADDRESS && + _topic0Fee.equals(TRANSFER_SIG) && + _addressFee.toHexString() == OVL_ADDRESS && receipt.logs[index].topics.length > 1 ) { + // Decode the recipient address from the log's topics let topics2address = ethereum.decode('address', receipt.logs[index].topics[2])!.toAddress() - log.warning("handleBuild: 1 stage receipt found: tx: {}, 2address: {}, feeRecipient: {}", [ - event.transaction.hash.toHexString(), - topics2address.toHexString(), - factory.feeRecipient.toLowerCase() - ]) + + // Check if the recipient is the fee recipient and decode the transfer amount if (topics2address.toHexString().toLowerCase() == factory.feeRecipient.toLowerCase()) { const _transferAmount = receipt.logs[index].data transferFeeAmount = ethereum.decode('uin256', _transferAmount)!.toBigInt() - log.warning("handleBuild: 2 stage receipt found: tx{}, transferFeeAmount: {}", [ + } else { + // Log an error if the recipient does not match the expected fee recipient + log.error("handleBuild: 2nd if: transaction: {}, buildIndex: {}, index {}", [ event.transaction.hash.toHexString(), - transferFeeAmount.toHexString() + buildIndex.toString(), + index.toString() ]) + } + } else { + // Log an error if the log does not match the expected ERC20 transfer event + log.error("handleBuild: 1st if: transaction: {}, buildIndex: {}, index {}", [ + event.transaction.hash.toHexString(), + buildIndex.toString(), + index.toString() + ]) + } + + index-- + + // Extract the first topic (event signature) and the address of the log at the calculated index + const _topic0Main = receipt.logs[index].topics[0] + const _addressMain = receipt.logs[index].address + + if ( + _topic0Main.equals(TRANSFER_SIG) && + _addressMain.toHexString() == OVL_ADDRESS && + receipt.logs[index].topics.length > 1 + ) { + // Decode the recipient address from the log's topics + let topics2address = ethereum.decode('address', receipt.logs[index].topics[2])!.toAddress() + + // Check if the recipient is the fee recipient and decode the transfer amount + if (topics2address.toHexString().toLowerCase() == market.id.toHexString().toLowerCase()) { + const _transferAmount = receipt.logs[index].data + userTransferAmount = ethereum.decode('uin256', _transferAmount)!.toBigInt() } else { + // Log an error if the recipient does not match the expected fee recipient log.error("handleBuild: 2nd if: transaction: {}, buildIndex: {}, index {}", [ event.transaction.hash.toHexString(), buildIndex.toString(), @@ -159,6 +210,7 @@ export function handleBuild(event: BuildEvent): void { ]) } } else { + // Log an error if the log does not match the expected ERC20 transfer event log.error("handleBuild: 1st if: transaction: {}, buildIndex: {}, index {}", [ event.transaction.hash.toHexString(), buildIndex.toString(), @@ -166,6 +218,7 @@ export function handleBuild(event: BuildEvent): void { ]) } } else { + // Log an error if the receipt or factory is missing log.error("handleBuild: receipt && factory: tx {}, {}, {}", [ event.transaction.hash.toHexString(), !receipt ? "no receipt" : "receipt found", @@ -173,16 +226,22 @@ export function handleBuild(event: BuildEvent): void { ]) } + // Initialize the Position entity with data from the event and calculations position.owner = sender.id position.positionId = positionId.toHexString() position.market = market.id position.initialOi = event.params.oi position.initialDebt = event.params.debt - let initialCollateral = stateContract.cost(marketAddress, senderAddress, positionId) + // Calculate initial collateral and notional using the state contract + let initialCollateral = userTransferAmount.minus(transferFeeAmount) + if (initialCollateral.equals(new BigInt(0))) { + initialCollateral = stateContract.cost(marketAddress, senderAddress, positionId) + } let initialNotional = initialCollateral.plus(event.params.debt) position.initialCollateral = initialCollateral position.initialNotional = initialNotional + // Calculate the leverage for the position position.leverage = (initialNotional.toBigDecimal()).div(initialCollateral.toBigDecimal()) position.isLong = event.params.isLong position.entryPrice = event.params.price @@ -195,6 +254,7 @@ export function handleBuild(event: BuildEvent): void { position.numberOfUniwnds = BigInt.fromI32(0) position.fractionUnwound = BigInt.fromI32(0) + // Update the open interest in the market based on the position's side (long/short) if (position.isLong) { market.oiLong = event.params.oiAfterBuild market.oiLongShares = event.params.oiSharesAfterBuild @@ -202,27 +262,26 @@ export function handleBuild(event: BuildEvent): void { market.oiShort = event.params.oiAfterBuild market.oiShortShares = event.params.oiSharesAfterBuild } + + // Update market-level metrics with the calculated fee and notional market.totalBuildFees = market.totalBuildFees.plus(transferFeeAmount) market.numberOfBuilds = market.numberOfBuilds.plus(ONE_BI) market.totalFees = market.totalFees.plus(transferFeeAmount) market.totalVolume = market.totalVolume.plus(initialNotional) + // Load or create the Transaction entity for this event let transaction = loadTransaction(event) - let build = new Build(position.id) as Build + // Create a new Build entity to track this specific build event + let build = new Build(position.id) as Build build.position = position.id build.owner = sender.id - build.currentOi = event.params.oi - build.currentDebt = event.params.debt - build.isLong = event.params.isLong build.price = event.params.price - build.collateral = initialCollateral - build.value = stateContract.value(marketAddress, senderAddress, positionId) build.timestamp = transaction.timestamp build.transaction = transaction.id build.feeAmount = transferFeeAmount - // analytics update + // Update the analytics entity to reflect the new build and market activity let analytics = loadAnalytics(market.factory) if (sender.ovlVolumeTraded.equals(ZERO_BI)) { analytics.totalUsers = analytics.totalUsers.plus(ONE_BI) @@ -232,16 +291,23 @@ export function handleBuild(event: BuildEvent): void { analytics.totalVolumeBuilds = analytics.totalVolumeBuilds.plus(initialNotional) analytics.totalVolume = analytics.totalVolume.plus(initialNotional) + // Update hourly analytics data for this market updateAnalyticsHourData(analytics, event.block.timestamp) + // Increment the sender's open positions count sender.numberOfOpenPositions = sender.numberOfOpenPositions.plus(ONE_BI) + // Update referral rewards and trader epoch volume for the sender updateReferralRewards(event, event.params.sender, transferFeeAmount) updateTraderEpochVolume(event.params.sender, initialNotional) + + // Update the sender's total traded volume sender.ovlVolumeTraded = sender.ovlVolumeTraded.plus(initialNotional) + // Update hourly market data with the new position's notional updateMarketHourData(market, event.block.timestamp, initialNotional, ZERO_BI) + // Save all the updated and new entities position.save() market.save() build.save() @@ -251,32 +317,37 @@ export function handleBuild(event: BuildEvent): void { } export function handleUnwind(event: UnwindEvent): void { + // Load the market entity using the market address from the event let market = loadMarket(event, event.address) - let marketState = updateMarketState(market.id) + // Load the account entity corresponding to the sender of the transaction let sender = loadAccount(event.params.sender) + // Convert the sender's ID to an Address type for further usage let senderAddress = Address.fromBytes(sender.id) + // Retrieve the position ID from the event and load the corresponding position entity let positionId = event.params.positionId let position = loadPosition(event, senderAddress, market, positionId) + // Track the current number of unwinds for this position let unwindNumber = position.numberOfUniwnds + // Increment the number of unwinds for this position position.mint = position.mint.plus(event.params.mint) position.numberOfUniwnds = position.numberOfUniwnds.plus(BigInt.fromI32(1)) - market.oiLong = marketState.oiLong - market.oiShort = marketState.oiShort + // Load or create the Transaction entity for this event let transaction = loadTransaction(event) - let unwind = new Unwind(position.id.concatI32(unwindNumber.toI32())) as Unwind + // Create a new Unwind entity to represent this unwind operation + let unwind = new Unwind(position.id.concat('-').concat(unwindNumber.toString())) as Unwind - let receipt = event.receipt - // initialize variables + // Initialize variables for the PnL, transfer amount, and fee amount let transferAmount = ZERO_BI let transferFeeAmount = ZERO_BI let pnl = ZERO_BI - // fraction of the position unwound BEFORE this transaction + + // Fraction of the position unwound BEFORE this transaction const fractionUnwound = position.fractionUnwound - // this unwind size = intialCollateral * (1 - fractionUnwound) * unwindFraction + // This unwind size = initialCollateral * (1 - fractionUnwound) * unwindFraction const fractionOfPosition = (ONE_18DEC_BI.minus(fractionUnwound)).times(event.params.fraction).div(ONE_18DEC_BI) const unwindSize = position.initialCollateral .times( @@ -285,49 +356,59 @@ export function handleUnwind(event: UnwindEvent): void { ONE_18DEC_BI ) + // Retrieve the transaction receipt and load the factory entity associated with the market + let receipt = event.receipt let factory = Factory.load(market.factory) + + // This block of code is to get `transferAmount` and `transferFeeAmount` from the logs + // Unwind transaction consists of 4 relevant logs: + // Unwind, (Approve for the first Unwind in the market), Transfer from SC to user, Transfer from SC to fee recipient + // The code below adjusts the index to correctly identify the transfer logs + if (receipt && factory) { - const logLength = receipt.logs.length - log.warning("handleUnwind: START: tx: {}, transactionLogIndex: {}, logIndex: {}, transaction.index: {}, logs length: {}, logs[0].logIndex: {}, logs[0].transactionIndex: {}, logs[0].transactionLogIndex: {}", [ - event.transaction.hash.toHexString(), - event.transactionLogIndex.toI32().toString(), - event.logIndex.toI32().toString(), - event.transaction.index.toI32().toString(), - logLength.toString(), - receipt.logs[0].logIndex.toI32().toString(), - receipt.logs[0].transactionIndex.toI32().toString(), - receipt.logs[0].transactionLogIndex.toI32().toString(), - ]) + // Initialize the index for the Unwind event log let unwindIndex = 0 + + // Iterate over the logs to find the log corresponding to the Unwind event for (let i = 0; i < receipt.logs.length; i++) { if (receipt.logs[i].logIndex.toI32() === event.logIndex.toI32()) { unwindIndex = i break } } + + // Check if the next log is not an ERC20 transfer event, adjust index if necessary + // The first Unwind event on each market emits an Approve event after the Unwind event if (receipt.logs[unwindIndex + 1].topics[0].notEqual(TRANSFER_SIG)) { unwindIndex += 1 } + + // Calculate the index for the transfer event log (user transfer) const userTransferIndex = +unwindIndex + 2 const feeIndex = +unwindIndex + 3 + + // Extract the first topic and address of the user transfer log const _topic0user = receipt.logs[userTransferIndex].topics[0] const _addressuser = receipt.logs[userTransferIndex].address - // find the log that matches the ERC20 transfer to the owner + + // Find the log that matches the ERC20 transfer to the owner if ( _topic0user.equals(TRANSFER_SIG) && _addressuser.toHexString() == OVL_ADDRESS && receipt.logs[userTransferIndex].topics.length > 1 ) { + // Decode the recipient address from the log's topics let topics2address = ethereum.decode('address', receipt.logs[userTransferIndex].topics[2])!.toAddress() + + // Check if the recipient is the sender and decode the transfer amount if (topics2address == event.params.sender) { const _transferAmount = receipt.logs[userTransferIndex].data - // save transferAmount from the ERC20 Transfer event + // Save transferAmount from the ERC20 Transfer event transferAmount = ethereum.decode('uin256', _transferAmount)!.toBigInt() - // calculate pnl = tranferAmount - unwindSize - pnl = transferAmount.minus( - unwindSize - ) + // Calculate PnL = transferAmount - unwindSize + pnl = transferAmount.minus(unwindSize) } else { + // Log an error if the recipient does not match the expected sender log.error("handleUnwind: 2nd if: transaction: {}, unwindIndex: {}, userTransferIndex {}", [ event.transaction.hash.toHexString(), unwindIndex.toString(), @@ -335,25 +416,34 @@ export function handleUnwind(event: UnwindEvent): void { ]) } } else { + // Log an error if the log does not match the expected ERC20 transfer event log.error("handleUnwind: 1st if: transaction: {}, unwindIndex: {}, userTransferIndex {}", [ event.transaction.hash.toHexString(), unwindIndex.toString(), userTransferIndex.toString() ]) } + + // Extract the first topic and address of the fee transfer log const _topic0 = receipt.logs[feeIndex].topics[0] const _address = receipt.logs[feeIndex].address - // find the log that matches the ERC20 transfer to the owner + + // Find the log that matches the ERC20 transfer to the fee recipient if ( _topic0.equals(TRANSFER_SIG) && _address.toHexString() == OVL_ADDRESS && receipt.logs[feeIndex].topics.length > 1 ) { + // Decode the recipient address from the log's topics let topics2address = ethereum.decode('address', receipt.logs[feeIndex].topics[2])!.toAddress() + + // Check if the recipient is the fee recipient and decode the transfer amount if (topics2address.toHexString().toLowerCase() == factory.feeRecipient.toLowerCase()) { const _transferAmount = receipt.logs[feeIndex].data + // Save the transferFeeAmount from the ERC20 Transfer event transferFeeAmount = ethereum.decode('uin256', _transferAmount)!.toBigInt() } else { + // Log an error if the recipient does not match the expected fee recipient log.error("handleUnwind: 2nd if: transaction: {}, unwindIndex: {}, feeIndex {}", [ event.transaction.hash.toHexString(), unwindIndex.toString(), @@ -361,6 +451,7 @@ export function handleUnwind(event: UnwindEvent): void { ]) } } else { + // Log an error if the log does not match the expected ERC20 transfer event log.error("handleUnwind: 1st if: transaction: {}, unwindIndex: {}, feeIndex {}", [ event.transaction.hash.toHexString(), unwindIndex.toString(), @@ -369,27 +460,37 @@ export function handleUnwind(event: UnwindEvent): void { } } + // Calculate the amount of open interest unwound + let oiUnwound: BigInt; // used later for fundingPayment calculations + + if (position.isLong) { + oiUnwound = market.oiLong.minus(event.params.oiAfterUnwind) + market.oiLong = event.params.oiAfterUnwind + market.oiLongShares = event.params.oiSharesAfterUnwind + } else { + oiUnwound = market.oiShort.minus(event.params.oiAfterUnwind) + market.oiShort = event.params.oiAfterUnwind + market.oiShortShares = event.params.oiSharesAfterUnwind + } + + // Assign calculated values to the Unwind entity based on the event and calculations unwind.position = position.id unwind.owner = sender.id unwind.size = unwindSize unwind.transferAmount = transferAmount unwind.pnl = pnl unwind.feeAmount = transferFeeAmount - unwind.currentOi = position.currentOi // TODO remove - unwind.currentDebt = position.currentDebt - unwind.isLong = position.isLong + unwind.oiUnwound = oiUnwound unwind.price = event.params.price unwind.fraction = event.params.fraction unwind.fractionOfPosition = fractionOfPosition unwind.volume = transferAmount.plus(position.initialDebt.times(fractionOfPosition).div(ONE_18DEC_BI)) unwind.mint = event.params.mint unwind.unwindNumber = unwindNumber - unwind.collateral = ZERO_BI - unwind.value = ZERO_BI unwind.timestamp = transaction.timestamp unwind.transaction = transaction.id - // analytics update + // Analytics update: update overall metrics let analytics = loadAnalytics(market.factory) analytics.totalTransactions = analytics.totalTransactions.plus(ONE_BI) analytics.totalTokensLocked = analytics.totalTokensLocked.minus(position.initialCollateral.times(fractionOfPosition).div(ONE_18DEC_BI)) @@ -398,39 +499,18 @@ export function handleUnwind(event: UnwindEvent): void { updateAnalyticsHourData(analytics, event.block.timestamp) - // position.currentOi = stateContract.oi(marketAddress, senderAddress, positionId) + position.currentOi = position.currentOi.minus(oiUnwound) + // Update the position's current debt and open interest position.currentDebt = position.currentDebt.times(ONE_18DEC_BI.minus(unwind.fraction)).div(ONE_18DEC_BI) - let oiUnwound: BigInt; // used later for fundingPayment calculations - - if (position.isLong) { - oiUnwound = market.oiLong.minus(event.params.oiAfterUnwind) - market.oiLong = event.params.oiAfterUnwind - market.oiLongShares = event.params.oiSharesAfterUnwind - } else { - oiUnwound = market.oiShort.minus(event.params.oiAfterUnwind) - market.oiShort = event.params.oiAfterUnwind - market.oiShortShares = event.params.oiSharesAfterUnwind - } - + // Update market-level metrics with the calculated fee and notional market.totalUnwindFees = market.totalUnwindFees.plus(transferFeeAmount) market.numberOfUnwinds = market.numberOfUnwinds.plus(ONE_BI) market.totalFees = market.totalFees.plus(transferFeeAmount) market.totalVolume = market.totalVolume.plus(unwind.volume) market.totalMint = market.totalMint.plus(event.params.mint) - log.warning("fractionUnwound: {}, {}, {}", [ - fractionUnwound.toString(), - event.params.fraction.toString(), - ONE_18DEC_BI - .minus( - (ONE_18DEC_BI.minus(fractionUnwound)) - .times - (ONE_18DEC_BI.minus(event.params.fraction)) - .div(ONE_18DEC_BI) - ).toString() - ]) - + // Calculate the updated fraction unwound for the position position.fractionUnwound = ONE_18DEC_BI .minus( @@ -440,28 +520,31 @@ export function handleUnwind(event: UnwindEvent): void { .div(ONE_18DEC_BI) ) + // Calculate the funding payment for the unwind operation // funding = exitPrice * (oiUnwound - oiInitial * fractionUnwound) - // oiUnwound = oiBeforeUnwind - oiAfterUnwind - const fundingPayment = event.params.price.times( oiUnwound.minus( - position.initialOi.times(fractionOfPosition) + position.initialOi.times(fractionOfPosition).div(ONE_18DEC_BI) ) - ) + ).div(ONE_18DEC_BI) unwind.fundingPayment = fundingPayment + // Update the sender's metrics: increment unwinds, update realized PnL, and decrement open positions if fully unwound sender.numberOfUnwinds = sender.numberOfUnwinds.plus(ONE_BI) sender.realizedPnl = sender.realizedPnl.plus(pnl) if (event.params.fraction == ONE_18DEC_BI) { sender.numberOfOpenPositions = sender.numberOfOpenPositions.minus(ONE_BI) } + // Update referral rewards and trader epoch volume for the sender updateReferralRewards(event, event.params.sender, transferFeeAmount) updateTraderEpochVolume(event.params.sender, unwind.volume) sender.ovlVolumeTraded = sender.ovlVolumeTraded.plus(unwind.volume) + // Update hourly market data with the new position's notional updateMarketHourData(market, event.block.timestamp, unwind.volume, event.params.mint) + // Save all the updated and new entities position.save() market.save() unwind.save() @@ -470,61 +553,76 @@ export function handleUnwind(event: UnwindEvent): void { analytics.save() } + export function handleEmergencyWithdraw(event: EmergencyWithdrawEvent): void { + // Load the market entity using the market address from the event let market = loadMarket(event, event.address) - let marketState = updateMarketState(market.id) + // Update the market state by retrieving fresh data from the state contract + // Load the account entity corresponding to the sender of the transaction let sender = loadAccount(event.params.sender) + // Convert the sender's ID to an Address type for further usage let senderAddress = Address.fromBytes(sender.id) + // Retrieve the position ID from the event and load the corresponding position entity let positionId = event.params.positionId let position = loadPosition(event, senderAddress, market, positionId) + // Track the current number of unwinds for this position let unwindNumber = position.numberOfUniwnds + // Increment the number of unwinds for this position position.numberOfUniwnds = position.numberOfUniwnds.plus(BigInt.fromI32(1)) - market.oiLong = marketState.oiLong - market.oiShort = marketState.oiShort + // Update the market's open interest values based on the current market state + if (position.isLong) { + market.oiLong = market.oiLong.minus(position.currentOi) + } else { + market.oiShort = market.oiShort.minus(position.currentOi) + } + // Load or create the Transaction entity for this event let transaction = loadTransaction(event) - let unwind = new Unwind(position.id.concatI32(unwindNumber.toI32())) as Unwind + // Create a new Unwind entity to represent this emergency withdrawal + let unwind = new Unwind(position.id.concat('-').concat(unwindNumber.toString())) as Unwind - // fraction of the position unwound BEFORE this transaction + // Calculate the fraction of the position that was unwound before this transaction const fractionUnwound = position.fractionUnwound - // this unwind size = intialCollateral * (1 - fractionUnwound) * unwindFraction + // Calculate the fraction of the position being unwound in this transaction const fractionOfPosition = (ONE_18DEC_BI.minus(fractionUnwound)).times(ONE_18DEC_BI).div(ONE_18DEC_BI) + // Assign values to the Unwind entity based on the event and position data unwind.position = position.id unwind.owner = sender.id - unwind.size = event.params.collateral - unwind.transferAmount = event.params.collateral - unwind.pnl = ZERO_BI - unwind.feeAmount = ZERO_BI - unwind.currentOi = position.currentOi - unwind.currentDebt = position.currentDebt - unwind.isLong = position.isLong - unwind.price = ZERO_BI - unwind.fraction = ONE_18DEC_BI - unwind.fractionOfPosition = fractionOfPosition - unwind.volume = ZERO_BI - unwind.mint = ZERO_BI - unwind.unwindNumber = unwindNumber - unwind.collateral = ZERO_BI - unwind.value = ZERO_BI - unwind.timestamp = transaction.timestamp - unwind.transaction = transaction.id + unwind.size = event.params.collateral // The size of the unwind is the collateral being withdrawn + unwind.transferAmount = event.params.collateral // The transfer amount is the same as the withdrawn collateral + unwind.pnl = ZERO_BI // PnL is zero since it's an emergency withdrawal + unwind.feeAmount = ZERO_BI // No fees are charged during an emergency withdrawal + unwind.oiUnwound = position.currentOi // Open interest at the time of the emergency withdrawal + unwind.price = ZERO_BI // Price is not applicable in an emergency withdrawal + unwind.fraction = ONE_18DEC_BI // The entire position is unwound + unwind.fractionOfPosition = fractionOfPosition // Fraction of the original position unwound + unwind.volume = ZERO_BI // No trading volume associated with an emergency withdrawal + unwind.mint = ZERO_BI // No minting occurs in an emergency withdrawal + unwind.unwindNumber = unwindNumber // The current unwind number for this position + unwind.timestamp = transaction.timestamp // Timestamp of the transaction + unwind.transaction = transaction.id // ID of the transaction unwind.fundingPayment = ZERO_BI + // Reset the position's open interest and debt to zero since the position is fully unwound position.currentOi = ZERO_BI position.currentDebt = ZERO_BI + // Increment the total number of unwinds in the market market.numberOfUnwinds = market.numberOfUnwinds.plus(ONE_BI) + // Set the fraction unwound to 1, indicating the entire position has been unwound position.fractionUnwound = ONE_18DEC_BI + // Update the sender's metrics: increment unwinds and decrement open positions sender.numberOfUnwinds = sender.numberOfUnwinds.plus(ONE_BI) sender.numberOfOpenPositions = sender.numberOfOpenPositions.minus(ONE_BI) + // Save the updated entities to the subgraph store position.save() market.save() unwind.save() @@ -534,7 +632,6 @@ export function handleEmergencyWithdraw(event: EmergencyWithdrawEvent): void { export function handleCacheRiskCalc(event: CacheRiskCalcEvent): void { let market = loadMarket(event, event.address) - let marketState = updateMarketState(market.id) market.dpUpperLimit = event.params.newDpUpperLimit @@ -543,7 +640,6 @@ export function handleCacheRiskCalc(event: CacheRiskCalcEvent): void { export function handleUpdate(event: UpdateEvent): void { let market = loadMarket(event, event.address) - let marketState = updateMarketState(market.id) market.oiLong = event.params.oiLong market.oiShort = event.params.oiShort @@ -552,57 +648,70 @@ export function handleUpdate(event: UpdateEvent): void { } export function handleLiquidate(event: LiquidateEvent): void { + // Load the market entity using the market address from the event let market = loadMarket(event, event.address) - let marketState = updateMarketState(market.id) + // Load the account entity corresponding to the sender (liquidator) of the transaction let sender = loadAccount(event.params.sender) + // Load the account entity corresponding to the owner of the liquidated position let owner = loadAccount(event.params.owner) + // Convert the owner's ID to an Address type for further usage let ownerAddress = Address.fromBytes(owner.id) + // Retrieve the position ID from the event and load the corresponding position entity let positionId = event.params.positionId let position = loadPosition(event, ownerAddress, market, positionId) + // Retrieve the transaction receipt and initialize variables for fee and liquidator amounts let receipt = event.receipt - // initialize variables let transferFeeAmount = ZERO_BI let transferLiquidatorAmount = ZERO_BI let factory = Factory.load(market.factory) + + // This block of code is to get `transferFeeAmount` and `transferLiquidatorAmount` from the logs + // Liquidate transaction consists of 4 relevant logs: + // Liquidate, (Approve for the first Liquidate in the market), Transfer from SC to liquidator, Transfer from SC to fee recipient + // The code below adjusts the index to correctly identify the transfer logs + if (receipt && factory) { - const logLength = receipt.logs.length - log.warning("handleLiquidate: START: tx: {}, transactionLogIndex: {}, logIndex: {}, transaction.index: {}, logs length: {}, logs[0].logIndex: {}, logs[0].transactionIndex: {}, logs[0].transactionLogIndex: {}", [ - event.transaction.hash.toHexString(), - event.transactionLogIndex.toI32().toString(), - event.logIndex.toI32().toString(), - event.transaction.index.toI32().toString(), - logLength.toString(), - receipt.logs[0].logIndex.toI32().toString(), - receipt.logs[0].transactionIndex.toI32().toString(), - receipt.logs[0].transactionLogIndex.toI32().toString(), - ]) + // Initialize the index for the Liquidate event log let liquidateIndex = 0 + + // Iterate over the logs to find the log corresponding to the Liquidate event for (let i = 0; i < receipt.logs.length; i++) { if (receipt.logs[i].logIndex.toI32() === event.logIndex.toI32()) { liquidateIndex = i break } } + + // Check if the next log is not an ERC20 transfer event, adjust index if necessary + // The first Liquidate event on each market emits an Approve event after the Liquidate event if (receipt.logs[liquidateIndex + 1].topics[0].notEqual(TRANSFER_SIG)) { liquidateIndex += 1 } + + // Calculate the index for the fee transfer event log const feeIndex = +liquidateIndex + 3 let _topic0 = receipt.logs[feeIndex].topics[0] let _address = receipt.logs[feeIndex].address - // find the log that matches the ERC20 transfer to the feeRecipient + + // Find the log that matches the ERC20 transfer to the feeRecipient if ( _topic0.equals(TRANSFER_SIG) && _address.toHexString() == OVL_ADDRESS && receipt.logs[feeIndex].topics.length > 1 ) { + // Decode the recipient address from the log's topics let topics2address = ethereum.decode('address', receipt.logs[feeIndex].topics[2])!.toAddress() + + // Check if the recipient is the fee recipient and decode the transfer amount if (topics2address.toHexString().toLowerCase() == factory.feeRecipient.toLowerCase()) { const _transferAmount = receipt.logs[feeIndex].data + // Save the transferFeeAmount from the ERC20 Transfer event transferFeeAmount = ethereum.decode('uin256', _transferAmount)!.toBigInt() } else { + // Log an error if the recipient does not match the expected fee recipient log.error("handleLiquidate: 2nd if: transaction: {}, liquidateIndex: {}, feeIndex {}", [ event.transaction.hash.toHexString(), liquidateIndex.toString(), @@ -610,26 +719,35 @@ export function handleLiquidate(event: LiquidateEvent): void { ]) } } else { + // Log an error if the log does not match the expected ERC20 transfer event log.error("handleLiquidate: 1st if: transaction: {}, liquidateIndex: {}, feeIndex {}", [ event.transaction.hash.toHexString(), liquidateIndex.toString(), feeIndex.toString() ]) } + + // Calculate the index for the liquidator transfer event log const liquidatorTransferIndex = +liquidateIndex + 2 _topic0 = receipt.logs[liquidatorTransferIndex].topics[0] _address = receipt.logs[liquidatorTransferIndex].address - // find the log that matches the ERC20 transfer to the sender + + // Find the log that matches the ERC20 transfer to the sender (liquidator) if ( _topic0.equals(TRANSFER_SIG) && _address.toHexString() == OVL_ADDRESS && receipt.logs[liquidatorTransferIndex].topics.length > 1 ) { + // Decode the recipient address from the log's topics let topics2address = ethereum.decode('address', receipt.logs[liquidatorTransferIndex].topics[2])!.toAddress() + + // Check if the recipient is the sender (liquidator) and decode the transfer amount if (topics2address == Address.fromBytes(sender.id)) { const _transferAmount = receipt.logs[liquidatorTransferIndex].data + // Save the transferLiquidatorAmount from the ERC20 Transfer event transferLiquidatorAmount = ethereum.decode('uin256', _transferAmount)!.toBigInt() } else { + // Log an error if the recipient does not match the expected sender (liquidator) log.error("handleLiquidate: 2nd if: transaction: {}, liquidateIndex: {}, liquidatorTransferIndex {}", [ event.transaction.hash.toHexString(), liquidateIndex.toString(), @@ -637,6 +755,7 @@ export function handleLiquidate(event: LiquidateEvent): void { ]) } } else { + // Log an error if the log does not match the expected ERC20 transfer event log.error("handleLiquidate: 1st if: transaction: {}, liquidateIndex: {}, liquidatorTransferIndex {}", [ event.transaction.hash.toHexString(), liquidateIndex.toString(), @@ -644,17 +763,24 @@ export function handleLiquidate(event: LiquidateEvent): void { ]) } } + + // Calculate the fraction of the position that is being liquidated const fractionOfPosition = ONE_18DEC_BI.minus(position.fractionUnwound) + // Calculate the size of the liquidated position const liquidateSize = position.initialCollateral.times(fractionOfPosition).div(ONE_18DEC_BI) + // Update the owner's realized PnL by subtracting the liquidated position's size owner.realizedPnl = owner.realizedPnl.minus(liquidateSize) + // Update the position with the liquidation information position.mint = position.mint.plus(event.params.mint) position.isLiquidated = true position.fractionUnwound = ONE_18DEC_BI + // Initialize a variable to hold the amount of open interest unwound let oiUnwound: BigInt; // used later for fundingPayment calculations + // Update the market's open interest and open interest shares based on the position's side (long/short) if (position.isLong) { oiUnwound = market.oiLong.minus(event.params.oiAfterLiquidate) market.oiLong = event.params.oiAfterLiquidate @@ -664,61 +790,71 @@ export function handleLiquidate(event: LiquidateEvent): void { market.oiShort = event.params.oiAfterLiquidate market.oiShortShares = event.params.oiSharesAfterLiquidate } + + // Update market-level metrics with the calculated fee and notional market.totalLiquidateFees = market.totalLiquidateFees.plus(transferFeeAmount) market.numberOfLiquidates = market.numberOfLiquidates.plus(ONE_BI) market.totalFees = market.totalFees.plus(transferFeeAmount) + // Load or create the Transaction entity for this event let transaction = loadTransaction(event) + // Create a new Liquidate entity to track this specific liquidation event let liquidate = new Liquidate(position.id) as Liquidate + // Calculate the funding payment for the liquidation operation // funding = exitPrice * (oiUnwound - oiInitial * fractionUnwound) // oiUnwound = oiBeforeUnwind - oiAfterUnwind const fundingPayment = event.params.price.times( - oiUnwound.minus(fractionOfPosition) - ) + oiUnwound.minus( + position.initialOi.times(fractionOfPosition).div(ONE_18DEC_BI) + ) + ).div(ONE_18DEC_BI) liquidate.fundingPayment = fundingPayment + + // Assign values to the Liquidate entity based on the event and calculations liquidate.position = position.id liquidate.owner = owner.id liquidate.sender = sender.id - liquidate.currentOi = position.currentOi - liquidate.currentDebt = position.currentDebt - liquidate.isLong = position.isLong liquidate.price = event.params.price liquidate.mint = event.params.mint - liquidate.collateral = ZERO_BI - liquidate.value = ZERO_BI liquidate.timestamp = transaction.timestamp liquidate.transaction = transaction.id liquidate.fractionOfPosition = fractionOfPosition liquidate.size = liquidateSize liquidate.liquidationFee = transferLiquidatorAmount - // marginToBurn = feeRecipientAmount / (1/ MaintenanceMarginBurnRate - 1) + // marginToBurn = feeRecipientAmount / (1 / MaintenanceMarginBurnRate - 1) const marginToBurn = transferFeeAmount.times(ONE_18DEC_BI).div(ONE_18DEC_BI.times(ONE_18DEC_BI).div(market.maintenanceMarginBurnRate).minus(ONE_18DEC_BI)) // volume = transferFeeAmount + initialDebt * fractionOfPosition / ONE + transferLiquidatorAmount liquidate.volume = transferFeeAmount.plus(position.initialDebt.times(fractionOfPosition).div(ONE_18DEC_BI)).plus(transferLiquidatorAmount) liquidate.marginToBurn = marginToBurn liquidate.transferFeeAmount = transferFeeAmount - // analytics update + // Update the analytics entity to reflect the new liquidation and market activity let analytics = loadAnalytics(market.factory) analytics.totalTransactions = analytics.totalTransactions.plus(ONE_BI) analytics.totalTokensLocked = analytics.totalTokensLocked.minus(position.initialCollateral.times(fractionOfPosition).div(ONE_18DEC_BI)) analytics.totalVolumeLiquidations = analytics.totalVolumeLiquidations.plus(liquidate.volume) analytics.totalVolume = analytics.totalVolume.plus(liquidate.volume) + // Update hourly analytics data for this market updateAnalyticsHourData(analytics, event.block.timestamp) + // Update market-level metrics with the new liquidation's volume and mint amount market.totalVolume = market.totalVolume.plus(liquidate.volume) market.totalMint = market.totalMint.plus(event.params.mint) + // Set the position's open interest and debt to zero after liquidation position.currentOi = ZERO_BI position.currentDebt = ZERO_BI + // Update the owner's metrics: increment liquidated positions and decrement open positions owner.numberOfLiquidatedPositions = owner.numberOfLiquidatedPositions.plus(ONE_BI) owner.numberOfOpenPositions = owner.numberOfOpenPositions.minus(ONE_BI) + // Update hourly market data with the new liquidation's volume and mint amount updateMarketHourData(market, event.block.timestamp, liquidate.volume, event.params.mint) + // Save all the updated and new entities position.save() market.save() liquidate.save() diff --git a/src/power-card.ts b/src/power-card.ts index 66b51b5..edc1f82 100644 --- a/src/power-card.ts +++ b/src/power-card.ts @@ -113,9 +113,6 @@ function loadERC1155TokenBalance(tokenAddress: Address, tokenId: BigInt, owner: function loadERC1155Token(tokenAddress: Address, tokenId: BigInt): ERC1155Token { let erc1155TokenId = tokenAddress.concatI32(tokenId.toI32()); - - log.info(erc1155TokenId.toHexString(), []) - let erc1155Token = ERC1155Token.load(erc1155TokenId); if (erc1155Token === null) { diff --git a/src/utils/index.ts b/src/utils/index.ts index 0fb97aa..01efe1b 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -1,4 +1,4 @@ -import { Address, BigInt, Bytes, ethereum } from '@graphprotocol/graph-ts' +import { Address, BigInt, Bytes, ethereum, log } from '@graphprotocol/graph-ts' import { Market, Transaction, Position, Factory, Account, Analytics, AnalyticsHourData } from '../../generated/schema' import { OverlayV1Market } from '../../generated/templates/OverlayV1Market/OverlayV1Market' import { OverlayV1Market as MarketTemplate } from '../../generated/templates'; @@ -65,6 +65,7 @@ export function loadMarket(event: ethereum.Event, marketId: Bytes): Market { market.minCollateral = marketContract.params(integer.fromNumber(12)) market.priceDriftUpperLimit = marketContract.params(integer.fromNumber(13)) market.averageBlockTime = marketContract.params(integer.fromNumber(14)) + log.warning("loadMarket makes external calls!", []) market.oiLong = stateContract.ois(marketAddress).value0 market.oiShort = stateContract.ois(marketAddress).value1 market.oiLongShares = marketContract.oiLongShares() @@ -88,7 +89,7 @@ export function loadMarket(event: ethereum.Event, marketId: Bytes): Market { } export function loadPosition(event: ethereum.Event, sender: Address, market: Market, positionId: BigInt): Position { - let marketPositionId = market.id.concatI32(positionId.toI32()) + let marketPositionId = market.id.toHexString().concat('-').concat(positionId.toHexString()) let marketAddress = Address.fromBytes(market.id) let position = Position.load(marketPositionId) @@ -99,6 +100,7 @@ export function loadPosition(event: ethereum.Event, sender: Address, market: Mar position.owner = sender position.market = market.id + log.warning("loadPosition makes external calls!", []) position.initialOi = stateContract.oi(marketAddress, sender, positionId) position.initialDebt = stateContract.debt(marketAddress, sender, positionId)