diff --git a/fee-tracking-subgraph/README.md b/fee-tracking-subgraph/README.md new file mode 100644 index 0000000..b48e22f --- /dev/null +++ b/fee-tracking-subgraph/README.md @@ -0,0 +1,110 @@ +# Fee Tracking Subgraph + +This subgraph tracks fee events from the Polymarket FeeModule contract, monitoring fee refunds and withdrawals to ensure transparency in the fee system. + +## Purpose + +The fee tracking subgraph monitors: +- Fee refunds given to users when the actual fee charged is higher than the intended fee +- Fee withdrawals by administrators +- Per-user fee statistics and global fee statistics + +This helps ensure the fee system works as intended and provides transparency into fee collection and refunds. + +## Entities + +### FeeRefund +Tracks individual fee refund events with: +- Order hash that triggered the refund +- Maker address who received the refund +- Token ID (0 for collateral, otherwise CTF token ID) +- Refund amount +- Original fee amount charged +- Timestamp and transaction details + +### FeeWithdrawal +Tracks fee withdrawal events with: +- Token address +- Recipient address +- Token ID (0 for collateral, otherwise CTF token ID) +- Amount withdrawn +- Timestamp and transaction details + +### GlobalFeeStats +Aggregate statistics across all fee events: +- Total refunds given +- Total fees withdrawn +- Number of refund events +- Number of withdrawal events + +### UserFeeStats +Per-user fee statistics: +- User's total refunds received +- User's total withdrawals +- User's refund event count +- User's withdrawal event count +- Last refund and withdrawal event timestamps + +## Usage + +Query fee refunds: +```graphql +{ + feeRefunds(first: 10, orderBy: timestamp, orderDirection: desc) { + orderHash + maker + tokenId + refundAmount + feeAmount + timestamp + } +} +``` + +Query fee withdrawals: +```graphql +{ + feeWithdrawals(first: 10, orderBy: timestamp, orderDirection: desc) { + token + to + tokenId + amount + timestamp + } +} +``` + +Query global statistics: +```graphql +{ + globalFeeStats(id: "") { + totalRefunds + totalWithdrawals + refundEventCount + withdrawalEventCount + } +} +``` + +Query user statistics: +```graphql +{ + userFeeStats(id: "0xUserAddress") { + totalRefunds + totalWithdrawals + refundEventCount + withdrawalEventCount + lastRefundEvent + lastWithdrawalEvent + } +} +``` + +## Deployment + +This subgraph requires: +- FeeModule contract address +- Start block for indexing +- Network configuration + +Update the `subgraph.template.yaml` with the correct contract address and start block before deployment. \ No newline at end of file diff --git a/fee-tracking-subgraph/abis/FeeModule.json b/fee-tracking-subgraph/abis/FeeModule.json new file mode 100644 index 0000000..4ad456f --- /dev/null +++ b/fee-tracking-subgraph/abis/FeeModule.json @@ -0,0 +1,507 @@ +[ + { + "type": "constructor", + "inputs": [ + { + "name": "_exchange", + "type": "address", + "internalType": "address" + } + ], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "addAdmin", + "inputs": [ + { + "name": "admin", + "type": "address", + "internalType": "address" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "admins", + "inputs": [ + { + "name": "", + "type": "address", + "internalType": "address" + } + ], + "outputs": [ + { + "name": "", + "type": "uint256", + "internalType": "uint256" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "collateral", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "address", + "internalType": "address" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "ctf", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "address", + "internalType": "address" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "exchange", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "address", + "internalType": "contract IExchange" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "isAdmin", + "inputs": [ + { + "name": "addr", + "type": "address", + "internalType": "address" + } + ], + "outputs": [ + { + "name": "", + "type": "bool", + "internalType": "bool" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "matchOrders", + "inputs": [ + { + "name": "takerOrder", + "type": "tuple", + "internalType": "struct Order", + "components": [ + { + "name": "salt", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "maker", + "type": "address", + "internalType": "address" + }, + { + "name": "signer", + "type": "address", + "internalType": "address" + }, + { + "name": "taker", + "type": "address", + "internalType": "address" + }, + { + "name": "tokenId", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "makerAmount", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "takerAmount", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "expiration", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "nonce", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "feeRateBps", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "side", + "type": "uint8", + "internalType": "enum Side" + }, + { + "name": "signatureType", + "type": "uint8", + "internalType": "enum SignatureType" + }, + { + "name": "signature", + "type": "bytes", + "internalType": "bytes" + } + ] + }, + { + "name": "makerOrders", + "type": "tuple[]", + "internalType": "struct Order[]", + "components": [ + { + "name": "salt", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "maker", + "type": "address", + "internalType": "address" + }, + { + "name": "signer", + "type": "address", + "internalType": "address" + }, + { + "name": "taker", + "type": "address", + "internalType": "address" + }, + { + "name": "tokenId", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "makerAmount", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "takerAmount", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "expiration", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "nonce", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "feeRateBps", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "side", + "type": "uint8", + "internalType": "enum Side" + }, + { + "name": "signatureType", + "type": "uint8", + "internalType": "enum SignatureType" + }, + { + "name": "signature", + "type": "bytes", + "internalType": "bytes" + } + ] + }, + { + "name": "takerFillAmount", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "makerFillAmounts", + "type": "uint256[]", + "internalType": "uint256[]" + }, + { + "name": "takerFeeAmount", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "makerFeeAmounts", + "type": "uint256[]", + "internalType": "uint256[]" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "onERC1155BatchReceived", + "inputs": [ + { + "name": "", + "type": "address", + "internalType": "address" + }, + { + "name": "", + "type": "address", + "internalType": "address" + }, + { + "name": "", + "type": "uint256[]", + "internalType": "uint256[]" + }, + { + "name": "", + "type": "uint256[]", + "internalType": "uint256[]" + }, + { + "name": "", + "type": "bytes", + "internalType": "bytes" + } + ], + "outputs": [ + { + "name": "", + "type": "bytes4", + "internalType": "bytes4" + } + ], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "onERC1155Received", + "inputs": [ + { + "name": "", + "type": "address", + "internalType": "address" + }, + { + "name": "", + "type": "address", + "internalType": "address" + }, + { + "name": "", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "", + "type": "bytes", + "internalType": "bytes" + } + ], + "outputs": [ + { + "name": "", + "type": "bytes4", + "internalType": "bytes4" + } + ], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "removeAdmin", + "inputs": [ + { + "name": "admin", + "type": "address", + "internalType": "address" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "renounceAdmin", + "inputs": [], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "withdrawFees", + "inputs": [ + { + "name": "to", + "type": "address", + "internalType": "address" + }, + { + "name": "id", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "amount", + "type": "uint256", + "internalType": "uint256" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "event", + "name": "FeeRefunded", + "inputs": [ + { + "name": "orderHash", + "type": "bytes32", + "indexed": true, + "internalType": "bytes32" + }, + { + "name": "to", + "type": "address", + "indexed": true, + "internalType": "address" + }, + { + "name": "id", + "type": "uint256", + "indexed": false, + "internalType": "uint256" + }, + { + "name": "refund", + "type": "uint256", + "indexed": false, + "internalType": "uint256" + }, + { + "name": "feeCharged", + "type": "uint256", + "indexed": true, + "internalType": "uint256" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "FeeWithdrawn", + "inputs": [ + { + "name": "token", + "type": "address", + "indexed": false, + "internalType": "address" + }, + { + "name": "to", + "type": "address", + "indexed": false, + "internalType": "address" + }, + { + "name": "id", + "type": "uint256", + "indexed": false, + "internalType": "uint256" + }, + { + "name": "amount", + "type": "uint256", + "indexed": false, + "internalType": "uint256" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "NewAdmin", + "inputs": [ + { + "name": "admin", + "type": "address", + "indexed": true, + "internalType": "address" + }, + { + "name": "newAdminAddress", + "type": "address", + "indexed": true, + "internalType": "address" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "RemovedAdmin", + "inputs": [ + { + "name": "admin", + "type": "address", + "indexed": true, + "internalType": "address" + }, + { + "name": "removedAdmin", + "type": "address", + "indexed": true, + "internalType": "address" + } + ], + "anonymous": false + }, + { + "type": "error", + "name": "NotAdmin", + "inputs": [] + } +] \ No newline at end of file diff --git a/fee-tracking-subgraph/schema.graphql b/fee-tracking-subgraph/schema.graphql new file mode 100644 index 0000000..5aa8559 --- /dev/null +++ b/fee-tracking-subgraph/schema.graphql @@ -0,0 +1,81 @@ +type FeeRefund @entity { + "Transaction hash + log index" + id: ID! + "Order hash" + orderHash: String! + "Recipient address who received the refund" + to: String! + "Token ID (0 for collateral, otherwise CTF token ID)" + tokenId: BigInt! + "Amount refunded" + refundAmount: BigInt! + "Actual fee amount charged by the system" + feeCharged: BigInt! + "Timestamp of the refund event" + timestamp: BigInt! + "Transaction hash" + txHash: String! +} + +type FeeWithdrawal @entity { + "Transaction hash + log index" + id: ID! + "Token address" + token: String! + "Recipient address" + to: String! + "Token ID (0 for collateral, otherwise CTF token ID)" + tokenId: BigInt! + "Amount withdrawn" + amount: BigInt! + "Timestamp of the withdrawal event" + timestamp: BigInt! + "Transaction hash" + txHash: String! +} + +type GlobalFeeStats @entity { + "Empty string" + id: ID! + "Total cash refunds given" + totalCashRefunds: BigInt! + "Total token refunds given" + totalTokenRefunds: BigInt! + "Total cash fees collected by admins" + totalCashWithdrawals: BigInt! + "Total token fees collected by admins" + totalTokenWithdrawals: BigInt! + "Total fees charged by the system (cash)" + totalCashFeesCharged: BigInt! + "Total fees charged by the system (tokens)" + totalTokenFeesCharged: BigInt! + "Number of refund events" + refundEventCount: BigInt! + "Number of withdrawal events" + withdrawalEventCount: BigInt! +} + +type UserFeeStats @entity { + "User address" + id: ID! + "Total cash refunds received by user" + totalCashRefunds: BigInt! + "Total token refunds received by user" + totalTokenRefunds: BigInt! + "Total cash fees collected by user (if admin)" + totalCashWithdrawals: BigInt! + "Total token fees collected by user (if admin)" + totalTokenWithdrawals: BigInt! + "Total fees charged to user (cash)" + totalCashFeesCharged: BigInt! + "Total fees charged to user (tokens)" + totalTokenFeesCharged: BigInt! + "Number of refund events for user" + refundEventCount: BigInt! + "Number of withdrawal events for user" + withdrawalEventCount: BigInt! + "Last refund event timestamp" + lastRefundEvent: BigInt! + "Last withdrawal event timestamp" + lastWithdrawalEvent: BigInt! +} \ No newline at end of file diff --git a/fee-tracking-subgraph/src/FeeModuleMapping.ts b/fee-tracking-subgraph/src/FeeModuleMapping.ts new file mode 100644 index 0000000..e2d3071 --- /dev/null +++ b/fee-tracking-subgraph/src/FeeModuleMapping.ts @@ -0,0 +1,135 @@ +import { BigInt } from '@graphprotocol/graph-ts'; +import { FeeRefunded, FeeWithdrawn } from '../types/FeeModule/FeeModule'; +import { FeeRefund, FeeWithdrawal, GlobalFeeStats, UserFeeStats } from '../types/schema'; + +function getGlobalFeeStats(): GlobalFeeStats { + let globalStats = GlobalFeeStats.load(''); + if (globalStats == null) { + globalStats = new GlobalFeeStats(''); + globalStats.totalCashRefunds = BigInt.fromI32(0); + globalStats.totalTokenRefunds = BigInt.fromI32(0); + globalStats.totalCashWithdrawals = BigInt.fromI32(0); + globalStats.totalTokenWithdrawals = BigInt.fromI32(0); + globalStats.totalCashFeesCharged = BigInt.fromI32(0); + globalStats.totalTokenFeesCharged = BigInt.fromI32(0); + globalStats.refundEventCount = BigInt.fromI32(0); + globalStats.withdrawalEventCount = BigInt.fromI32(0); + } + return globalStats; +} + +function getUserFeeStats(userAddress: string): UserFeeStats { + let userStats = UserFeeStats.load(userAddress); + if (userStats == null) { + userStats = new UserFeeStats(userAddress); + userStats.totalCashRefunds = BigInt.fromI32(0); + userStats.totalTokenRefunds = BigInt.fromI32(0); + userStats.totalCashWithdrawals = BigInt.fromI32(0); + userStats.totalTokenWithdrawals = BigInt.fromI32(0); + userStats.totalCashFeesCharged = BigInt.fromI32(0); + userStats.totalTokenFeesCharged = BigInt.fromI32(0); + userStats.refundEventCount = BigInt.fromI32(0); + userStats.withdrawalEventCount = BigInt.fromI32(0); + userStats.lastRefundEvent = BigInt.fromI32(0); + userStats.lastWithdrawalEvent = BigInt.fromI32(0); + } + return userStats; +} + +export function handleFeeRefunded(event: FeeRefunded): void { + // Extract event parameters + const orderHash = event.params.orderHash.toHexString(); + const to = event.params.to.toHexString(); + const tokenId = event.params.id; + const refundAmount = event.params.refund; + const feeCharged = event.params.feeCharged; + + // Create unique ID for the fee refund event + const eventId = event.transaction.hash.toHexString() + '-' + event.logIndex.toString(); + + // Create FeeRefund entity + const feeRefund = new FeeRefund(eventId); + feeRefund.orderHash = orderHash; + feeRefund.to = to; + feeRefund.tokenId = tokenId; + feeRefund.refundAmount = refundAmount; + feeRefund.feeCharged = feeCharged; + feeRefund.timestamp = event.block.timestamp; + feeRefund.txHash = event.transaction.hash.toHexString(); + feeRefund.save(); + + // Update global fee statistics + const globalStats = getGlobalFeeStats(); + if (tokenId.equals(BigInt.fromI32(0))) { + // Cash refund (tokenId = 0) + globalStats.totalCashRefunds = globalStats.totalCashRefunds.plus(refundAmount); + globalStats.totalCashFeesCharged = globalStats.totalCashFeesCharged.plus(feeCharged); + } else { + // Token refund (tokenId > 0) + globalStats.totalTokenRefunds = globalStats.totalTokenRefunds.plus(refundAmount); + globalStats.totalTokenFeesCharged = globalStats.totalTokenFeesCharged.plus(feeCharged); + } + globalStats.refundEventCount = globalStats.refundEventCount.plus(BigInt.fromI32(1)); + globalStats.save(); + + // Update user fee statistics + const userStats = getUserFeeStats(to); + if (tokenId.equals(BigInt.fromI32(0))) { + // Cash refund (tokenId = 0) + userStats.totalCashRefunds = userStats.totalCashRefunds.plus(refundAmount); + userStats.totalCashFeesCharged = userStats.totalCashFeesCharged.plus(feeCharged); + } else { + // Token refund (tokenId > 0) + userStats.totalTokenRefunds = userStats.totalTokenRefunds.plus(refundAmount); + userStats.totalTokenFeesCharged = userStats.totalTokenFeesCharged.plus(feeCharged); + } + userStats.refundEventCount = userStats.refundEventCount.plus(BigInt.fromI32(1)); + userStats.lastRefundEvent = event.block.timestamp; + userStats.save(); +} + +export function handleFeeWithdrawn(event: FeeWithdrawn): void { + // Extract event parameters + const token = event.params.token.toHexString(); + const to = event.params.to.toHexString(); + const tokenId = event.params.id; + const amount = event.params.amount; + + // Create unique ID for the fee withdrawal event + const eventId = event.transaction.hash.toHexString() + '-' + event.logIndex.toString(); + + // Create FeeWithdrawal entity + const feeWithdrawal = new FeeWithdrawal(eventId); + feeWithdrawal.token = token; + feeWithdrawal.to = to; + feeWithdrawal.tokenId = tokenId; + feeWithdrawal.amount = amount; + feeWithdrawal.timestamp = event.block.timestamp; + feeWithdrawal.txHash = event.transaction.hash.toHexString(); + feeWithdrawal.save(); + + // Update global fee statistics + const globalStats = getGlobalFeeStats(); + if (tokenId.equals(BigInt.fromI32(0))) { + // Cash withdrawal (tokenId = 0) + globalStats.totalCashWithdrawals = globalStats.totalCashWithdrawals.plus(amount); + } else { + // Token withdrawal (tokenId > 0) + globalStats.totalTokenWithdrawals = globalStats.totalTokenWithdrawals.plus(amount); + } + globalStats.withdrawalEventCount = globalStats.withdrawalEventCount.plus(BigInt.fromI32(1)); + globalStats.save(); + + // Update user fee statistics + const userStats = getUserFeeStats(to); + if (tokenId.equals(BigInt.fromI32(0))) { + // Cash withdrawal (tokenId = 0) + userStats.totalCashWithdrawals = userStats.totalCashWithdrawals.plus(amount); + } else { + // Token withdrawal (tokenId > 0) + userStats.totalTokenWithdrawals = userStats.totalTokenWithdrawals.plus(amount); + } + userStats.withdrawalEventCount = userStats.withdrawalEventCount.plus(BigInt.fromI32(1)); + userStats.lastWithdrawalEvent = event.block.timestamp; + userStats.save(); +} \ No newline at end of file diff --git a/fee-tracking-subgraph/subgraph.template.yaml b/fee-tracking-subgraph/subgraph.template.yaml new file mode 100644 index 0000000..fd1e686 --- /dev/null +++ b/fee-tracking-subgraph/subgraph.template.yaml @@ -0,0 +1,29 @@ +specVersion: 0.0.4 +schema: + file: ./schema.graphql +dataSources: + - kind: ethereum + name: FeeModule + network: {{ network }} + source: + address: "{{ contracts.FeeModule.address }}" + abi: FeeModule + startBlock: {{ contracts.FeeModule.startBlock }} + mapping: + kind: ethereum/events + apiVersion: 0.0.6 + language: wasm/assemblyscript + entities: + - FeeRefund + - FeeWithdrawal + - GlobalFeeStats + - UserFeeStats + abis: + - name: FeeModule + file: ./abis/FeeModule.json + eventHandlers: + - event: FeeRefunded(indexed bytes32,indexed address,uint256,uint256,uint256) + handler: handleFeeRefunded + - event: FeeWithdrawn(indexed address,indexed address,uint256,uint256) + handler: handleFeeWithdrawn + file: ./src/FeeModuleMapping.ts \ No newline at end of file