From 6391b105e43355bccf883407c4ae793868180019 Mon Sep 17 00:00:00 2001 From: Ton-Chanh Le Date: Fri, 12 Dec 2025 15:26:36 -0500 Subject: [PATCH 01/10] support staking --- abis/uniswap/factory.json | 48 +- abis/v3/DataRegistryImplementation.json | 19 - ...DataPortabilityGranteesImplementation.json | 24 +- ...aPortabilityPermissionsImplementation.json | 19 - .../DataPortabilityServersImplementation.json | 19 - abis/v7/VanaPoolEntityImplementation.json | 1198 +++++++++++++++++ abis/v7/VanaPoolStakingImplementation.json | 1003 ++++++++++++++ config/moksha.json | 4 + config/vana.json | 4 + schema.graphql | 103 ++ src/lib/contract/v7/vana-pool-entity.ts | 207 +++ src/lib/contract/v7/vana-pool-staking.ts | 191 +++ src/mapping.ts.template | 8 + subgraph.moksha.yaml | 66 +- subgraph.vana.yaml | 65 + 15 files changed, 2896 insertions(+), 82 deletions(-) create mode 100644 abis/v7/VanaPoolEntityImplementation.json create mode 100644 abis/v7/VanaPoolStakingImplementation.json create mode 100644 src/lib/contract/v7/vana-pool-entity.ts create mode 100644 src/lib/contract/v7/vana-pool-staking.ts diff --git a/abis/uniswap/factory.json b/abis/uniswap/factory.json index 2503212..63f8e7a 100644 --- a/abis/uniswap/factory.json +++ b/abis/uniswap/factory.json @@ -1,4 +1,9 @@ [ + { + "inputs": [], + "stateMutability": "nonpayable", + "type": "constructor" + }, { "anonymous": false, "inputs": [ @@ -125,7 +130,7 @@ "inputs": [ { "internalType": "uint24", - "name": "fee", + "name": "", "type": "uint24" } ], @@ -144,17 +149,17 @@ "inputs": [ { "internalType": "address", - "name": "tokenA", + "name": "", "type": "address" }, { "internalType": "address", - "name": "tokenB", + "name": "", "type": "address" }, { "internalType": "uint24", - "name": "fee", + "name": "", "type": "uint24" } ], @@ -162,7 +167,7 @@ "outputs": [ { "internalType": "address", - "name": "pool", + "name": "", "type": "address" } ], @@ -182,6 +187,39 @@ "stateMutability": "view", "type": "function" }, + { + "inputs": [], + "name": "parameters", + "outputs": [ + { + "internalType": "address", + "name": "factory", + "type": "address" + }, + { + "internalType": "address", + "name": "token0", + "type": "address" + }, + { + "internalType": "address", + "name": "token1", + "type": "address" + }, + { + "internalType": "uint24", + "name": "fee", + "type": "uint24" + }, + { + "internalType": "int24", + "name": "tickSpacing", + "type": "int24" + } + ], + "stateMutability": "view", + "type": "function" + }, { "inputs": [ { diff --git a/abis/v3/DataRegistryImplementation.json b/abis/v3/DataRegistryImplementation.json index 8466f71..cab0c0e 100644 --- a/abis/v3/DataRegistryImplementation.json +++ b/abis/v3/DataRegistryImplementation.json @@ -1080,25 +1080,6 @@ "stateMutability": "view", "type": "function" }, - { - "inputs": [ - { - "internalType": "bytes[]", - "name": "data", - "type": "bytes[]" - } - ], - "name": "multicall", - "outputs": [ - { - "internalType": "bytes[]", - "name": "results", - "type": "bytes[]" - } - ], - "stateMutability": "nonpayable", - "type": "function" - }, { "inputs": [], "name": "pause", diff --git a/abis/v6/DataPortabilityGranteesImplementation.json b/abis/v6/DataPortabilityGranteesImplementation.json index 01a356f..35b147a 100644 --- a/abis/v6/DataPortabilityGranteesImplementation.json +++ b/abis/v6/DataPortabilityGranteesImplementation.json @@ -108,6 +108,11 @@ "name": "UUPSUnsupportedProxiableUUID", "type": "error" }, + { + "inputs": [], + "name": "UnauthorizedRegistration", + "type": "error" + }, { "inputs": [], "name": "ZeroAddress", @@ -794,25 +799,6 @@ "stateMutability": "view", "type": "function" }, - { - "inputs": [ - { - "internalType": "bytes[]", - "name": "data", - "type": "bytes[]" - } - ], - "name": "multicall", - "outputs": [ - { - "internalType": "bytes[]", - "name": "results", - "type": "bytes[]" - } - ], - "stateMutability": "nonpayable", - "type": "function" - }, { "inputs": [], "name": "pause", diff --git a/abis/v6/DataPortabilityPermissionsImplementation.json b/abis/v6/DataPortabilityPermissionsImplementation.json index b15e69c..db133af 100644 --- a/abis/v6/DataPortabilityPermissionsImplementation.json +++ b/abis/v6/DataPortabilityPermissionsImplementation.json @@ -815,25 +815,6 @@ "stateMutability": "view", "type": "function" }, - { - "inputs": [ - { - "internalType": "bytes[]", - "name": "data", - "type": "bytes[]" - } - ], - "name": "multicall", - "outputs": [ - { - "internalType": "bytes[]", - "name": "results", - "type": "bytes[]" - } - ], - "stateMutability": "nonpayable", - "type": "function" - }, { "inputs": [], "name": "pause", diff --git a/abis/v6/DataPortabilityServersImplementation.json b/abis/v6/DataPortabilityServersImplementation.json index 7943686..17dd9ff 100644 --- a/abis/v6/DataPortabilityServersImplementation.json +++ b/abis/v6/DataPortabilityServersImplementation.json @@ -743,25 +743,6 @@ "stateMutability": "view", "type": "function" }, - { - "inputs": [ - { - "internalType": "bytes[]", - "name": "data", - "type": "bytes[]" - } - ], - "name": "multicall", - "outputs": [ - { - "internalType": "bytes[]", - "name": "results", - "type": "bytes[]" - } - ], - "stateMutability": "nonpayable", - "type": "function" - }, { "inputs": [], "name": "pause", diff --git a/abis/v7/VanaPoolEntityImplementation.json b/abis/v7/VanaPoolEntityImplementation.json new file mode 100644 index 0000000..b8ac324 --- /dev/null +++ b/abis/v7/VanaPoolEntityImplementation.json @@ -0,0 +1,1198 @@ +[ + { + "inputs": [], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "inputs": [], + "name": "AccessControlBadConfirmation", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "account", + "type": "address" + }, + { + "internalType": "bytes32", + "name": "neededRole", + "type": "bytes32" + } + ], + "name": "AccessControlUnauthorizedAccount", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "target", + "type": "address" + } + ], + "name": "AddressEmptyCode", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "implementation", + "type": "address" + } + ], + "name": "ERC1967InvalidImplementation", + "type": "error" + }, + { + "inputs": [], + "name": "ERC1967NonPayable", + "type": "error" + }, + { + "inputs": [], + "name": "EnforcedPause", + "type": "error" + }, + { + "inputs": [], + "name": "EntityNameAlreadyExists", + "type": "error" + }, + { + "inputs": [], + "name": "ExpectedPause", + "type": "error" + }, + { + "inputs": [], + "name": "FailedInnerCall", + "type": "error" + }, + { + "inputs": [], + "name": "InvalidAddress", + "type": "error" + }, + { + "inputs": [], + "name": "InvalidEntityId", + "type": "error" + }, + { + "inputs": [], + "name": "InvalidEntityStatus", + "type": "error" + }, + { + "inputs": [], + "name": "InvalidInitialization", + "type": "error" + }, + { + "inputs": [], + "name": "InvalidName", + "type": "error" + }, + { + "inputs": [], + "name": "InvalidParam", + "type": "error" + }, + { + "inputs": [], + "name": "InvalidRegistrationStake", + "type": "error" + }, + { + "inputs": [], + "name": "NameTooShort", + "type": "error" + }, + { + "inputs": [], + "name": "NotAuthorized", + "type": "error" + }, + { + "inputs": [], + "name": "NotEntityOwner", + "type": "error" + }, + { + "inputs": [], + "name": "NotInitializing", + "type": "error" + }, + { + "inputs": [], + "name": "ReentrancyGuardReentrantCall", + "type": "error" + }, + { + "inputs": [], + "name": "StakersStillPresent", + "type": "error" + }, + { + "inputs": [], + "name": "TransferFailed", + "type": "error" + }, + { + "inputs": [], + "name": "UUPSUnauthorizedCallContext", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "slot", + "type": "bytes32" + } + ], + "name": "UUPSUnsupportedProxiableUUID", + "type": "error" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "uint256", + "name": "entityId", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "address", + "name": "ownerAddress", + "type": "address" + }, + { + "indexed": false, + "internalType": "string", + "name": "name", + "type": "string" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "maxAPY", + "type": "uint256" + } + ], + "name": "EntityCreated", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "uint256", + "name": "entityId", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "newMaxAPY", + "type": "uint256" + } + ], + "name": "EntityMaxAPYUpdated", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "uint256", + "name": "entityId", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "enum IVanaPoolEntity.EntityStatus", + "name": "newStatus", + "type": "uint8" + } + ], + "name": "EntityStatusUpdated", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "uint256", + "name": "entityId", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "address", + "name": "ownerAddress", + "type": "address" + }, + { + "indexed": false, + "internalType": "string", + "name": "name", + "type": "string" + } + ], + "name": "EntityUpdated", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "uint256", + "name": "entityId", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "ForfeitedRewardsReturned", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint64", + "name": "version", + "type": "uint64" + } + ], + "name": "Initialized", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "address", + "name": "account", + "type": "address" + } + ], + "name": "Paused", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "uint256", + "name": "entityId", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "RewardsAdded", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "uint256", + "name": "entityId", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "distributedAmount", + "type": "uint256" + } + ], + "name": "RewardsProcessed", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "bytes32", + "name": "role", + "type": "bytes32" + }, + { + "indexed": true, + "internalType": "bytes32", + "name": "previousAdminRole", + "type": "bytes32" + }, + { + "indexed": true, + "internalType": "bytes32", + "name": "newAdminRole", + "type": "bytes32" + } + ], + "name": "RoleAdminChanged", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "bytes32", + "name": "role", + "type": "bytes32" + }, + { + "indexed": true, + "internalType": "address", + "name": "account", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "sender", + "type": "address" + } + ], + "name": "RoleGranted", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "bytes32", + "name": "role", + "type": "bytes32" + }, + { + "indexed": true, + "internalType": "address", + "name": "account", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "sender", + "type": "address" + } + ], + "name": "RoleRevoked", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "address", + "name": "account", + "type": "address" + } + ], + "name": "Unpaused", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "implementation", + "type": "address" + } + ], + "name": "Upgraded", + "type": "event" + }, + { + "inputs": [], + "name": "DEFAULT_ADMIN_ROLE", + "outputs": [ + { + "internalType": "bytes32", + "name": "", + "type": "bytes32" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "MAINTAINER_ROLE", + "outputs": [ + { + "internalType": "bytes32", + "name": "", + "type": "bytes32" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "UPGRADE_INTERFACE_VERSION", + "outputs": [ + { + "internalType": "string", + "name": "", + "type": "string" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "VANA_POOL_ROLE", + "outputs": [ + { + "internalType": "bytes32", + "name": "", + "type": "bytes32" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "activeEntitiesValues", + "outputs": [ + { + "internalType": "uint256[]", + "name": "", + "type": "uint256[]" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "entityId", + "type": "uint256" + } + ], + "name": "addRewards", + "outputs": [], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "entityId", + "type": "uint256" + } + ], + "name": "calculateContinuousAPYByEntity", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "exponent", + "type": "uint256" + } + ], + "name": "calculateExponential", + "outputs": [ + { + "internalType": "uint256", + "name": "r", + "type": "uint256" + } + ], + "stateMutability": "pure", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "principal", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "apy", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "time", + "type": "uint256" + } + ], + "name": "calculateYield", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "pure", + "type": "function" + }, + { + "inputs": [ + { + "components": [ + { + "internalType": "address", + "name": "ownerAddress", + "type": "address" + }, + { + "internalType": "string", + "name": "name", + "type": "string" + } + ], + "internalType": "struct IVanaPoolEntity.EntityRegistrationInfo", + "name": "entityRegistrationInfo", + "type": "tuple" + } + ], + "name": "createEntity", + "outputs": [], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "entityId", + "type": "uint256" + } + ], + "name": "entities", + "outputs": [ + { + "components": [ + { + "internalType": "uint256", + "name": "entityId", + "type": "uint256" + }, + { + "internalType": "address", + "name": "ownerAddress", + "type": "address" + }, + { + "internalType": "enum IVanaPoolEntity.EntityStatus", + "name": "status", + "type": "uint8" + }, + { + "internalType": "string", + "name": "name", + "type": "string" + }, + { + "internalType": "uint256", + "name": "maxAPY", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "lockedRewardPool", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "activeRewardPool", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "totalShares", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "lastUpdateTimestamp", + "type": "uint256" + } + ], + "internalType": "struct IVanaPoolEntity.EntityInfo", + "name": "", + "type": "tuple" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "entitiesCount", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "string", + "name": "entityName", + "type": "string" + } + ], + "name": "entityByName", + "outputs": [ + { + "components": [ + { + "internalType": "uint256", + "name": "entityId", + "type": "uint256" + }, + { + "internalType": "address", + "name": "ownerAddress", + "type": "address" + }, + { + "internalType": "enum IVanaPoolEntity.EntityStatus", + "name": "status", + "type": "uint8" + }, + { + "internalType": "string", + "name": "name", + "type": "string" + }, + { + "internalType": "uint256", + "name": "maxAPY", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "lockedRewardPool", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "activeRewardPool", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "totalShares", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "lastUpdateTimestamp", + "type": "uint256" + } + ], + "internalType": "struct IVanaPoolEntity.EntityInfo", + "name": "", + "type": "tuple" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "string", + "name": "entityName", + "type": "string" + } + ], + "name": "entityNameToId", + "outputs": [ + { + "internalType": "uint256", + "name": "entityId", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "entityId", + "type": "uint256" + } + ], + "name": "entityShareToVana", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "role", + "type": "bytes32" + } + ], + "name": "getRoleAdmin", + "outputs": [ + { + "internalType": "bytes32", + "name": "", + "type": "bytes32" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "role", + "type": "bytes32" + }, + { + "internalType": "address", + "name": "account", + "type": "address" + } + ], + "name": "grantRole", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "role", + "type": "bytes32" + }, + { + "internalType": "address", + "name": "account", + "type": "address" + } + ], + "name": "hasRole", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "ownerAddress", + "type": "address" + }, + { + "internalType": "address", + "name": "vanaPoolStakingAddress", + "type": "address" + }, + { + "internalType": "uint256", + "name": "initialMinRegistrationStake", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "initialMaxAPYDefault", + "type": "uint256" + } + ], + "name": "initialize", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "maxAPYDefault", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "minRegistrationStake", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "pause", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "paused", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "entityId", + "type": "uint256" + } + ], + "name": "processRewards", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "proxiableUUID", + "outputs": [ + { + "internalType": "bytes32", + "name": "", + "type": "bytes32" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "role", + "type": "bytes32" + }, + { + "internalType": "address", + "name": "callerConfirmation", + "type": "address" + } + ], + "name": "renounceRole", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "entityId", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "returnForfeitedRewards", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "role", + "type": "bytes32" + }, + { + "internalType": "address", + "name": "account", + "type": "address" + } + ], + "name": "revokeRole", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes4", + "name": "interfaceId", + "type": "bytes4" + } + ], + "name": "supportsInterface", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "unpause", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "entityId", + "type": "uint256" + }, + { + "components": [ + { + "internalType": "address", + "name": "ownerAddress", + "type": "address" + }, + { + "internalType": "string", + "name": "name", + "type": "string" + } + ], + "internalType": "struct IVanaPoolEntity.EntityRegistrationInfo", + "name": "entityRegistrationInfo", + "type": "tuple" + } + ], + "name": "updateEntity", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "entityId", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "newMaxAPY", + "type": "uint256" + } + ], + "name": "updateEntityMaxAPY", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "entityId", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "shares", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + }, + { + "internalType": "bool", + "name": "isStake", + "type": "bool" + } + ], + "name": "updateEntityPool", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "newMinRegistrationStake", + "type": "uint256" + } + ], + "name": "updateMinRegistrationStake", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "newVanaPoolStakingAddress", + "type": "address" + } + ], + "name": "updateVanaPool", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "newImplementation", + "type": "address" + }, + { + "internalType": "bytes", + "name": "data", + "type": "bytes" + } + ], + "name": "upgradeToAndCall", + "outputs": [], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [], + "name": "vanaPoolStaking", + "outputs": [ + { + "internalType": "contract IVanaPoolStaking", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "entityId", + "type": "uint256" + } + ], + "name": "vanaToEntityShare", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "version", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "pure", + "type": "function" + } +] \ No newline at end of file diff --git a/abis/v7/VanaPoolStakingImplementation.json b/abis/v7/VanaPoolStakingImplementation.json new file mode 100644 index 0000000..c3e92a5 --- /dev/null +++ b/abis/v7/VanaPoolStakingImplementation.json @@ -0,0 +1,1003 @@ +[ + { + "inputs": [], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "inputs": [], + "name": "AccessControlBadConfirmation", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "account", + "type": "address" + }, + { + "internalType": "bytes32", + "name": "neededRole", + "type": "bytes32" + } + ], + "name": "AccessControlUnauthorizedAccount", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "target", + "type": "address" + } + ], + "name": "AddressEmptyCode", + "type": "error" + }, + { + "inputs": [], + "name": "CannotRemoveRegistrationStake", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "implementation", + "type": "address" + } + ], + "name": "ERC1967InvalidImplementation", + "type": "error" + }, + { + "inputs": [], + "name": "ERC1967NonPayable", + "type": "error" + }, + { + "inputs": [], + "name": "EnforcedPause", + "type": "error" + }, + { + "inputs": [], + "name": "EntityNotActive", + "type": "error" + }, + { + "inputs": [], + "name": "EntityNotFound", + "type": "error" + }, + { + "inputs": [], + "name": "ExpectedPause", + "type": "error" + }, + { + "inputs": [], + "name": "FailedInnerCall", + "type": "error" + }, + { + "inputs": [], + "name": "InsufficientShares", + "type": "error" + }, + { + "inputs": [], + "name": "InsufficientStakeAmount", + "type": "error" + }, + { + "inputs": [], + "name": "InvalidAddress", + "type": "error" + }, + { + "inputs": [], + "name": "InvalidAmount", + "type": "error" + }, + { + "inputs": [], + "name": "InvalidEntity", + "type": "error" + }, + { + "inputs": [], + "name": "InvalidInitialization", + "type": "error" + }, + { + "inputs": [], + "name": "InvalidRecipient", + "type": "error" + }, + { + "inputs": [], + "name": "InvalidSlippage", + "type": "error" + }, + { + "inputs": [], + "name": "NotAuthorized", + "type": "error" + }, + { + "inputs": [], + "name": "NotEntityOwner", + "type": "error" + }, + { + "inputs": [], + "name": "NotInitializing", + "type": "error" + }, + { + "inputs": [], + "name": "ReentrancyGuardReentrantCall", + "type": "error" + }, + { + "inputs": [], + "name": "TransferFailed", + "type": "error" + }, + { + "inputs": [], + "name": "UUPSUnauthorizedCallContext", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "slot", + "type": "bytes32" + } + ], + "name": "UUPSUnsupportedProxiableUUID", + "type": "error" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "uint256", + "name": "entityId", + "type": "uint256" + }, + { + "indexed": true, + "internalType": "address", + "name": "ownerAddress", + "type": "address" + } + ], + "name": "EntityStakeRegistered", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint64", + "name": "version", + "type": "uint64" + } + ], + "name": "Initialized", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint256", + "name": "newMinStake", + "type": "uint256" + } + ], + "name": "MinStakeUpdated", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "address", + "name": "account", + "type": "address" + } + ], + "name": "Paused", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "bytes32", + "name": "role", + "type": "bytes32" + }, + { + "indexed": true, + "internalType": "bytes32", + "name": "previousAdminRole", + "type": "bytes32" + }, + { + "indexed": true, + "internalType": "bytes32", + "name": "newAdminRole", + "type": "bytes32" + } + ], + "name": "RoleAdminChanged", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "bytes32", + "name": "role", + "type": "bytes32" + }, + { + "indexed": true, + "internalType": "address", + "name": "account", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "sender", + "type": "address" + } + ], + "name": "RoleGranted", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "bytes32", + "name": "role", + "type": "bytes32" + }, + { + "indexed": true, + "internalType": "address", + "name": "account", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "sender", + "type": "address" + } + ], + "name": "RoleRevoked", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "uint256", + "name": "entityId", + "type": "uint256" + }, + { + "indexed": true, + "internalType": "address", + "name": "staker", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "sharesIssued", + "type": "uint256" + } + ], + "name": "Staked", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "address", + "name": "account", + "type": "address" + } + ], + "name": "Unpaused", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "uint256", + "name": "entityId", + "type": "uint256" + }, + { + "indexed": true, + "internalType": "address", + "name": "staker", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "sharesBurned", + "type": "uint256" + } + ], + "name": "Unstaked", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "implementation", + "type": "address" + } + ], + "name": "Upgraded", + "type": "event" + }, + { + "inputs": [], + "name": "DEFAULT_ADMIN_ROLE", + "outputs": [ + { + "internalType": "bytes32", + "name": "", + "type": "bytes32" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "MAINTAINER_ROLE", + "outputs": [ + { + "internalType": "bytes32", + "name": "", + "type": "bytes32" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "UPGRADE_INTERFACE_VERSION", + "outputs": [ + { + "internalType": "string", + "name": "", + "type": "string" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "VANA_POOL_ENTITY_ROLE", + "outputs": [ + { + "internalType": "bytes32", + "name": "", + "type": "bytes32" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "index", + "type": "uint256" + } + ], + "name": "activeStakersListAt", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "activeStakersListCount", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "from", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "to", + "type": "uint256" + } + ], + "name": "activeStakersListValues", + "outputs": [ + { + "internalType": "address[]", + "name": "", + "type": "address[]" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "role", + "type": "bytes32" + } + ], + "name": "getRoleAdmin", + "outputs": [ + { + "internalType": "bytes32", + "name": "", + "type": "bytes32" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "role", + "type": "bytes32" + }, + { + "internalType": "address", + "name": "account", + "type": "address" + } + ], + "name": "grantRole", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "role", + "type": "bytes32" + }, + { + "internalType": "address", + "name": "account", + "type": "address" + } + ], + "name": "hasRole", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "index", + "type": "uint256" + } + ], + "name": "inactiveStakersListAt", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "inactiveStakersListCount", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "from", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "to", + "type": "uint256" + } + ], + "name": "inactiveStakersListValues", + "outputs": [ + { + "internalType": "address[]", + "name": "", + "type": "address[]" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "trustedForwarderAddress", + "type": "address" + }, + { + "internalType": "address", + "name": "ownerAddress", + "type": "address" + }, + { + "internalType": "uint256", + "name": "initialMinStake", + "type": "uint256" + } + ], + "name": "initialize", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "forwarder", + "type": "address" + } + ], + "name": "isTrustedForwarder", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "minStakeAmount", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes[]", + "name": "data", + "type": "bytes[]" + } + ], + "name": "multicall", + "outputs": [ + { + "internalType": "bytes[]", + "name": "results", + "type": "bytes[]" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "pause", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "paused", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "proxiableUUID", + "outputs": [ + { + "internalType": "bytes32", + "name": "", + "type": "bytes32" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "entityId", + "type": "uint256" + }, + { + "internalType": "address", + "name": "ownerAddress", + "type": "address" + }, + { + "internalType": "uint256", + "name": "registrationStake", + "type": "uint256" + } + ], + "name": "registerEntityStake", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "role", + "type": "bytes32" + }, + { + "internalType": "address", + "name": "callerConfirmation", + "type": "address" + } + ], + "name": "renounceRole", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "role", + "type": "bytes32" + }, + { + "internalType": "address", + "name": "account", + "type": "address" + } + ], + "name": "revokeRole", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "entityId", + "type": "uint256" + }, + { + "internalType": "address", + "name": "recipient", + "type": "address" + }, + { + "internalType": "uint256", + "name": "shareAmountMin", + "type": "uint256" + } + ], + "name": "stake", + "outputs": [], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "staker", + "type": "address" + }, + { + "internalType": "uint256", + "name": "entityId", + "type": "uint256" + } + ], + "name": "stakerEntities", + "outputs": [ + { + "components": [ + { + "internalType": "uint256", + "name": "shares", + "type": "uint256" + } + ], + "internalType": "struct IVanaPoolStaking.StakerEntity", + "name": "", + "type": "tuple" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes4", + "name": "interfaceId", + "type": "bytes4" + } + ], + "name": "supportsInterface", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "trustedForwarder", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "unpause", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "entityId", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "shareAmount", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "vanaAmountMin", + "type": "uint256" + } + ], + "name": "unstake", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "newMinStake", + "type": "uint256" + } + ], + "name": "updateMinStakeAmount", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "trustedForwarderAddress", + "type": "address" + } + ], + "name": "updateTrustedForwarder", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "newVanaPoolEntityAddress", + "type": "address" + } + ], + "name": "updateVanaPoolEntity", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "newVanaPoolTreasuryAddress", + "type": "address" + } + ], + "name": "updateVanaPoolTreasury", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "newImplementation", + "type": "address" + }, + { + "internalType": "bytes", + "name": "data", + "type": "bytes" + } + ], + "name": "upgradeToAndCall", + "outputs": [], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [], + "name": "vanaPoolEntity", + "outputs": [ + { + "internalType": "contract IVanaPoolEntity", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "vanaPoolTreasury", + "outputs": [ + { + "internalType": "contract IVanaPoolTreasury", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "version", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "pure", + "type": "function" + } +] \ No newline at end of file diff --git a/config/moksha.json b/config/moksha.json index 917ff8e..b464db0 100644 --- a/config/moksha.json +++ b/config/moksha.json @@ -28,6 +28,10 @@ "dataPortabilityPermissions": true, "dataPortabilityServers": true }, + "staking": { + "vanaPoolStaking": true, + "vanaPoolEntity": true + }, "uniswap": { "v3": true } diff --git a/config/vana.json b/config/vana.json index 5c3dcd4..59e02f1 100644 --- a/config/vana.json +++ b/config/vana.json @@ -28,6 +28,10 @@ "dataPortabilityPermissions": true, "dataPortabilityServers": true }, + "staking": { + "vanaPoolStaking": true, + "vanaPoolEntity": true + }, "uniswap": { "v3": true } diff --git a/schema.graphql b/schema.graphql index c24b4e1..6b4b445 100644 --- a/schema.graphql +++ b/schema.graphql @@ -166,6 +166,109 @@ type File @entity(immutable: false) { transactionHash: Bytes! } +# ==================== Staking Entities ==================== + +type StakingEntity @entity(immutable: false) { + "The unique ID of the staking entity, equivalent to the on-chain entityId." + id: ID! + "The owner address of this entity." + owner: Bytes! + "The name of this entity." + name: String! + "Entity status: 0=None, 1=Active, 2=Removed" + status: BigInt! + "Maximum APY for this entity (in basis points, 1% = 100)" + maxAPY: BigInt! + "Locked rewards for this entity awaiting distribution" + lockedRewardPool: BigInt! + "Active rewards available for distribution" + activeRewardPool: BigInt! + "Total shares for this entity" + totalShares: BigInt! + "Timestamp when rewards were last processed" + lastUpdate: BigInt! + "All stakes in this entity" + stakes: [Stake!]! @derivedFrom(field: "entity") + "All stake events for this entity" + stakeEvents: [StakeEvent!]! @derivedFrom(field: "entity") + "All reward events for this entity" + rewardEvents: [RewardEvent!]! @derivedFrom(field: "entity") + "The block number when the entity was created." + createdAtBlock: BigInt! + "The timestamp when the entity was created." + createdAt: BigInt! + "The transaction hash of the entity creation." + createdTxHash: Bytes! +} + +type Stake @entity(immutable: false) { + "Composite ID: staker address - entityId" + id: ID! + "The address of the staker." + staker: Bytes! + "The staking entity." + entity: StakingEntity! + "Current shares owned by staker in this entity." + shares: BigInt! + "Cumulative VANA staked by this staker in this entity." + totalStaked: BigInt! + "Cumulative VANA unstaked by this staker from this entity." + totalUnstaked: BigInt! + "The block number when the stake was first created." + createdAtBlock: BigInt! + "The timestamp when the stake was first created." + createdAt: BigInt! + "The timestamp when the stake was last updated." + updatedAt: BigInt! +} + +type StakeEvent @entity(immutable: true) { + "Composite ID: txHash-logIndex" + id: ID! + "Event type: 'stake' or 'unstake'" + eventType: String! + "The address of the staker." + staker: Bytes! + "The staking entity." + entity: StakingEntity! + "VANA amount staked or unstaked." + amount: BigInt! + "Shares issued or burned." + shares: BigInt! + "The block number of the event." + blockNumber: BigInt! + "The timestamp of the event." + timestamp: BigInt! + "The transaction hash of the event." + txHash: Bytes! +} + +type RewardEvent @entity(immutable: true) { + "Composite ID: txHash-logIndex" + id: ID! + "Event type: 'added' (RewardsAdded), 'processed' (RewardsProcessed), or 'forfeited' (ForfeitedRewardsReturned)" + eventType: String! + "The staking entity." + entity: StakingEntity! + "Amount of rewards added or distributed." + amount: BigInt! + "The block number of the event." + blockNumber: BigInt! + "The timestamp of the event." + timestamp: BigInt! + "The transaction hash of the event." + txHash: Bytes! +} + +type StakingParams @entity(immutable: false) { + "Singleton ID: 'staking-params'" + id: ID! + "Minimum stake amount in wei." + minStakeAmount: BigInt! + "Bonding period in seconds." + bondingPeriod: BigInt! +} + type Grantee @entity(immutable: false) { "The unique ID of the grantee, equivalent to the on-chain granteeId." id: ID! diff --git a/src/lib/contract/v7/vana-pool-entity.ts b/src/lib/contract/v7/vana-pool-entity.ts new file mode 100644 index 0000000..0494300 --- /dev/null +++ b/src/lib/contract/v7/vana-pool-entity.ts @@ -0,0 +1,207 @@ +import { BigInt as GraphBigInt, log } from "@graphprotocol/graph-ts"; +import { + EntityCreated, + EntityUpdated, + EntityStatusUpdated, + EntityMaxAPYUpdated, + RewardsAdded, + RewardsProcessed, + ForfeitedRewardsReturned, +} from "../../../../generated/VanaPoolEntityImplementation/VanaPoolEntityImplementation"; +import { StakingEntity, RewardEvent } from "../../../../generated/schema"; + +// Mirrored from IVanaPoolEntity.EntityStatus +enum EntityStatus { + NONE = 0, + ACTIVE = 1, + REMOVED = 2, +} + +export function handleEntityCreated(event: EntityCreated): void { + log.info("Handling EntityCreated event with transaction hash: {}", [ + event.transaction.hash.toHexString(), + ]); + + const entityId = event.params.entityId.toString(); + + const stakingEntity = new StakingEntity(entityId); + stakingEntity.owner = event.params.ownerAddress; + stakingEntity.name = event.params.name; + stakingEntity.status = GraphBigInt.fromI32(EntityStatus.ACTIVE); + stakingEntity.maxAPY = event.params.maxAPY; + stakingEntity.lockedRewardPool = GraphBigInt.zero(); + stakingEntity.activeRewardPool = GraphBigInt.zero(); + stakingEntity.totalShares = GraphBigInt.zero(); + stakingEntity.lastUpdate = event.block.timestamp; + stakingEntity.createdAt = event.block.timestamp; + stakingEntity.createdAtBlock = event.block.number; + stakingEntity.createdTxHash = event.transaction.hash; + stakingEntity.save(); +} + +export function handleEntityUpdated(event: EntityUpdated): void { + log.info("Handling EntityUpdated event with transaction hash: {}", [ + event.transaction.hash.toHexString(), + ]); + + const entityId = event.params.entityId.toString(); + const stakingEntity = StakingEntity.load(entityId); + + if (stakingEntity == null) { + log.error("StakingEntity not found for EntityUpdated event: {}", [ + entityId, + ]); + return; + } + + stakingEntity.owner = event.params.ownerAddress; + stakingEntity.name = event.params.name; + stakingEntity.save(); +} + +export function handleEntityStatusUpdated(event: EntityStatusUpdated): void { + log.info("Handling EntityStatusUpdated event with transaction hash: {}", [ + event.transaction.hash.toHexString(), + ]); + + const entityId = event.params.entityId.toString(); + const stakingEntity = StakingEntity.load(entityId); + + if (stakingEntity == null) { + log.error("StakingEntity not found for EntityStatusUpdated event: {}", [ + entityId, + ]); + return; + } + + stakingEntity.status = GraphBigInt.fromI32(event.params.newStatus); + stakingEntity.save(); +} + +export function handleEntityMaxAPYUpdated(event: EntityMaxAPYUpdated): void { + log.info("Handling EntityMaxAPYUpdated event with transaction hash: {}", [ + event.transaction.hash.toHexString(), + ]); + + const entityId = event.params.entityId.toString(); + const stakingEntity = StakingEntity.load(entityId); + + if (stakingEntity == null) { + log.error("StakingEntity not found for EntityMaxAPYUpdated event: {}", [ + entityId, + ]); + return; + } + + stakingEntity.maxAPY = event.params.newMaxAPY; + stakingEntity.save(); +} + +export function handleRewardsAdded(event: RewardsAdded): void { + log.info("Handling RewardsAdded event with transaction hash: {}", [ + event.transaction.hash.toHexString(), + ]); + + const entityId = event.params.entityId.toString(); + const amount = event.params.amount; + + const stakingEntity = StakingEntity.load(entityId); + if (stakingEntity == null) { + log.error("StakingEntity not found for RewardsAdded event: {}", [entityId]); + return; + } + + stakingEntity.lockedRewardPool = stakingEntity.lockedRewardPool.plus(amount); + stakingEntity.save(); + + // Create RewardEvent + const rewardEventId = + event.transaction.hash.toHexString() + "-" + event.logIndex.toString(); + const rewardEvent = new RewardEvent(rewardEventId); + rewardEvent.eventType = "added"; + rewardEvent.entity = entityId; + rewardEvent.amount = amount; + rewardEvent.blockNumber = event.block.number; + rewardEvent.timestamp = event.block.timestamp; + rewardEvent.txHash = event.transaction.hash; + rewardEvent.save(); +} + +export function handleRewardsProcessed(event: RewardsProcessed): void { + log.info("Handling RewardsProcessed event with transaction hash: {}", [ + event.transaction.hash.toHexString(), + ]); + + const entityId = event.params.entityId.toString(); + const distributedAmount = event.params.distributedAmount; + + const stakingEntity = StakingEntity.load(entityId); + if (stakingEntity == null) { + log.error("StakingEntity not found for RewardsProcessed event: {}", [ + entityId, + ]); + return; + } + + // Move rewards from locked to active pool + stakingEntity.lockedRewardPool = + stakingEntity.lockedRewardPool.minus(distributedAmount); + stakingEntity.activeRewardPool = + stakingEntity.activeRewardPool.plus(distributedAmount); + stakingEntity.lastUpdate = event.block.timestamp; + stakingEntity.save(); + + // Create RewardEvent + const rewardEventId = + event.transaction.hash.toHexString() + "-" + event.logIndex.toString(); + const rewardEvent = new RewardEvent(rewardEventId); + rewardEvent.eventType = "processed"; + rewardEvent.entity = entityId; + rewardEvent.amount = distributedAmount; + rewardEvent.blockNumber = event.block.number; + rewardEvent.timestamp = event.block.timestamp; + rewardEvent.txHash = event.transaction.hash; + rewardEvent.save(); +} + +export function handleForfeitedRewardsReturned( + event: ForfeitedRewardsReturned, +): void { + log.info( + "Handling ForfeitedRewardsReturned event with transaction hash: {}", + [event.transaction.hash.toHexString()], + ); + + const entityId = event.params.entityId.toString(); + const amount = event.params.amount; + + const stakingEntity = StakingEntity.load(entityId); + if (stakingEntity == null) { + log.error( + "StakingEntity not found for ForfeitedRewardsReturned event: {}", + [entityId], + ); + return; + } + + // When a user unstakes during bonding period, they only receive their principal (vanaToReturn). + // The contract deducts the full shareValue from activeRewardPool via updateEntityPool. + // The forfeited portion (shareValue - vanaToReturn) is then added back to lockedRewardPool. + // In handleUnstaked, we deducted vanaToReturn from activeRewardPool. + // Here we deduct the remaining forfeitedRewards from activeRewardPool and add to lockedRewardPool. + stakingEntity.activeRewardPool = stakingEntity.activeRewardPool.minus(amount); + stakingEntity.lockedRewardPool = stakingEntity.lockedRewardPool.plus(amount); + stakingEntity.save(); + + // Create RewardEvent + const rewardEventId = + event.transaction.hash.toHexString() + "-" + event.logIndex.toString(); + const rewardEvent = new RewardEvent(rewardEventId); + rewardEvent.eventType = "forfeited"; + rewardEvent.entity = entityId; + rewardEvent.amount = amount; + rewardEvent.blockNumber = event.block.number; + rewardEvent.timestamp = event.block.timestamp; + rewardEvent.txHash = event.transaction.hash; + rewardEvent.save(); +} diff --git a/src/lib/contract/v7/vana-pool-staking.ts b/src/lib/contract/v7/vana-pool-staking.ts new file mode 100644 index 0000000..657a479 --- /dev/null +++ b/src/lib/contract/v7/vana-pool-staking.ts @@ -0,0 +1,191 @@ +import { BigInt as GraphBigInt, Bytes, log } from "@graphprotocol/graph-ts"; +import { + Staked, + Unstaked, + MinStakeUpdated, + EntityStakeRegistered, +} from "../../../../generated/VanaPoolStakingImplementation/VanaPoolStakingImplementation"; +import { + StakingEntity, + Stake, + StakeEvent, + StakingParams, +} from "../../../../generated/schema"; + +const STAKING_PARAMS_ID = "staking-params"; + +function getOrCreateStakingParams(): StakingParams { + let params = StakingParams.load(STAKING_PARAMS_ID); + if (params == null) { + params = new StakingParams(STAKING_PARAMS_ID); + params.minStakeAmount = GraphBigInt.zero(); + params.bondingPeriod = GraphBigInt.zero(); + params.save(); + } + return params; +} + +function getOrCreateStake( + staker: Bytes, + entityId: string, + timestamp: GraphBigInt, + blockNumber: GraphBigInt, +): Stake { + const stakeId = staker.toHexString() + "-" + entityId; + let stake = Stake.load(stakeId); + if (stake == null) { + stake = new Stake(stakeId); + stake.staker = staker; + stake.entity = entityId; + stake.shares = GraphBigInt.zero(); + stake.totalStaked = GraphBigInt.zero(); + stake.totalUnstaked = GraphBigInt.zero(); + stake.createdAt = timestamp; + stake.createdAtBlock = blockNumber; + stake.updatedAt = timestamp; + stake.save(); + } + return stake; +} + +export function handleStaked(event: Staked): void { + log.info("Handling Staked event with transaction hash: {}", [ + event.transaction.hash.toHexString(), + ]); + + const entityId = event.params.entityId.toString(); + const staker = event.params.staker; + const amount = event.params.amount; + const sharesIssued = event.params.sharesIssued; + + // Update StakingEntity + const stakingEntity = StakingEntity.load(entityId); + if (stakingEntity == null) { + log.error("StakingEntity not found for Staked event: {}", [entityId]); + return; + } + + stakingEntity.totalShares = stakingEntity.totalShares.plus(sharesIssued); + stakingEntity.activeRewardPool = stakingEntity.activeRewardPool.plus(amount); + stakingEntity.lastUpdate = event.block.timestamp; + stakingEntity.save(); + + // Update or create Stake + const stake = getOrCreateStake( + staker, + entityId, + event.block.timestamp, + event.block.number, + ); + stake.shares = stake.shares.plus(sharesIssued); + stake.totalStaked = stake.totalStaked.plus(amount); + stake.updatedAt = event.block.timestamp; + stake.save(); + + // Create StakeEvent + const stakeEventId = + event.transaction.hash.toHexString() + "-" + event.logIndex.toString(); + const stakeEvent = new StakeEvent(stakeEventId); + stakeEvent.eventType = "stake"; + stakeEvent.staker = staker; + stakeEvent.entity = entityId; + stakeEvent.amount = amount; + stakeEvent.shares = sharesIssued; + stakeEvent.blockNumber = event.block.number; + stakeEvent.timestamp = event.block.timestamp; + stakeEvent.txHash = event.transaction.hash; + stakeEvent.save(); +} + +export function handleUnstaked(event: Unstaked): void { + log.info("Handling Unstaked event with transaction hash: {}", [ + event.transaction.hash.toHexString(), + ]); + + const entityId = event.params.entityId.toString(); + const staker = event.params.staker; + const amount = event.params.amount; // vanaToReturn - the actual amount user receives + const sharesBurned = event.params.sharesBurned; + + // Update StakingEntity + // The contract deducts shareValue from activeRewardPool via updateEntityPool. + // shareValue = vanaToReturn + forfeitedRewards + // We handle this in two parts: + // 1. Here: activeRewardPool -= vanaToReturn (amount from this event) + // 2. In handleForfeitedRewardsReturned: activeRewardPool -= forfeitedRewards + const stakingEntity = StakingEntity.load(entityId); + if (stakingEntity == null) { + log.error("StakingEntity not found for Unstaked event: {}", [entityId]); + return; + } + + stakingEntity.totalShares = stakingEntity.totalShares.minus(sharesBurned); + stakingEntity.activeRewardPool = stakingEntity.activeRewardPool.minus(amount); + stakingEntity.lastUpdate = event.block.timestamp; + stakingEntity.save(); + + // Update Stake + const stakeId = staker.toHexString() + "-" + entityId; + const stake = Stake.load(stakeId); + if (stake == null) { + log.error("Stake not found for Unstaked event: {}", [stakeId]); + return; + } + + stake.shares = stake.shares.minus(sharesBurned); + stake.totalUnstaked = stake.totalUnstaked.plus(amount); + stake.updatedAt = event.block.timestamp; + stake.save(); + + // Create StakeEvent + const stakeEventId = + event.transaction.hash.toHexString() + "-" + event.logIndex.toString(); + const stakeEvent = new StakeEvent(stakeEventId); + stakeEvent.eventType = "unstake"; + stakeEvent.staker = staker; + stakeEvent.entity = entityId; + stakeEvent.amount = amount; + stakeEvent.shares = sharesBurned; + stakeEvent.blockNumber = event.block.number; + stakeEvent.timestamp = event.block.timestamp; + stakeEvent.txHash = event.transaction.hash; + stakeEvent.save(); +} + +export function handleMinStakeUpdated(event: MinStakeUpdated): void { + log.info("Handling MinStakeUpdated event with transaction hash: {}", [ + event.transaction.hash.toHexString(), + ]); + + const params = getOrCreateStakingParams(); + params.minStakeAmount = event.params.newMinStake; + params.save(); +} + +export function handleEntityStakeRegistered( + event: EntityStakeRegistered, +): void { + log.info("Handling EntityStakeRegistered event with transaction hash: {}", [ + event.transaction.hash.toHexString(), + ]); + + // This event is emitted when an entity is created and the owner's registration stake is recorded + // The StakingEntity should already exist from EntityCreated event + // This handler can be used to track the initial stake registration if needed + const entityId = event.params.entityId.toString(); + const ownerAddress = event.params.ownerAddress; + + const stakingEntity = StakingEntity.load(entityId); + if (stakingEntity == null) { + log.warning( + "StakingEntity not found for EntityStakeRegistered event: {}. This is expected if EntityCreated hasn't been processed yet.", + [entityId], + ); + return; + } + + log.info("EntityStakeRegistered processed for entity {} with owner {}", [ + entityId, + ownerAddress.toHexString(), + ]); +} diff --git a/src/mapping.ts.template b/src/mapping.ts.template index 5bb4b9f..dd28fc2 100644 --- a/src/mapping.ts.template +++ b/src/mapping.ts.template @@ -97,6 +97,14 @@ export * from "./lib/contract/v6/data-portability-permissions"; export * from "./lib/contract/v6/data-portability-servers"; {{/includes.v6.dataPortabilityServers}} +// Staking +{{#includes.staking.vanaPoolStaking}} +export * from "./lib/contract/v7/vana-pool-staking"; +{{/includes.staking.vanaPoolStaking}} +{{#includes.staking.vanaPoolEntity}} +export * from "./lib/contract/v7/vana-pool-entity"; +{{/includes.staking.vanaPoolEntity}} + //uniswap {{#includes.uniswap.v3}} export * from "./uniswap/v3/mappings/factory"; diff --git a/subgraph.moksha.yaml b/subgraph.moksha.yaml index 623189a..feaee02 100644 --- a/subgraph.moksha.yaml +++ b/subgraph.moksha.yaml @@ -342,8 +342,72 @@ dataSources: handler: handleSchemaAdded file: ./src/mapping.ts +# Staking + - kind: ethereum/contract + name: VanaPoolStakingImplementation + network: moksha + source: + address: "0x641C18E2F286c86f96CE95C8ec1EB9fC0415Ca0e" + abi: VanaPoolStakingImplementation + startBlock: 2101757 + mapping: + kind: ethereum/events + apiVersion: 0.0.6 + language: wasm/assemblyscript + entities: + - StakingEntity + - Stake + - StakeEvent + - StakingParams + abis: + - name: VanaPoolStakingImplementation + file: ./abis/v7/VanaPoolStakingImplementation.json + eventHandlers: + - event: Staked(indexed uint256,indexed address,uint256,uint256) + handler: handleStaked + - event: Unstaked(indexed uint256,indexed address,uint256,uint256) + handler: handleUnstaked + - event: MinStakeUpdated(uint256) + handler: handleMinStakeUpdated + - event: EntityStakeRegistered(indexed uint256,indexed address) + handler: handleEntityStakeRegistered + file: ./src/mapping.ts + + - kind: ethereum/contract + name: VanaPoolEntityImplementation + network: moksha + source: + address: "0x44f20490A82e1f1F1cC25Dd3BA8647034eDdce30" + abi: VanaPoolEntityImplementation + startBlock: 2101776 + mapping: + kind: ethereum/events + apiVersion: 0.0.6 + language: wasm/assemblyscript + entities: + - StakingEntity + - RewardEvent + abis: + - name: VanaPoolEntityImplementation + file: ./abis/v7/VanaPoolEntityImplementation.json + eventHandlers: + - event: EntityCreated(indexed uint256,address,string,uint256) + handler: handleEntityCreated + - event: EntityUpdated(indexed uint256,address,string) + handler: handleEntityUpdated + - event: EntityStatusUpdated(indexed uint256,uint8) + handler: handleEntityStatusUpdated + - event: EntityMaxAPYUpdated(indexed uint256,uint256) + handler: handleEntityMaxAPYUpdated + - event: RewardsAdded(indexed uint256,uint256) + handler: handleRewardsAdded + - event: RewardsProcessed(indexed uint256,uint256) + handler: handleRewardsProcessed + - event: ForfeitedRewardsReturned(indexed uint256,uint256) + handler: handleForfeitedRewardsReturned + file: ./src/mapping.ts -# unuiswap +# uniswap - kind: ethereum/contract name: Factory network: moksha diff --git a/subgraph.vana.yaml b/subgraph.vana.yaml index f9188af..1667d2d 100644 --- a/subgraph.vana.yaml +++ b/subgraph.vana.yaml @@ -341,6 +341,71 @@ dataSources: handler: handleSchemaAdded file: ./src/mapping.ts + # v7 - Staking + - kind: ethereum/contract + name: VanaPoolStakingImplementation + network: vana + source: + address: "0x641C18E2F286c86f96CE95C8ec1EB9fC0415Ca0e" + abi: VanaPoolStakingImplementation + startBlock: 2442348 # + mapping: + kind: ethereum/events + apiVersion: 0.0.6 + language: wasm/assemblyscript + entities: + - StakingEntity + - Stake + - StakeEvent + - StakingParams + abis: + - name: VanaPoolStakingImplementation + file: ./abis/v7/VanaPoolStakingImplementation.json + eventHandlers: + - event: Staked(indexed uint256,indexed address,uint256,uint256) + handler: handleStaked + - event: Unstaked(indexed uint256,indexed address,uint256,uint256) + handler: handleUnstaked + - event: MinStakeUpdated(uint256) + handler: handleMinStakeUpdated + - event: EntityStakeRegistered(indexed uint256,indexed address) + handler: handleEntityStakeRegistered + file: ./src/mapping.ts + + - kind: ethereum/contract + name: VanaPoolEntityImplementation + network: vana + source: + address: "0x44f20490A82e1f1F1cC25Dd3BA8647034eDdce30" + abi: VanaPoolEntityImplementation + startBlock: 2442427 + mapping: + kind: ethereum/events + apiVersion: 0.0.6 + language: wasm/assemblyscript + entities: + - StakingEntity + - RewardEvent + abis: + - name: VanaPoolEntityImplementation + file: ./abis/v7/VanaPoolEntityImplementation.json + eventHandlers: + - event: EntityCreated(indexed uint256,address,string,uint256) + handler: handleEntityCreated + - event: EntityUpdated(indexed uint256,address,string) + handler: handleEntityUpdated + - event: EntityStatusUpdated(indexed uint256,uint8) + handler: handleEntityStatusUpdated + - event: EntityMaxAPYUpdated(indexed uint256,uint256) + handler: handleEntityMaxAPYUpdated + - event: RewardsAdded(indexed uint256,uint256) + handler: handleRewardsAdded + - event: RewardsProcessed(indexed uint256,uint256) + handler: handleRewardsProcessed + - event: ForfeitedRewardsReturned(indexed uint256,uint256) + handler: handleForfeitedRewardsReturned + file: ./src/mapping.ts + - kind: ethereum/contract name: Factory network: vana From 43c2b4617fdaeed8b5979896b85d37acfcc0daa3 Mon Sep 17 00:00:00 2001 From: Ton-Chanh Le Date: Sat, 13 Dec 2025 08:17:22 -0500 Subject: [PATCH 02/10] fixed totalScore in DLP performance --- config/moksha.json | 2 +- config/vana.json | 2 +- schema.graphql | 2 +- src/mapping.ts.template | 10 +++++----- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/config/moksha.json b/config/moksha.json index b464db0..2198a09 100644 --- a/config/moksha.json +++ b/config/moksha.json @@ -28,7 +28,7 @@ "dataPortabilityPermissions": true, "dataPortabilityServers": true }, - "staking": { + "v7": { "vanaPoolStaking": true, "vanaPoolEntity": true }, diff --git a/config/vana.json b/config/vana.json index 59e02f1..8e59a61 100644 --- a/config/vana.json +++ b/config/vana.json @@ -28,7 +28,7 @@ "dataPortabilityPermissions": true, "dataPortabilityServers": true }, - "staking": { + "v7": { "vanaPoolStaking": true, "vanaPoolEntity": true }, diff --git a/schema.graphql b/schema.graphql index 6b4b445..60c4a10 100644 --- a/schema.graphql +++ b/schema.graphql @@ -136,7 +136,7 @@ type DlpPerformance @entity(immutable: false) { id: ID! dlp: Dlp! epoch: Epoch! - totalScore: BigInt! + totalScore: BigInt tradingVolume: BigInt! uniqueContributors: BigInt! dataAccessFees: BigInt! diff --git a/src/mapping.ts.template b/src/mapping.ts.template index dd28fc2..4c05676 100644 --- a/src/mapping.ts.template +++ b/src/mapping.ts.template @@ -97,13 +97,13 @@ export * from "./lib/contract/v6/data-portability-permissions"; export * from "./lib/contract/v6/data-portability-servers"; {{/includes.v6.dataPortabilityServers}} -// Staking -{{#includes.staking.vanaPoolStaking}} +// V7 +{{#includes.v7.vanaPoolStaking}} export * from "./lib/contract/v7/vana-pool-staking"; -{{/includes.staking.vanaPoolStaking}} -{{#includes.staking.vanaPoolEntity}} +{{/includes.v7.vanaPoolStaking}} +{{#includes.v7.vanaPoolEntity}} export * from "./lib/contract/v7/vana-pool-entity"; -{{/includes.staking.vanaPoolEntity}} +{{/includes.v7.vanaPoolEntity}} //uniswap {{#includes.uniswap.v3}} From 46958cf32f202a0c7d00ab28db0c401fc5b44673 Mon Sep 17 00:00:00 2001 From: Ton-Chanh Le Date: Sat, 13 Dec 2025 08:18:16 -0500 Subject: [PATCH 03/10] added unit tests for staking --- .../unit/contract/v7/utils/staking-events.ts | 272 +++++++++++++ .../contract/v7/vana-pool-staking.test.ts | 382 ++++++++++++++++++ 2 files changed, 654 insertions(+) create mode 100644 tests/unit/contract/v7/utils/staking-events.ts create mode 100644 tests/unit/contract/v7/vana-pool-staking.test.ts diff --git a/tests/unit/contract/v7/utils/staking-events.ts b/tests/unit/contract/v7/utils/staking-events.ts new file mode 100644 index 0000000..4c17d84 --- /dev/null +++ b/tests/unit/contract/v7/utils/staking-events.ts @@ -0,0 +1,272 @@ +import { + Address, + ethereum, + BigInt as GraphBigInt, +} from "@graphprotocol/graph-ts"; +import { newMockEvent } from "matchstick-as/assembly/index"; +import { + EntityCreated, + EntityUpdated, + EntityStatusUpdated, + EntityMaxAPYUpdated, + RewardsAdded, + RewardsProcessed, + ForfeitedRewardsReturned, +} from "../../../../../generated/VanaPoolEntityImplementation/VanaPoolEntityImplementation"; +import { + Staked, + Unstaked, + MinStakeUpdated, + EntityStakeRegistered, +} from "../../../../../generated/VanaPoolStakingImplementation/VanaPoolStakingImplementation"; + +// VanaPoolEntity events +export function createEntityCreatedEvent( + entityId: number, + ownerAddress: string, + name: string, + maxAPY: GraphBigInt +): EntityCreated { + const event = changetype(newMockEvent()); + + event.parameters = [ + new ethereum.EventParam( + "entityId", + ethereum.Value.fromUnsignedBigInt(GraphBigInt.fromI32(entityId)) + ), + new ethereum.EventParam( + "ownerAddress", + ethereum.Value.fromAddress(Address.fromString(ownerAddress)) + ), + new ethereum.EventParam("name", ethereum.Value.fromString(name)), + new ethereum.EventParam( + "maxAPY", + ethereum.Value.fromUnsignedBigInt(maxAPY) + ), + ]; + + return event; +} + +export function createEntityUpdatedEvent( + entityId: number, + ownerAddress: string, + name: string +): EntityUpdated { + const event = changetype(newMockEvent()); + + event.parameters = [ + new ethereum.EventParam( + "entityId", + ethereum.Value.fromUnsignedBigInt(GraphBigInt.fromI32(entityId)) + ), + new ethereum.EventParam( + "ownerAddress", + ethereum.Value.fromAddress(Address.fromString(ownerAddress)) + ), + new ethereum.EventParam("name", ethereum.Value.fromString(name)), + ]; + + return event; +} + +export function createEntityStatusUpdatedEvent( + entityId: number, + newStatus: number +): EntityStatusUpdated { + const event = changetype(newMockEvent()); + + event.parameters = [ + new ethereum.EventParam( + "entityId", + ethereum.Value.fromUnsignedBigInt(GraphBigInt.fromI32(entityId)) + ), + new ethereum.EventParam( + "newStatus", + ethereum.Value.fromI32(newStatus) + ), + ]; + + return event; +} + +export function createEntityMaxAPYUpdatedEvent( + entityId: number, + newMaxAPY: GraphBigInt +): EntityMaxAPYUpdated { + const event = changetype(newMockEvent()); + + event.parameters = [ + new ethereum.EventParam( + "entityId", + ethereum.Value.fromUnsignedBigInt(GraphBigInt.fromI32(entityId)) + ), + new ethereum.EventParam( + "newMaxAPY", + ethereum.Value.fromUnsignedBigInt(newMaxAPY) + ), + ]; + + return event; +} + +export function createRewardsAddedEvent( + entityId: number, + amount: GraphBigInt +): RewardsAdded { + const event = changetype(newMockEvent()); + event.logIndex = GraphBigInt.fromI32(0); + + event.parameters = [ + new ethereum.EventParam( + "entityId", + ethereum.Value.fromUnsignedBigInt(GraphBigInt.fromI32(entityId)) + ), + new ethereum.EventParam( + "amount", + ethereum.Value.fromUnsignedBigInt(amount) + ), + ]; + + return event; +} + +export function createRewardsProcessedEvent( + entityId: number, + distributedAmount: GraphBigInt +): RewardsProcessed { + const event = changetype(newMockEvent()); + event.logIndex = GraphBigInt.fromI32(1); + + event.parameters = [ + new ethereum.EventParam( + "entityId", + ethereum.Value.fromUnsignedBigInt(GraphBigInt.fromI32(entityId)) + ), + new ethereum.EventParam( + "distributedAmount", + ethereum.Value.fromUnsignedBigInt(distributedAmount) + ), + ]; + + return event; +} + +export function createForfeitedRewardsReturnedEvent( + entityId: number, + amount: GraphBigInt +): ForfeitedRewardsReturned { + const event = changetype(newMockEvent()); + event.logIndex = GraphBigInt.fromI32(2); + + event.parameters = [ + new ethereum.EventParam( + "entityId", + ethereum.Value.fromUnsignedBigInt(GraphBigInt.fromI32(entityId)) + ), + new ethereum.EventParam( + "amount", + ethereum.Value.fromUnsignedBigInt(amount) + ), + ]; + + return event; +} + +// VanaPoolStaking events +export function createStakedEvent( + entityId: number, + staker: string, + amount: GraphBigInt, + sharesIssued: GraphBigInt +): Staked { + const event = changetype(newMockEvent()); + event.logIndex = GraphBigInt.fromI32(0); + + event.parameters = [ + new ethereum.EventParam( + "entityId", + ethereum.Value.fromUnsignedBigInt(GraphBigInt.fromI32(entityId)) + ), + new ethereum.EventParam( + "staker", + ethereum.Value.fromAddress(Address.fromString(staker)) + ), + new ethereum.EventParam( + "amount", + ethereum.Value.fromUnsignedBigInt(amount) + ), + new ethereum.EventParam( + "sharesIssued", + ethereum.Value.fromUnsignedBigInt(sharesIssued) + ), + ]; + + return event; +} + +export function createUnstakedEvent( + entityId: number, + staker: string, + amount: GraphBigInt, + sharesBurned: GraphBigInt +): Unstaked { + const event = changetype(newMockEvent()); + event.logIndex = GraphBigInt.fromI32(1); + + event.parameters = [ + new ethereum.EventParam( + "entityId", + ethereum.Value.fromUnsignedBigInt(GraphBigInt.fromI32(entityId)) + ), + new ethereum.EventParam( + "staker", + ethereum.Value.fromAddress(Address.fromString(staker)) + ), + new ethereum.EventParam( + "amount", + ethereum.Value.fromUnsignedBigInt(amount) + ), + new ethereum.EventParam( + "sharesBurned", + ethereum.Value.fromUnsignedBigInt(sharesBurned) + ), + ]; + + return event; +} + +export function createMinStakeUpdatedEvent( + newMinStake: GraphBigInt +): MinStakeUpdated { + const event = changetype(newMockEvent()); + + event.parameters = [ + new ethereum.EventParam( + "newMinStake", + ethereum.Value.fromUnsignedBigInt(newMinStake) + ), + ]; + + return event; +} + +export function createEntityStakeRegisteredEvent( + entityId: i32, + ownerAddress: string +): EntityStakeRegistered { + const event = changetype(newMockEvent()); + + event.parameters = [ + new ethereum.EventParam( + "entityId", + ethereum.Value.fromUnsignedBigInt(GraphBigInt.fromI32(entityId)) + ), + new ethereum.EventParam( + "ownerAddress", + ethereum.Value.fromAddress(Address.fromString(ownerAddress)) + ), + ]; + + return event; +} diff --git a/tests/unit/contract/v7/vana-pool-staking.test.ts b/tests/unit/contract/v7/vana-pool-staking.test.ts new file mode 100644 index 0000000..8310a28 --- /dev/null +++ b/tests/unit/contract/v7/vana-pool-staking.test.ts @@ -0,0 +1,382 @@ +import { + assert, + beforeEach, + clearStore, + describe, + test, +} from "matchstick-as/assembly/index"; +import { BigInt as GraphBigInt, Address } from "@graphprotocol/graph-ts"; +import { + handleEntityCreated, + handleEntityUpdated, + handleEntityStatusUpdated, + handleEntityMaxAPYUpdated, + handleRewardsAdded, + handleRewardsProcessed, + handleForfeitedRewardsReturned, +} from "../../../../src/lib/contract/v7/vana-pool-entity"; +import { + handleStaked, + handleUnstaked, + handleMinStakeUpdated, + handleEntityStakeRegistered, +} from "../../../../src/lib/contract/v7/vana-pool-staking"; +import { + StakingEntity, + Stake, + StakingParams, +} from "../../../../generated/schema"; +import { + createEntityCreatedEvent, + createEntityUpdatedEvent, + createEntityStatusUpdatedEvent, + createEntityMaxAPYUpdatedEvent, + createRewardsAddedEvent, + createRewardsProcessedEvent, + createForfeitedRewardsReturnedEvent, + createStakedEvent, + createUnstakedEvent, + createMinStakeUpdatedEvent, + createEntityStakeRegisteredEvent, +} from "./utils/staking-events"; + +// Helper to create a staking entity for tests +function createTestStakingEntity(entityId: number): void { + const ownerAddress = "0x1111111111111111111111111111111111111111"; + const event = createEntityCreatedEvent( + entityId, + ownerAddress, + "Test Entity", + GraphBigInt.fromI32(1000) + ); + handleEntityCreated(event); +} + +// Clear the store before each test +beforeEach(() => { + clearStore(); +}); + +describe("VanaPoolEntity - handleEntityCreated", () => { + test("creates a new staking entity", () => { + const entityId = 1; + const ownerAddress = "0x1111111111111111111111111111111111111111"; + const name = "Test DLP"; + const maxAPY = GraphBigInt.fromI32(5000); // 50% + + const event = createEntityCreatedEvent(entityId, ownerAddress, name, maxAPY); + handleEntityCreated(event); + + const entity = StakingEntity.load(entityId.toString()); + assert.assertNotNull(entity); + assert.bytesEquals(entity!.owner, Address.fromString(ownerAddress)); + assert.stringEquals(entity!.name, name); + assert.bigIntEquals(entity!.maxAPY, maxAPY); + assert.bigIntEquals(entity!.status, GraphBigInt.fromI32(1)); // ACTIVE + assert.bigIntEquals(entity!.lockedRewardPool, GraphBigInt.zero()); + assert.bigIntEquals(entity!.activeRewardPool, GraphBigInt.zero()); + assert.bigIntEquals(entity!.totalShares, GraphBigInt.zero()); + }); +}); + +describe("VanaPoolEntity - handleEntityUpdated", () => { + test("updates an existing staking entity", () => { + createTestStakingEntity(1); + + const newOwner = "0x2222222222222222222222222222222222222222"; + const newName = "Updated Entity"; + + const event = createEntityUpdatedEvent(1, newOwner, newName); + handleEntityUpdated(event); + + const entity = StakingEntity.load("1"); + assert.assertNotNull(entity); + assert.bytesEquals(entity!.owner, Address.fromString(newOwner)); + assert.stringEquals(entity!.name, newName); + }); + + test("handles update for non-existent entity gracefully", () => { + const event = createEntityUpdatedEvent( + 999, + "0x1111111111111111111111111111111111111111", + "Non-existent" + ); + handleEntityUpdated(event); + + // Should not create an entity + assert.assertNull(StakingEntity.load("999")); + }); +}); + +describe("VanaPoolEntity - handleEntityStatusUpdated", () => { + test("updates entity status to REMOVED", () => { + createTestStakingEntity(1); + + const event = createEntityStatusUpdatedEvent(1, 2); // REMOVED = 2 + handleEntityStatusUpdated(event); + + const entity = StakingEntity.load("1"); + assert.assertNotNull(entity); + assert.bigIntEquals(entity!.status, GraphBigInt.fromI32(2)); + }); +}); + +describe("VanaPoolEntity - handleEntityMaxAPYUpdated", () => { + test("updates entity max APY", () => { + createTestStakingEntity(1); + + const newMaxAPY = GraphBigInt.fromI32(7500); // 75% + const event = createEntityMaxAPYUpdatedEvent(1, newMaxAPY); + handleEntityMaxAPYUpdated(event); + + const entity = StakingEntity.load("1"); + assert.assertNotNull(entity); + assert.bigIntEquals(entity!.maxAPY, newMaxAPY); + }); +}); + +describe("VanaPoolEntity - handleRewardsAdded", () => { + test("adds rewards to locked pool", () => { + createTestStakingEntity(1); + + const amount = GraphBigInt.fromString("1000000000000000000"); // 1 ETH + const event = createRewardsAddedEvent(1, amount); + handleRewardsAdded(event); + + const entity = StakingEntity.load("1"); + assert.assertNotNull(entity); + assert.bigIntEquals(entity!.lockedRewardPool, amount); + }); +}); + +describe("VanaPoolEntity - handleRewardsProcessed", () => { + test("moves rewards from locked to active pool", () => { + createTestStakingEntity(1); + + // First add some rewards to locked pool + const lockedAmount = GraphBigInt.fromString("1000000000000000000"); + const addEvent = createRewardsAddedEvent(1, lockedAmount); + handleRewardsAdded(addEvent); + + // Then process some rewards + const processedAmount = GraphBigInt.fromString("500000000000000000"); + const processEvent = createRewardsProcessedEvent(1, processedAmount); + handleRewardsProcessed(processEvent); + + const entity = StakingEntity.load("1"); + assert.assertNotNull(entity); + assert.bigIntEquals( + entity!.lockedRewardPool, + lockedAmount.minus(processedAmount) + ); + assert.bigIntEquals(entity!.activeRewardPool, processedAmount); + }); +}); + +describe("VanaPoolEntity - handleForfeitedRewardsReturned", () => { + test("moves forfeited rewards from active to locked pool", () => { + createTestStakingEntity(1); + + // Setup: Add rewards and process them to active pool + const initialAmount = GraphBigInt.fromString("1000000000000000000"); + const addEvent = createRewardsAddedEvent(1, initialAmount); + handleRewardsAdded(addEvent); + + const processEvent = createRewardsProcessedEvent(1, initialAmount); + handleRewardsProcessed(processEvent); + + // Verify active pool has the rewards + let entity = StakingEntity.load("1"); + assert.bigIntEquals(entity!.activeRewardPool, initialAmount); + assert.bigIntEquals(entity!.lockedRewardPool, GraphBigInt.zero()); + + // Now handle forfeited rewards + const forfeitedAmount = GraphBigInt.fromString("200000000000000000"); + const forfeitEvent = createForfeitedRewardsReturnedEvent(1, forfeitedAmount); + handleForfeitedRewardsReturned(forfeitEvent); + + entity = StakingEntity.load("1"); + assert.assertNotNull(entity); + assert.bigIntEquals( + entity!.activeRewardPool, + initialAmount.minus(forfeitedAmount) + ); + assert.bigIntEquals(entity!.lockedRewardPool, forfeitedAmount); + }); +}); + +describe("VanaPoolStaking - handleStaked", () => { + test("creates stake and updates entity pool", () => { + createTestStakingEntity(1); + + const staker = "0x3333333333333333333333333333333333333333"; + const amount = GraphBigInt.fromString("1000000000000000000"); + const sharesIssued = GraphBigInt.fromString("1000000000000000000"); + + const event = createStakedEvent(1, staker, amount, sharesIssued); + handleStaked(event); + + // Check StakingEntity was updated + const entity = StakingEntity.load("1"); + assert.assertNotNull(entity); + assert.bigIntEquals(entity!.totalShares, sharesIssued); + assert.bigIntEquals(entity!.activeRewardPool, amount); + + // Check Stake was created + const stakeId = staker.toLowerCase() + "-1"; + const stake = Stake.load(stakeId); + assert.assertNotNull(stake); + assert.bigIntEquals(stake!.shares, sharesIssued); + assert.bigIntEquals(stake!.totalStaked, amount); + assert.bigIntEquals(stake!.totalUnstaked, GraphBigInt.zero()); + }); + + test("accumulates stakes for same staker", () => { + createTestStakingEntity(1); + + const staker = "0x3333333333333333333333333333333333333333"; + const amount1 = GraphBigInt.fromString("1000000000000000000"); + const shares1 = GraphBigInt.fromString("1000000000000000000"); + + const event1 = createStakedEvent(1, staker, amount1, shares1); + handleStaked(event1); + + // Second stake + const amount2 = GraphBigInt.fromString("500000000000000000"); + const shares2 = GraphBigInt.fromString("400000000000000000"); // Different ratio + + const event2 = createStakedEvent(1, staker, amount2, shares2); + handleStaked(event2); + + // Check Stake was accumulated + const stakeId = staker.toLowerCase() + "-1"; + const stake = Stake.load(stakeId); + assert.assertNotNull(stake); + assert.bigIntEquals(stake!.shares, shares1.plus(shares2)); + assert.bigIntEquals(stake!.totalStaked, amount1.plus(amount2)); + + // Check entity totals + const entity = StakingEntity.load("1"); + assert.bigIntEquals(entity!.totalShares, shares1.plus(shares2)); + assert.bigIntEquals(entity!.activeRewardPool, amount1.plus(amount2)); + }); +}); + +describe("VanaPoolStaking - handleUnstaked", () => { + test("reduces stake and updates entity pool", () => { + createTestStakingEntity(1); + + const staker = "0x3333333333333333333333333333333333333333"; + const stakeAmount = GraphBigInt.fromString("1000000000000000000"); + const stakeShares = GraphBigInt.fromString("1000000000000000000"); + + // First stake + const stakeEvent = createStakedEvent(1, staker, stakeAmount, stakeShares); + handleStaked(stakeEvent); + + // Then unstake part of it + const unstakeAmount = GraphBigInt.fromString("400000000000000000"); + const unstakeShares = GraphBigInt.fromString("400000000000000000"); + + const unstakeEvent = createUnstakedEvent(1, staker, unstakeAmount, unstakeShares); + handleUnstaked(unstakeEvent); + + // Check StakingEntity was updated + const entity = StakingEntity.load("1"); + assert.assertNotNull(entity); + assert.bigIntEquals(entity!.totalShares, stakeShares.minus(unstakeShares)); + assert.bigIntEquals(entity!.activeRewardPool, stakeAmount.minus(unstakeAmount)); + + // Check Stake was updated + const stakeId = staker.toLowerCase() + "-1"; + const stake = Stake.load(stakeId); + assert.assertNotNull(stake); + assert.bigIntEquals(stake!.shares, stakeShares.minus(unstakeShares)); + assert.bigIntEquals(stake!.totalUnstaked, unstakeAmount); + }); +}); + +describe("VanaPoolStaking - handleMinStakeUpdated", () => { + test("creates or updates staking params", () => { + const newMinStake = GraphBigInt.fromString("100000000000000000000"); // 100 VANA + + const event = createMinStakeUpdatedEvent(newMinStake); + handleMinStakeUpdated(event); + + const params = StakingParams.load("staking-params"); + assert.assertNotNull(params); + assert.bigIntEquals(params!.minStakeAmount, newMinStake); + }); +}); + +describe("VanaPoolStaking - handleEntityStakeRegistered", () => { + test("logs entity stake registration for existing entity", () => { + createTestStakingEntity(1); + + const ownerAddress = "0x1111111111111111111111111111111111111111"; + const event = createEntityStakeRegisteredEvent(1, ownerAddress); + + // Should not throw, just log + handleEntityStakeRegistered(event); + + // Entity should still exist + const entity = StakingEntity.load("1"); + assert.assertNotNull(entity); + }); +}); + +describe("Integration - Staking with Forfeited Rewards", () => { + test("full unstake during bonding period flow", () => { + createTestStakingEntity(1); + + const staker = "0x3333333333333333333333333333333333333333"; + + // 1. Add and process rewards + const rewardAmount = GraphBigInt.fromString("1000000000000000000"); + const addEvent = createRewardsAddedEvent(1, rewardAmount); + handleRewardsAdded(addEvent); + + const processEvent = createRewardsProcessedEvent(1, rewardAmount); + handleRewardsProcessed(processEvent); + + // 2. User stakes + const stakeAmount = GraphBigInt.fromString("500000000000000000"); + const stakeShares = GraphBigInt.fromString("500000000000000000"); + const stakeEvent = createStakedEvent(1, staker, stakeAmount, stakeShares); + handleStaked(stakeEvent); + + // Entity should have: activeRewardPool = 1000 + 500 = 1500 + let entity = StakingEntity.load("1"); + assert.bigIntEquals( + entity!.activeRewardPool, + rewardAmount.plus(stakeAmount) + ); + + // 3. User unstakes during bonding period + // They only get vanaToReturn (principal), forfeited rewards go back + const vanaToReturn = GraphBigInt.fromString("500000000000000000"); + const sharesBurned = GraphBigInt.fromString("500000000000000000"); + const unstakeEvent = createUnstakedEvent(1, staker, vanaToReturn, sharesBurned); + handleUnstaked(unstakeEvent); + + // 4. Forfeited rewards are returned to locked pool + const forfeitedAmount = GraphBigInt.fromString("100000000000000000"); + const forfeitEvent = createForfeitedRewardsReturnedEvent(1, forfeitedAmount); + handleForfeitedRewardsReturned(forfeitEvent); + + // Final state check + entity = StakingEntity.load("1"); + + // activeRewardPool = 1500 - 500 (unstake vanaToReturn) - 100 (forfeited) = 900 + assert.bigIntEquals( + entity!.activeRewardPool, + rewardAmount.plus(stakeAmount).minus(vanaToReturn).minus(forfeitedAmount) + ); + + // lockedRewardPool = 100 (forfeited) + assert.bigIntEquals(entity!.lockedRewardPool, forfeitedAmount); + + // totalShares = 0 + assert.bigIntEquals(entity!.totalShares, GraphBigInt.zero()); + }); +}); From 1a4c8db51db9ef4c10dc66591ebe8a89b58f98c6 Mon Sep 17 00:00:00 2001 From: Ton-Chanh Le Date: Sat, 13 Dec 2025 22:33:21 -0500 Subject: [PATCH 04/10] fixed Permission entity --- schema.graphql | 2 -- src/lib/contract/v6/data-portability-permissions.ts | 4 +--- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/schema.graphql b/schema.graphql index 60c4a10..7aa3b93 100644 --- a/schema.graphql +++ b/schema.graphql @@ -299,8 +299,6 @@ type Permission @entity(immutable: false) { grant: String! "The nonce used for this permission grant." nonce: BigInt! - "The signature provided by the user." - signature: Bytes! "The block number when the permission starts." startBlock: BigInt! "The block number when the permission ends (null if no end)." diff --git a/src/lib/contract/v6/data-portability-permissions.ts b/src/lib/contract/v6/data-portability-permissions.ts index 230f210..c1872ca 100644 --- a/src/lib/contract/v6/data-portability-permissions.ts +++ b/src/lib/contract/v6/data-portability-permissions.ts @@ -2,7 +2,6 @@ import { log, store, BigInt as GraphBigInt, - Bytes, } from "@graphprotocol/graph-ts"; import { PermissionAdded, @@ -53,11 +52,10 @@ export function handlePermissionAdded(event: PermissionAdded): void { permission.endBlock = permissionData.value.endBlock; } else { log.warning( - "Could not get permission data for id {}. Nonce and signature will be zero.", + "Could not get permission data for id {}. Nonce will be zero.", [permissionId], ); permission.nonce = GraphBigInt.zero(); - permission.signature = new Bytes(0); permission.startBlock = event.block.number; permission.endBlock = null; } From ac73ec43021df121b35372490f1203dfc568c8a8 Mon Sep 17 00:00:00 2001 From: Ton-Chanh Le Date: Mon, 15 Dec 2025 09:52:45 -0500 Subject: [PATCH 05/10] add total distributed rewards --- schema.graphql | 2 ++ src/lib/contract/v7/vana-pool-entity.ts | 7 +++++++ 2 files changed, 9 insertions(+) diff --git a/schema.graphql b/schema.graphql index 7aa3b93..3e5f56d 100644 --- a/schema.graphql +++ b/schema.graphql @@ -183,6 +183,8 @@ type StakingEntity @entity(immutable: false) { lockedRewardPool: BigInt! "Active rewards available for distribution" activeRewardPool: BigInt! + "Total rewards distributed to stakers (cumulative sum of all RewardsProcessed events)" + totalDistributedRewards: BigInt! "Total shares for this entity" totalShares: BigInt! "Timestamp when rewards were last processed" diff --git a/src/lib/contract/v7/vana-pool-entity.ts b/src/lib/contract/v7/vana-pool-entity.ts index 0494300..057d081 100644 --- a/src/lib/contract/v7/vana-pool-entity.ts +++ b/src/lib/contract/v7/vana-pool-entity.ts @@ -31,6 +31,7 @@ export function handleEntityCreated(event: EntityCreated): void { stakingEntity.maxAPY = event.params.maxAPY; stakingEntity.lockedRewardPool = GraphBigInt.zero(); stakingEntity.activeRewardPool = GraphBigInt.zero(); + stakingEntity.totalDistributedRewards = GraphBigInt.zero(); stakingEntity.totalShares = GraphBigInt.zero(); stakingEntity.lastUpdate = event.block.timestamp; stakingEntity.createdAt = event.block.timestamp; @@ -148,6 +149,9 @@ export function handleRewardsProcessed(event: RewardsProcessed): void { stakingEntity.lockedRewardPool.minus(distributedAmount); stakingEntity.activeRewardPool = stakingEntity.activeRewardPool.plus(distributedAmount); + // Track cumulative distributed rewards + stakingEntity.totalDistributedRewards = + stakingEntity.totalDistributedRewards.plus(distributedAmount); stakingEntity.lastUpdate = event.block.timestamp; stakingEntity.save(); @@ -191,6 +195,9 @@ export function handleForfeitedRewardsReturned( // Here we deduct the remaining forfeitedRewards from activeRewardPool and add to lockedRewardPool. stakingEntity.activeRewardPool = stakingEntity.activeRewardPool.minus(amount); stakingEntity.lockedRewardPool = stakingEntity.lockedRewardPool.plus(amount); + // Forfeited rewards were previously counted as distributed, so subtract them + stakingEntity.totalDistributedRewards = + stakingEntity.totalDistributedRewards.minus(amount); stakingEntity.save(); // Create RewardEvent From 65c5254bd1505d9872e9a695cd3c95afa3f6a350 Mon Sep 17 00:00:00 2001 From: Ton-Chanh Le Date: Mon, 15 Dec 2025 10:01:47 -0500 Subject: [PATCH 06/10] added tests for total distributed rewards --- .../contract/v7/vana-pool-staking.test.ts | 51 +++++++++++++++++-- 1 file changed, 47 insertions(+), 4 deletions(-) diff --git a/tests/unit/contract/v7/vana-pool-staking.test.ts b/tests/unit/contract/v7/vana-pool-staking.test.ts index 8310a28..1e07498 100644 --- a/tests/unit/contract/v7/vana-pool-staking.test.ts +++ b/tests/unit/contract/v7/vana-pool-staking.test.ts @@ -75,6 +75,7 @@ describe("VanaPoolEntity - handleEntityCreated", () => { assert.bigIntEquals(entity!.status, GraphBigInt.fromI32(1)); // ACTIVE assert.bigIntEquals(entity!.lockedRewardPool, GraphBigInt.zero()); assert.bigIntEquals(entity!.activeRewardPool, GraphBigInt.zero()); + assert.bigIntEquals(entity!.totalDistributedRewards, GraphBigInt.zero()); assert.bigIntEquals(entity!.totalShares, GraphBigInt.zero()); }); }); @@ -150,7 +151,7 @@ describe("VanaPoolEntity - handleRewardsAdded", () => { }); describe("VanaPoolEntity - handleRewardsProcessed", () => { - test("moves rewards from locked to active pool", () => { + test("moves rewards from locked to active pool and updates totalDistributedRewards", () => { createTestStakingEntity(1); // First add some rewards to locked pool @@ -170,11 +171,37 @@ describe("VanaPoolEntity - handleRewardsProcessed", () => { lockedAmount.minus(processedAmount) ); assert.bigIntEquals(entity!.activeRewardPool, processedAmount); + assert.bigIntEquals(entity!.totalDistributedRewards, processedAmount); + }); + + test("accumulates totalDistributedRewards across multiple process events", () => { + createTestStakingEntity(1); + + // Add rewards to locked pool + const lockedAmount = GraphBigInt.fromString("1000000000000000000"); + const addEvent = createRewardsAddedEvent(1, lockedAmount); + handleRewardsAdded(addEvent); + + // Process rewards in two batches + const firstProcessed = GraphBigInt.fromString("300000000000000000"); + const processEvent1 = createRewardsProcessedEvent(1, firstProcessed); + handleRewardsProcessed(processEvent1); + + const secondProcessed = GraphBigInt.fromString("400000000000000000"); + const processEvent2 = createRewardsProcessedEvent(1, secondProcessed); + handleRewardsProcessed(processEvent2); + + const entity = StakingEntity.load("1"); + assert.assertNotNull(entity); + assert.bigIntEquals( + entity!.totalDistributedRewards, + firstProcessed.plus(secondProcessed) + ); }); }); describe("VanaPoolEntity - handleForfeitedRewardsReturned", () => { - test("moves forfeited rewards from active to locked pool", () => { + test("moves forfeited rewards from active to locked pool and decrements totalDistributedRewards", () => { createTestStakingEntity(1); // Setup: Add rewards and process them to active pool @@ -185,10 +212,11 @@ describe("VanaPoolEntity - handleForfeitedRewardsReturned", () => { const processEvent = createRewardsProcessedEvent(1, initialAmount); handleRewardsProcessed(processEvent); - // Verify active pool has the rewards + // Verify active pool has the rewards and totalDistributedRewards is set let entity = StakingEntity.load("1"); assert.bigIntEquals(entity!.activeRewardPool, initialAmount); assert.bigIntEquals(entity!.lockedRewardPool, GraphBigInt.zero()); + assert.bigIntEquals(entity!.totalDistributedRewards, initialAmount); // Now handle forfeited rewards const forfeitedAmount = GraphBigInt.fromString("200000000000000000"); @@ -202,6 +230,11 @@ describe("VanaPoolEntity - handleForfeitedRewardsReturned", () => { initialAmount.minus(forfeitedAmount) ); assert.bigIntEquals(entity!.lockedRewardPool, forfeitedAmount); + // totalDistributedRewards should be decremented by forfeited amount + assert.bigIntEquals( + entity!.totalDistributedRewards, + initialAmount.minus(forfeitedAmount) + ); }); }); @@ -339,6 +372,10 @@ describe("Integration - Staking with Forfeited Rewards", () => { const processEvent = createRewardsProcessedEvent(1, rewardAmount); handleRewardsProcessed(processEvent); + // Verify totalDistributedRewards after processing + let entity = StakingEntity.load("1"); + assert.bigIntEquals(entity!.totalDistributedRewards, rewardAmount); + // 2. User stakes const stakeAmount = GraphBigInt.fromString("500000000000000000"); const stakeShares = GraphBigInt.fromString("500000000000000000"); @@ -346,7 +383,7 @@ describe("Integration - Staking with Forfeited Rewards", () => { handleStaked(stakeEvent); // Entity should have: activeRewardPool = 1000 + 500 = 1500 - let entity = StakingEntity.load("1"); + entity = StakingEntity.load("1"); assert.bigIntEquals( entity!.activeRewardPool, rewardAmount.plus(stakeAmount) @@ -378,5 +415,11 @@ describe("Integration - Staking with Forfeited Rewards", () => { // totalShares = 0 assert.bigIntEquals(entity!.totalShares, GraphBigInt.zero()); + + // totalDistributedRewards = 1000 (processed) - 100 (forfeited) = 900 + assert.bigIntEquals( + entity!.totalDistributedRewards, + rewardAmount.minus(forfeitedAmount) + ); }); }); From 4755901f70737b7b8f5af97992d86962cdede64e Mon Sep 17 00:00:00 2001 From: Ton-Chanh Le Date: Mon, 15 Dec 2025 12:29:07 -0500 Subject: [PATCH 07/10] handled nondeterministic event order --- src/lib/contract/v7/vana-pool-entity.ts | 26 +++- src/lib/contract/v7/vana-pool-staking.ts | 61 ++++++++- .../contract/v7/vana-pool-staking.test.ts | 118 ++++++++++++++++++ 3 files changed, 194 insertions(+), 11 deletions(-) diff --git a/src/lib/contract/v7/vana-pool-entity.ts b/src/lib/contract/v7/vana-pool-entity.ts index 057d081..e6ae4fa 100644 --- a/src/lib/contract/v7/vana-pool-entity.ts +++ b/src/lib/contract/v7/vana-pool-entity.ts @@ -24,15 +24,31 @@ export function handleEntityCreated(event: EntityCreated): void { const entityId = event.params.entityId.toString(); - const stakingEntity = new StakingEntity(entityId); + // Check if entity already exists (created as placeholder by handleStaked) + let stakingEntity = StakingEntity.load(entityId); + if (stakingEntity == null) { + stakingEntity = new StakingEntity(entityId); + stakingEntity.lockedRewardPool = GraphBigInt.zero(); + stakingEntity.activeRewardPool = GraphBigInt.zero(); + stakingEntity.totalDistributedRewards = GraphBigInt.zero(); + stakingEntity.totalShares = GraphBigInt.zero(); + } else { + // Entity exists as placeholder - preserve accumulated values from Staked events + log.info( + "EntityCreated: Merging with existing placeholder entity {}. Current totalShares: {}, activeRewardPool: {}", + [ + entityId, + stakingEntity.totalShares.toString(), + stakingEntity.activeRewardPool.toString(), + ], + ); + } + + // Update with actual entity data from the event stakingEntity.owner = event.params.ownerAddress; stakingEntity.name = event.params.name; stakingEntity.status = GraphBigInt.fromI32(EntityStatus.ACTIVE); stakingEntity.maxAPY = event.params.maxAPY; - stakingEntity.lockedRewardPool = GraphBigInt.zero(); - stakingEntity.activeRewardPool = GraphBigInt.zero(); - stakingEntity.totalDistributedRewards = GraphBigInt.zero(); - stakingEntity.totalShares = GraphBigInt.zero(); stakingEntity.lastUpdate = event.block.timestamp; stakingEntity.createdAt = event.block.timestamp; stakingEntity.createdAtBlock = event.block.number; diff --git a/src/lib/contract/v7/vana-pool-staking.ts b/src/lib/contract/v7/vana-pool-staking.ts index 657a479..feca288 100644 --- a/src/lib/contract/v7/vana-pool-staking.ts +++ b/src/lib/contract/v7/vana-pool-staking.ts @@ -14,6 +14,54 @@ import { const STAKING_PARAMS_ID = "staking-params"; +// Mirrored from IVanaPoolEntity.EntityStatus +enum EntityStatus { + NONE = 0, + ACTIVE = 1, + REMOVED = 2, +} + +/** + * Get or create a StakingEntity. This handles the case where Staked events + * from VanaPoolStakingImplementation are processed before EntityCreated events + * from VanaPoolEntityImplementation within the same transaction (e.g., during createEntity). + * + * The placeholder entity will have default values that will be properly populated + * when handleEntityCreated processes the EntityCreated event. + */ +export function getOrCreateStakingEntity( + entityId: string, + timestamp: GraphBigInt, + blockNumber: GraphBigInt, + txHash: Bytes, +): StakingEntity { + let stakingEntity = StakingEntity.load(entityId); + if (stakingEntity == null) { + log.warning( + "Creating placeholder StakingEntity {} - EntityCreated event not yet processed", + [entityId], + ); + stakingEntity = new StakingEntity(entityId); + // Set placeholder values - these will be updated by handleEntityCreated + stakingEntity.owner = Bytes.fromHexString( + "0x0000000000000000000000000000000000000000", + ); + stakingEntity.name = ""; + stakingEntity.status = GraphBigInt.fromI32(EntityStatus.ACTIVE); + stakingEntity.maxAPY = GraphBigInt.zero(); + stakingEntity.lockedRewardPool = GraphBigInt.zero(); + stakingEntity.activeRewardPool = GraphBigInt.zero(); + stakingEntity.totalDistributedRewards = GraphBigInt.zero(); + stakingEntity.totalShares = GraphBigInt.zero(); + stakingEntity.lastUpdate = timestamp; + stakingEntity.createdAt = timestamp; + stakingEntity.createdAtBlock = blockNumber; + stakingEntity.createdTxHash = txHash; + stakingEntity.save(); + } + return stakingEntity; +} + function getOrCreateStakingParams(): StakingParams { let params = StakingParams.load(STAKING_PARAMS_ID); if (params == null) { @@ -58,12 +106,13 @@ export function handleStaked(event: Staked): void { const amount = event.params.amount; const sharesIssued = event.params.sharesIssued; - // Update StakingEntity - const stakingEntity = StakingEntity.load(entityId); - if (stakingEntity == null) { - log.error("StakingEntity not found for Staked event: {}", [entityId]); - return; - } + // Get or create StakingEntity - handles case where Staked is processed before EntityCreated + const stakingEntity = getOrCreateStakingEntity( + entityId, + event.block.timestamp, + event.block.number, + event.transaction.hash, + ); stakingEntity.totalShares = stakingEntity.totalShares.plus(sharesIssued); stakingEntity.activeRewardPool = stakingEntity.activeRewardPool.plus(amount); diff --git a/tests/unit/contract/v7/vana-pool-staking.test.ts b/tests/unit/contract/v7/vana-pool-staking.test.ts index 1e07498..0d52c3b 100644 --- a/tests/unit/contract/v7/vana-pool-staking.test.ts +++ b/tests/unit/contract/v7/vana-pool-staking.test.ts @@ -423,3 +423,121 @@ describe("Integration - Staking with Forfeited Rewards", () => { ); }); }); + +describe("Out-of-Order Event Processing - Staked before EntityCreated", () => { + test("handleStaked creates placeholder entity when EntityCreated not yet processed", () => { + // Simulate Staked event being processed BEFORE EntityCreated + // This happens when both events are in the same transaction but from different contracts + const entityId = 1; + const staker = "0x3333333333333333333333333333333333333333"; + const amount = GraphBigInt.fromString("1000000000000000000"); + const sharesIssued = GraphBigInt.fromString("1000000000000000000"); + + // Process Staked event first (without EntityCreated) + const stakeEvent = createStakedEvent(entityId, staker, amount, sharesIssued); + handleStaked(stakeEvent); + + // Verify placeholder entity was created + const entity = StakingEntity.load(entityId.toString()); + assert.assertNotNull(entity); + // Placeholder has zero address for owner and empty name + assert.bytesEquals( + entity!.owner, + Address.fromString("0x0000000000000000000000000000000000000000") + ); + assert.stringEquals(entity!.name, ""); + // But totalShares and activeRewardPool should be updated + assert.bigIntEquals(entity!.totalShares, sharesIssued); + assert.bigIntEquals(entity!.activeRewardPool, amount); + }); + + test("handleEntityCreated merges with existing placeholder entity", () => { + const entityId = 1; + const staker = "0x3333333333333333333333333333333333333333"; + const ownerAddress = "0x1111111111111111111111111111111111111111"; + const entityName = "Test DLP"; + const maxAPY = GraphBigInt.fromI32(5000); + const stakeAmount = GraphBigInt.fromString("1000000000000000000"); + const sharesIssued = GraphBigInt.fromString("1000000000000000000"); + + // Step 1: Process Staked event first (simulating out-of-order processing) + const stakeEvent = createStakedEvent(entityId, staker, stakeAmount, sharesIssued); + handleStaked(stakeEvent); + + // Verify placeholder was created with stake data + let entity = StakingEntity.load(entityId.toString()); + assert.assertNotNull(entity); + assert.bigIntEquals(entity!.totalShares, sharesIssued); + assert.bigIntEquals(entity!.activeRewardPool, stakeAmount); + + // Step 2: Process EntityCreated event (should merge, not overwrite) + const entityCreatedEvent = createEntityCreatedEvent( + entityId, + ownerAddress, + entityName, + maxAPY + ); + handleEntityCreated(entityCreatedEvent); + + // Verify entity has proper owner and name from EntityCreated + entity = StakingEntity.load(entityId.toString()); + assert.assertNotNull(entity); + assert.bytesEquals(entity!.owner, Address.fromString(ownerAddress)); + assert.stringEquals(entity!.name, entityName); + assert.bigIntEquals(entity!.maxAPY, maxAPY); + + // CRITICAL: Verify that totalShares and activeRewardPool were PRESERVED + assert.bigIntEquals(entity!.totalShares, sharesIssued); + assert.bigIntEquals(entity!.activeRewardPool, stakeAmount); + }); + + test("multiple Staked events before EntityCreated accumulate correctly", () => { + const entityId = 1; + const staker1 = "0x3333333333333333333333333333333333333333"; + const staker2 = "0x4444444444444444444444444444444444444444"; + const ownerAddress = "0x1111111111111111111111111111111111111111"; + + const amount1 = GraphBigInt.fromString("1000000000000000000"); + const shares1 = GraphBigInt.fromString("1000000000000000000"); + const amount2 = GraphBigInt.fromString("500000000000000000"); + const shares2 = GraphBigInt.fromString("500000000000000000"); + + // Process multiple Staked events before EntityCreated + const stakeEvent1 = createStakedEvent(entityId, staker1, amount1, shares1); + handleStaked(stakeEvent1); + + const stakeEvent2 = createStakedEvent(entityId, staker2, amount2, shares2); + handleStaked(stakeEvent2); + + // Verify accumulated values + let entity = StakingEntity.load(entityId.toString()); + assert.bigIntEquals(entity!.totalShares, shares1.plus(shares2)); + assert.bigIntEquals(entity!.activeRewardPool, amount1.plus(amount2)); + + // Now process EntityCreated + const entityCreatedEvent = createEntityCreatedEvent( + entityId, + ownerAddress, + "Test DLP", + GraphBigInt.fromI32(5000) + ); + handleEntityCreated(entityCreatedEvent); + + // Verify accumulated values were preserved + entity = StakingEntity.load(entityId.toString()); + assert.bytesEquals(entity!.owner, Address.fromString(ownerAddress)); + assert.bigIntEquals(entity!.totalShares, shares1.plus(shares2)); + assert.bigIntEquals(entity!.activeRewardPool, amount1.plus(amount2)); + + // Verify individual stakes were created correctly + const stake1Id = staker1.toLowerCase() + "-1"; + const stake1 = Stake.load(stake1Id); + assert.assertNotNull(stake1); + assert.bigIntEquals(stake1!.shares, shares1); + + const stake2Id = staker2.toLowerCase() + "-1"; + const stake2 = Stake.load(stake2Id); + assert.assertNotNull(stake2); + assert.bigIntEquals(stake2!.shares, shares2); + }); +}); From cf727d55cb9bca3ada0621c2f338399a56202b7b Mon Sep 17 00:00:00 2001 From: Ton-Chanh Le Date: Mon, 15 Dec 2025 12:36:03 -0500 Subject: [PATCH 08/10] staking-only --- config/moksha-v7-only.json | 46 +++++ scripts/compare-staking-events.mjs | 287 ++++++++++++++++++++++++++++ scripts/compare-staking-events.ts | 293 +++++++++++++++++++++++++++++ subgraph.moksha-v7-only.yaml | 72 +++++++ 4 files changed, 698 insertions(+) create mode 100644 config/moksha-v7-only.json create mode 100644 scripts/compare-staking-events.mjs create mode 100644 scripts/compare-staking-events.ts create mode 100644 subgraph.moksha-v7-only.yaml diff --git a/config/moksha-v7-only.json b/config/moksha-v7-only.json new file mode 100644 index 0000000..518b2c8 --- /dev/null +++ b/config/moksha-v7-only.json @@ -0,0 +1,46 @@ +{ + "network": "moksha", + "includes": { + "v1": { + "dataRegistry": false + }, + "v2": { + "dataRegistry": false + }, + "v3": { + "dataRegistry": false + }, + "v4": { + "queryEngine": false, + "dataRefinerRegistry": false + }, + "v5": { + "dlpRegistry": false, + "vanaEpoch": false, + "dlpPerformance": false, + "queryEngine": false, + "dataRefinerRegistry": false, + "dataPermission": false + }, + "v6": { + "dataRefinerRegistry": false, + "dataPortabilityGrantees": false, + "dataPortabilityPermissions": false, + "dataPortabilityServers": false + }, + "v7": { + "vanaPoolStaking": true, + "vanaPoolEntity": true + }, + "uniswap": { + "v3": false + } + }, + "initParams": { + "daySize": "600", + "epochSize": "91", + "epochRewardAmount": "10000000000000000000" + }, + "epochRanges": [], + "dlps": [] +} diff --git a/scripts/compare-staking-events.mjs b/scripts/compare-staking-events.mjs new file mode 100644 index 0000000..bc8d8a2 --- /dev/null +++ b/scripts/compare-staking-events.mjs @@ -0,0 +1,287 @@ +import { ethers } from "ethers"; + +const SUBGRAPH_URL = + "https://api.goldsky.com/api/public/project_cm168cz887zva010j39il7a6p/subgraphs/moksha/staking/gn"; +const RPC_URL = + "https://falling-tame-liquid.vana-moksha.quiknode.pro/522e4e45df28df82a4d7729726e68cdc7d630011"; +const STAKING_CONTRACT = "0x641C18E2F286c86f96CE95C8ec1EB9fC0415Ca0e"; +const START_BLOCK = 2101757; + +// ABI for Staked and Unstaked events +const STAKING_ABI = [ + "event Staked(uint256 indexed entityId, address indexed staker, uint256 amount, uint256 sharesIssued)", + "event Unstaked(uint256 indexed entityId, address indexed staker, uint256 amount, uint256 sharesBurned)", +]; + +async function fetchSubgraphEvents() { + const allEvents = []; + let skip = 0; + const first = 1000; + + while (true) { + const query = ` + query { + stakeEvents( + first: ${first} + skip: ${skip} + orderBy: blockNumber + orderDirection: asc + ) { + id + eventType + staker + entity { + id + } + amount + shares + blockNumber + txHash + } + } + `; + + const response = await fetch(SUBGRAPH_URL, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ query }), + }); + + const json = await response.json(); + const events = json.data?.stakeEvents || []; + + if (events.length === 0) break; + + allEvents.push(...events); + skip += first; + + if (events.length < first) break; + } + + return allEvents; +} + +async function fetchChainEvents() { + const provider = new ethers.JsonRpcProvider(RPC_URL); + const contract = new ethers.Contract(STAKING_CONTRACT, STAKING_ABI, provider); + + const latestBlock = await provider.getBlockNumber(); + console.log(`Fetching events from block ${START_BLOCK} to ${latestBlock}`); + + const allEvents = []; + + // Fetch in chunks to avoid RPC limits + const chunkSize = 10000; + for (let fromBlock = START_BLOCK; fromBlock <= latestBlock; fromBlock += chunkSize) { + const toBlock = Math.min(fromBlock + chunkSize - 1, latestBlock); + + // Fetch Staked events + const stakedFilter = contract.filters.Staked(); + const stakedEvents = await contract.queryFilter(stakedFilter, fromBlock, toBlock); + + for (const event of stakedEvents) { + allEvents.push({ + eventType: "stake", + entityId: event.args[0].toString(), + staker: event.args[1].toLowerCase(), + amount: event.args[2].toString(), + shares: event.args[3].toString(), + blockNumber: event.blockNumber, + txHash: event.transactionHash, + logIndex: event.index, + }); + } + + // Fetch Unstaked events + const unstakedFilter = contract.filters.Unstaked(); + const unstakedEvents = await contract.queryFilter(unstakedFilter, fromBlock, toBlock); + + for (const event of unstakedEvents) { + allEvents.push({ + eventType: "unstake", + entityId: event.args[0].toString(), + staker: event.args[1].toLowerCase(), + amount: event.args[2].toString(), + shares: event.args[3].toString(), + blockNumber: event.blockNumber, + txHash: event.transactionHash, + logIndex: event.index, + }); + } + + console.log(`Processed blocks ${fromBlock} - ${toBlock}`); + } + + // Sort by block number and log index + allEvents.sort((a, b) => { + if (a.blockNumber !== b.blockNumber) return a.blockNumber - b.blockNumber; + return a.logIndex - b.logIndex; + }); + + return allEvents; +} + +function compareEvents(subgraphEvents, chainEvents) { + console.log("\n========== COMPARISON RESULTS ==========\n"); + console.log(`Subgraph events: ${subgraphEvents.length}`); + console.log(`Chain events: ${chainEvents.length}`); + + // Create maps for quick lookup + const subgraphMap = new Map(); + for (const event of subgraphEvents) { + // Key: txHash-eventType-entityId-staker + const key = `${event.txHash.toLowerCase()}-${event.eventType}-${event.entity.id}-${event.staker.toLowerCase()}`; + subgraphMap.set(key, event); + } + + const chainMap = new Map(); + for (const event of chainEvents) { + const key = `${event.txHash.toLowerCase()}-${event.eventType}-${event.entityId}-${event.staker.toLowerCase()}`; + chainMap.set(key, event); + } + + // Find events in chain but not in subgraph + const missingInSubgraph = []; + for (const event of chainEvents) { + const key = `${event.txHash.toLowerCase()}-${event.eventType}-${event.entityId}-${event.staker.toLowerCase()}`; + if (!subgraphMap.has(key)) { + missingInSubgraph.push(event); + } + } + + // Find events in subgraph but not in chain + const missingInChain = []; + for (const event of subgraphEvents) { + const key = `${event.txHash.toLowerCase()}-${event.eventType}-${event.entity.id}-${event.staker.toLowerCase()}`; + if (!chainMap.has(key)) { + missingInChain.push(event); + } + } + + // Find events with mismatched amounts/shares + const mismatched = []; + for (const event of chainEvents) { + const key = `${event.txHash.toLowerCase()}-${event.eventType}-${event.entityId}-${event.staker.toLowerCase()}`; + const subgraphEvent = subgraphMap.get(key); + if (subgraphEvent) { + if ( + subgraphEvent.amount !== event.amount || + subgraphEvent.shares !== event.shares + ) { + mismatched.push({ subgraph: subgraphEvent, chain: event }); + } + } + } + + console.log(`\n----- Missing in Subgraph (${missingInSubgraph.length}) -----`); + for (const event of missingInSubgraph) { + console.log( + ` ${event.eventType.toUpperCase()} | Entity: ${event.entityId} | Staker: ${event.staker} | Amount: ${event.amount} | Shares: ${event.shares} | Block: ${event.blockNumber} | TX: ${event.txHash}` + ); + } + + console.log(`\n----- Missing in Chain (${missingInChain.length}) -----`); + for (const event of missingInChain) { + console.log( + ` ${event.eventType.toUpperCase()} | Entity: ${event.entity.id} | Staker: ${event.staker} | Amount: ${event.amount} | Shares: ${event.shares} | Block: ${event.blockNumber} | TX: ${event.txHash}` + ); + } + + console.log(`\n----- Mismatched Values (${mismatched.length}) -----`); + for (const { subgraph, chain } of mismatched) { + console.log(` TX: ${chain.txHash}`); + console.log(` Chain: Amount=${chain.amount}, Shares=${chain.shares}`); + console.log(` Subgraph: Amount=${subgraph.amount}, Shares=${subgraph.shares}`); + } + + // Summary by entity + console.log("\n----- Summary by Entity -----"); + const entitySummary = new Map(); + + for (const event of chainEvents) { + if (!entitySummary.has(event.entityId)) { + entitySummary.set(event.entityId, { + chainStaked: 0n, + chainUnstaked: 0n, + chainStakedShares: 0n, + chainUnstakedShares: 0n, + subgraphStaked: 0n, + subgraphUnstaked: 0n, + subgraphStakedShares: 0n, + subgraphUnstakedShares: 0n, + }); + } + const summary = entitySummary.get(event.entityId); + if (event.eventType === "stake") { + summary.chainStaked += BigInt(event.amount); + summary.chainStakedShares += BigInt(event.shares); + } else { + summary.chainUnstaked += BigInt(event.amount); + summary.chainUnstakedShares += BigInt(event.shares); + } + } + + for (const event of subgraphEvents) { + if (!entitySummary.has(event.entity.id)) { + entitySummary.set(event.entity.id, { + chainStaked: 0n, + chainUnstaked: 0n, + chainStakedShares: 0n, + chainUnstakedShares: 0n, + subgraphStaked: 0n, + subgraphUnstaked: 0n, + subgraphStakedShares: 0n, + subgraphUnstakedShares: 0n, + }); + } + const summary = entitySummary.get(event.entity.id); + if (event.eventType === "stake") { + summary.subgraphStaked += BigInt(event.amount); + summary.subgraphStakedShares += BigInt(event.shares); + } else { + summary.subgraphUnstaked += BigInt(event.amount); + summary.subgraphUnstakedShares += BigInt(event.shares); + } + } + + for (const [entityId, summary] of entitySummary) { + const stakedDiff = summary.chainStaked - summary.subgraphStaked; + const unstakedDiff = summary.chainUnstaked - summary.subgraphUnstaked; + const stakedSharesDiff = summary.chainStakedShares - summary.subgraphStakedShares; + const unstakedSharesDiff = summary.chainUnstakedShares - summary.subgraphUnstakedShares; + + console.log(`\n Entity ${entityId}:`); + console.log(` Chain Staked: ${summary.chainStaked}`); + console.log(` Subgraph Staked: ${summary.subgraphStaked}`); + console.log(` Staked Amount Diff: ${stakedDiff}`); + console.log(` Chain Staked Shares: ${summary.chainStakedShares}`); + console.log(` Subgraph Staked Shares: ${summary.subgraphStakedShares}`); + console.log(` Staked Shares Diff: ${stakedSharesDiff}`); + console.log(` ---`); + console.log(` Chain Unstaked: ${summary.chainUnstaked}`); + console.log(` Subgraph Unstaked: ${summary.subgraphUnstaked}`); + console.log(` Unstaked Amount Diff: ${unstakedDiff}`); + + // Net difference (what's missing in activeRewardPool and totalShares) + const netAmountDiff = stakedDiff - unstakedDiff; + const netSharesDiff = stakedSharesDiff - unstakedSharesDiff; + if (netAmountDiff !== 0n || netSharesDiff !== 0n) { + console.log(` *** NET AMOUNT DIFF (activeRewardPool): ${netAmountDiff}`); + console.log(` *** NET SHARES DIFF (totalShares): ${netSharesDiff}`); + } + } +} + +async function main() { + console.log("Fetching events from subgraph..."); + const subgraphEvents = await fetchSubgraphEvents(); + console.log(`Found ${subgraphEvents.length} events in subgraph`); + + console.log("\nFetching events from chain..."); + const chainEvents = await fetchChainEvents(); + console.log(`Found ${chainEvents.length} events on chain`); + + compareEvents(subgraphEvents, chainEvents); +} + +main().catch(console.error); diff --git a/scripts/compare-staking-events.ts b/scripts/compare-staking-events.ts new file mode 100644 index 0000000..4295688 --- /dev/null +++ b/scripts/compare-staking-events.ts @@ -0,0 +1,293 @@ +import { ethers } from "ethers"; + +const SUBGRAPH_URL = + "https://api.goldsky.com/api/public/project_cm168cz887zva010j39il7a6p/subgraphs/moksha/staking/gn"; +const RPC_URL = + "https://falling-tame-liquid.vana-moksha.quiknode.pro/522e4e45df28df82a4d7729726e68cdc7d630011"; +const STAKING_CONTRACT = "0x641C18E2F286c86f96CE95C8ec1EB9fC0415Ca0e"; +const START_BLOCK = 2101757; + +// ABI for Staked and Unstaked events +const STAKING_ABI = [ + "event Staked(uint256 indexed entityId, address indexed staker, uint256 amount, uint256 sharesIssued)", + "event Unstaked(uint256 indexed entityId, address indexed staker, uint256 amount, uint256 sharesBurned)", +]; + +interface SubgraphStakeEvent { + id: string; + eventType: string; + staker: string; + entity: { id: string }; + amount: string; + shares: string; + blockNumber: string; + txHash: string; +} + +interface ChainEvent { + eventType: string; + entityId: string; + staker: string; + amount: string; + shares: string; + blockNumber: number; + txHash: string; + logIndex: number; +} + +async function fetchSubgraphEvents(): Promise { + const allEvents: SubgraphStakeEvent[] = []; + let skip = 0; + const first = 1000; + + while (true) { + const query = ` + query { + stakeEvents( + first: ${first} + skip: ${skip} + orderBy: blockNumber + orderDirection: asc + ) { + id + eventType + staker + entity { + id + } + amount + shares + blockNumber + txHash + } + } + `; + + const response = await fetch(SUBGRAPH_URL, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ query }), + }); + + const json = await response.json(); + const events = json.data?.stakeEvents || []; + + if (events.length === 0) break; + + allEvents.push(...events); + skip += first; + + if (events.length < first) break; + } + + return allEvents; +} + +async function fetchChainEvents(): Promise { + const provider = new ethers.JsonRpcProvider(RPC_URL); + const contract = new ethers.Contract(STAKING_CONTRACT, STAKING_ABI, provider); + + const latestBlock = await provider.getBlockNumber(); + console.log(`Fetching events from block ${START_BLOCK} to ${latestBlock}`); + + const allEvents: ChainEvent[] = []; + + // Fetch in chunks to avoid RPC limits + const chunkSize = 10000; + for (let fromBlock = START_BLOCK; fromBlock <= latestBlock; fromBlock += chunkSize) { + const toBlock = Math.min(fromBlock + chunkSize - 1, latestBlock); + + // Fetch Staked events + const stakedFilter = contract.filters.Staked(); + const stakedEvents = await contract.queryFilter(stakedFilter, fromBlock, toBlock); + + for (const event of stakedEvents) { + const log = event as ethers.EventLog; + allEvents.push({ + eventType: "stake", + entityId: log.args[0].toString(), + staker: log.args[1].toLowerCase(), + amount: log.args[2].toString(), + shares: log.args[3].toString(), + blockNumber: log.blockNumber, + txHash: log.transactionHash, + logIndex: log.index, + }); + } + + // Fetch Unstaked events + const unstakedFilter = contract.filters.Unstaked(); + const unstakedEvents = await contract.queryFilter(unstakedFilter, fromBlock, toBlock); + + for (const event of unstakedEvents) { + const log = event as ethers.EventLog; + allEvents.push({ + eventType: "unstake", + entityId: log.args[0].toString(), + staker: log.args[1].toLowerCase(), + amount: log.args[2].toString(), + shares: log.args[3].toString(), + blockNumber: log.blockNumber, + txHash: log.transactionHash, + logIndex: log.index, + }); + } + + console.log(`Processed blocks ${fromBlock} - ${toBlock}`); + } + + // Sort by block number and log index + allEvents.sort((a, b) => { + if (a.blockNumber !== b.blockNumber) return a.blockNumber - b.blockNumber; + return a.logIndex - b.logIndex; + }); + + return allEvents; +} + +function compareEvents( + subgraphEvents: SubgraphStakeEvent[], + chainEvents: ChainEvent[] +): void { + console.log("\n========== COMPARISON RESULTS ==========\n"); + console.log(`Subgraph events: ${subgraphEvents.length}`); + console.log(`Chain events: ${chainEvents.length}`); + + // Create maps for quick lookup + const subgraphMap = new Map(); + for (const event of subgraphEvents) { + // Key: txHash-eventType-entityId-staker + const key = `${event.txHash.toLowerCase()}-${event.eventType}-${event.entity.id}-${event.staker.toLowerCase()}`; + subgraphMap.set(key, event); + } + + const chainMap = new Map(); + for (const event of chainEvents) { + const key = `${event.txHash.toLowerCase()}-${event.eventType}-${event.entityId}-${event.staker.toLowerCase()}`; + chainMap.set(key, event); + } + + // Find events in chain but not in subgraph + const missingInSubgraph: ChainEvent[] = []; + for (const event of chainEvents) { + const key = `${event.txHash.toLowerCase()}-${event.eventType}-${event.entityId}-${event.staker.toLowerCase()}`; + if (!subgraphMap.has(key)) { + missingInSubgraph.push(event); + } + } + + // Find events in subgraph but not in chain + const missingInChain: SubgraphStakeEvent[] = []; + for (const event of subgraphEvents) { + const key = `${event.txHash.toLowerCase()}-${event.eventType}-${event.entity.id}-${event.staker.toLowerCase()}`; + if (!chainMap.has(key)) { + missingInChain.push(event); + } + } + + // Find events with mismatched amounts/shares + const mismatched: { subgraph: SubgraphStakeEvent; chain: ChainEvent }[] = []; + for (const event of chainEvents) { + const key = `${event.txHash.toLowerCase()}-${event.eventType}-${event.entityId}-${event.staker.toLowerCase()}`; + const subgraphEvent = subgraphMap.get(key); + if (subgraphEvent) { + if ( + subgraphEvent.amount !== event.amount || + subgraphEvent.shares !== event.shares + ) { + mismatched.push({ subgraph: subgraphEvent, chain: event }); + } + } + } + + console.log(`\n----- Missing in Subgraph (${missingInSubgraph.length}) -----`); + for (const event of missingInSubgraph) { + console.log( + ` ${event.eventType.toUpperCase()} | Entity: ${event.entityId} | Staker: ${event.staker} | Amount: ${event.amount} | Shares: ${event.shares} | Block: ${event.blockNumber} | TX: ${event.txHash}` + ); + } + + console.log(`\n----- Missing in Chain (${missingInChain.length}) -----`); + for (const event of missingInChain) { + console.log( + ` ${event.eventType.toUpperCase()} | Entity: ${event.entity.id} | Staker: ${event.staker} | Amount: ${event.amount} | Shares: ${event.shares} | Block: ${event.blockNumber} | TX: ${event.txHash}` + ); + } + + console.log(`\n----- Mismatched Values (${mismatched.length}) -----`); + for (const { subgraph, chain } of mismatched) { + console.log(` TX: ${chain.txHash}`); + console.log(` Chain: Amount=${chain.amount}, Shares=${chain.shares}`); + console.log(` Subgraph: Amount=${subgraph.amount}, Shares=${subgraph.shares}`); + } + + // Summary by entity + console.log("\n----- Summary by Entity -----"); + const entitySummary = new Map< + string, + { chainStaked: bigint; chainUnstaked: bigint; subgraphStaked: bigint; subgraphUnstaked: bigint } + >(); + + for (const event of chainEvents) { + if (!entitySummary.has(event.entityId)) { + entitySummary.set(event.entityId, { + chainStaked: 0n, + chainUnstaked: 0n, + subgraphStaked: 0n, + subgraphUnstaked: 0n, + }); + } + const summary = entitySummary.get(event.entityId)!; + if (event.eventType === "stake") { + summary.chainStaked += BigInt(event.amount); + } else { + summary.chainUnstaked += BigInt(event.amount); + } + } + + for (const event of subgraphEvents) { + if (!entitySummary.has(event.entity.id)) { + entitySummary.set(event.entity.id, { + chainStaked: 0n, + chainUnstaked: 0n, + subgraphStaked: 0n, + subgraphUnstaked: 0n, + }); + } + const summary = entitySummary.get(event.entity.id)!; + if (event.eventType === "stake") { + summary.subgraphStaked += BigInt(event.amount); + } else { + summary.subgraphUnstaked += BigInt(event.amount); + } + } + + for (const [entityId, summary] of entitySummary) { + const stakedDiff = summary.chainStaked - summary.subgraphStaked; + const unstakedDiff = summary.chainUnstaked - summary.subgraphUnstaked; + + if (stakedDiff !== 0n || unstakedDiff !== 0n) { + console.log(` Entity ${entityId}:`); + console.log(` Chain Staked: ${summary.chainStaked}`); + console.log(` Subgraph Staked: ${summary.subgraphStaked}`); + console.log(` Staked Diff: ${stakedDiff}`); + console.log(` Chain Unstaked: ${summary.chainUnstaked}`); + console.log(` Subgraph Unstaked: ${summary.subgraphUnstaked}`); + console.log(` Unstaked Diff: ${unstakedDiff}`); + } + } +} + +async function main() { + console.log("Fetching events from subgraph..."); + const subgraphEvents = await fetchSubgraphEvents(); + console.log(`Found ${subgraphEvents.length} events in subgraph`); + + console.log("\nFetching events from chain..."); + const chainEvents = await fetchChainEvents(); + console.log(`Found ${chainEvents.length} events on chain`); + + compareEvents(subgraphEvents, chainEvents); +} + +main().catch(console.error); diff --git a/subgraph.moksha-v7-only.yaml b/subgraph.moksha-v7-only.yaml new file mode 100644 index 0000000..0f971ab --- /dev/null +++ b/subgraph.moksha-v7-only.yaml @@ -0,0 +1,72 @@ +specVersion: 0.0.9 +description: Subgraph for VanaPool Staking (v7 only) +repository: "https://github.com/vana-com/vana-subgraph" + +schema: + file: ./schema.graphql + +dataSources: +# Staking + - kind: ethereum/contract + name: VanaPoolStakingImplementation + network: moksha + source: + address: "0x641C18E2F286c86f96CE95C8ec1EB9fC0415Ca0e" + abi: VanaPoolStakingImplementation + startBlock: 2101757 + mapping: + kind: ethereum/events + apiVersion: 0.0.6 + language: wasm/assemblyscript + entities: + - StakingEntity + - Stake + - StakeEvent + - StakingParams + abis: + - name: VanaPoolStakingImplementation + file: ./abis/v7/VanaPoolStakingImplementation.json + eventHandlers: + - event: Staked(indexed uint256,indexed address,uint256,uint256) + handler: handleStaked + - event: Unstaked(indexed uint256,indexed address,uint256,uint256) + handler: handleUnstaked + - event: MinStakeUpdated(uint256) + handler: handleMinStakeUpdated + - event: EntityStakeRegistered(indexed uint256,indexed address) + handler: handleEntityStakeRegistered + file: ./src/mapping.ts + + - kind: ethereum/contract + name: VanaPoolEntityImplementation + network: moksha + source: + address: "0x44f20490A82e1f1F1cC25Dd3BA8647034eDdce30" + abi: VanaPoolEntityImplementation + startBlock: 2101776 + mapping: + kind: ethereum/events + apiVersion: 0.0.6 + language: wasm/assemblyscript + entities: + - StakingEntity + - RewardEvent + abis: + - name: VanaPoolEntityImplementation + file: ./abis/v7/VanaPoolEntityImplementation.json + eventHandlers: + - event: EntityCreated(indexed uint256,address,string,uint256) + handler: handleEntityCreated + - event: EntityUpdated(indexed uint256,address,string) + handler: handleEntityUpdated + - event: EntityStatusUpdated(indexed uint256,uint8) + handler: handleEntityStatusUpdated + - event: EntityMaxAPYUpdated(indexed uint256,uint256) + handler: handleEntityMaxAPYUpdated + - event: RewardsAdded(indexed uint256,uint256) + handler: handleRewardsAdded + - event: RewardsProcessed(indexed uint256,uint256) + handler: handleRewardsProcessed + - event: ForfeitedRewardsReturned(indexed uint256,uint256) + handler: handleForfeitedRewardsReturned + file: ./src/mapping.ts From aad5b2aee1df64e3e802f68ecf2e125990040ac5 Mon Sep 17 00:00:00 2001 From: Ton-Chanh Le Date: Mon, 15 Dec 2025 13:18:19 -0500 Subject: [PATCH 09/10] update ABIs --- abis/v7/VanaPoolStakingImplementation.json | 175 ++++++++++++++++++--- scripts/compare-staking-events.mjs | 2 +- scripts/compare-staking-events.ts | 2 +- 3 files changed, 158 insertions(+), 21 deletions(-) diff --git a/abis/v7/VanaPoolStakingImplementation.json b/abis/v7/VanaPoolStakingImplementation.json index c3e92a5..47743f5 100644 --- a/abis/v7/VanaPoolStakingImplementation.json +++ b/abis/v7/VanaPoolStakingImplementation.json @@ -492,6 +492,106 @@ "stateMutability": "view", "type": "function" }, + { + "inputs": [], + "name": "bondingPeriod", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "staker", + "type": "address" + }, + { + "internalType": "uint256", + "name": "entityId", + "type": "uint256" + } + ], + "name": "getAccruingInterest", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "staker", + "type": "address" + }, + { + "internalType": "uint256", + "name": "entityId", + "type": "uint256" + } + ], + "name": "getEarnedRewards", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "staker", + "type": "address" + }, + { + "internalType": "uint256", + "name": "entityId", + "type": "uint256" + } + ], + "name": "getMaxUnstakeAmount", + "outputs": [ + { + "internalType": "uint256", + "name": "maxVana", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "maxShares", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "limitingFactor", + "type": "uint256" + }, + { + "internalType": "bool", + "name": "isInBondingPeriod", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, { "inputs": [ { @@ -664,25 +764,6 @@ "stateMutability": "view", "type": "function" }, - { - "inputs": [ - { - "internalType": "bytes[]", - "name": "data", - "type": "bytes[]" - } - ], - "name": "multicall", - "outputs": [ - { - "internalType": "bytes[]", - "name": "results", - "type": "bytes[]" - } - ], - "stateMutability": "nonpayable", - "type": "function" - }, { "inputs": [], "name": "pause", @@ -819,6 +900,26 @@ "internalType": "uint256", "name": "shares", "type": "uint256" + }, + { + "internalType": "uint256", + "name": "costBasis", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "rewardEligibilityTimestamp", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "realizedRewards", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "vestedRewards", + "type": "uint256" } ], "internalType": "struct IVanaPoolStaking.StakerEntity", @@ -891,6 +992,42 @@ "stateMutability": "nonpayable", "type": "function" }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "entityId", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "vanaAmount", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "shareAmountMax", + "type": "uint256" + } + ], + "name": "unstakeVana", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "newBondingPeriod", + "type": "uint256" + } + ], + "name": "updateBondingPeriod", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, { "inputs": [ { diff --git a/scripts/compare-staking-events.mjs b/scripts/compare-staking-events.mjs index bc8d8a2..03fe08c 100644 --- a/scripts/compare-staking-events.mjs +++ b/scripts/compare-staking-events.mjs @@ -1,7 +1,7 @@ import { ethers } from "ethers"; const SUBGRAPH_URL = - "https://api.goldsky.com/api/public/project_cm168cz887zva010j39il7a6p/subgraphs/moksha/staking/gn"; + "https://api.goldsky.com/api/public/project_cm168cz887zva010j39il7a6p/subgraphs/moksha/staking-only/gn"; const RPC_URL = "https://falling-tame-liquid.vana-moksha.quiknode.pro/522e4e45df28df82a4d7729726e68cdc7d630011"; const STAKING_CONTRACT = "0x641C18E2F286c86f96CE95C8ec1EB9fC0415Ca0e"; diff --git a/scripts/compare-staking-events.ts b/scripts/compare-staking-events.ts index 4295688..c309f98 100644 --- a/scripts/compare-staking-events.ts +++ b/scripts/compare-staking-events.ts @@ -1,7 +1,7 @@ import { ethers } from "ethers"; const SUBGRAPH_URL = - "https://api.goldsky.com/api/public/project_cm168cz887zva010j39il7a6p/subgraphs/moksha/staking/gn"; + "https://api.goldsky.com/api/public/project_cm168cz887zva010j39il7a6p/subgraphs/moksha/staking-only/gn"; const RPC_URL = "https://falling-tame-liquid.vana-moksha.quiknode.pro/522e4e45df28df82a4d7729726e68cdc7d630011"; const STAKING_CONTRACT = "0x641C18E2F286c86f96CE95C8ec1EB9fC0415Ca0e"; From e8e286035de920a0f9fbe9f77d7b60a9a36702df Mon Sep 17 00:00:00 2001 From: Ton-Chanh Le Date: Mon, 5 Jan 2026 14:12:53 -0500 Subject: [PATCH 10/10] update abis --- abis/v7/VanaPoolStakingImplementation.json | 31 ++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/abis/v7/VanaPoolStakingImplementation.json b/abis/v7/VanaPoolStakingImplementation.json index 47743f5..9c42291 100644 --- a/abis/v7/VanaPoolStakingImplementation.json +++ b/abis/v7/VanaPoolStakingImplementation.json @@ -102,6 +102,11 @@ "name": "InvalidAmount", "type": "error" }, + { + "inputs": [], + "name": "InvalidBondingPeriod", + "type": "error" + }, { "inputs": [], "name": "InvalidEntity", @@ -163,6 +168,19 @@ "name": "UUPSUnsupportedProxiableUUID", "type": "error" }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint256", + "name": "newBondingPeriod", + "type": "uint256" + } + ], + "name": "BondingPeriodUpdated", + "type": "event" + }, { "anonymous": false, "inputs": [ @@ -410,6 +428,19 @@ "stateMutability": "view", "type": "function" }, + { + "inputs": [], + "name": "MAX_BONDING_PERIOD", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, { "inputs": [], "name": "UPGRADE_INTERFACE_VERSION",