Skip to content

Commit c137049

Browse files
authored
Merge pull request #226 from r4topunk/feat/treasury-accruing-mor
feat(stake): treasury conta o MOR pendente (25% da Gnars acumulando)
2 parents 618ac22 + bee10bd commit c137049

1 file changed

Lines changed: 61 additions & 7 deletions

File tree

src/services/stake-graph.ts

Lines changed: 61 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -285,6 +285,40 @@ async function etherscanClaimReceiver(
285285
return null;
286286
}
287287

288+
// A staker's currently-claimable MOR on a pool (still in the Morpheus contract,
289+
// pre-claim). Gnars' 25% of this is revenue ACCRUING to the treasury — it shows
290+
// up here so the treasury figure ticks up like the (also-unrealized) vault fee,
291+
// instead of sitting at 0 until the first claim→split→distribute. Etherscan
292+
// proxy eth_call (datacenter-safe); returns wei, 0 on any failure.
293+
const LATEST_REWARD_SIG = keccak256(toHex("getLatestUserReward(uint256,address)")).slice(0, 10);
294+
async function etherscanLatestReward(pool: Address, user: Address, key: string): Promise<bigint> {
295+
const data = `${LATEST_REWARD_SIG}${"0".repeat(64)}${pad32(user).slice(2)}`;
296+
const url =
297+
`https://api.etherscan.io/v2/api?chainid=1&module=proxy&action=eth_call` +
298+
`&to=${pool}&data=${data}&tag=latest&apikey=${key}`;
299+
for (let i = 0; i < 4; i++) {
300+
try {
301+
const res = await fetch(url, { cache: "no-store" });
302+
const j = (await res.json()) as { result?: unknown; message?: string };
303+
if (typeof j.result === "string" && /^0x[0-9a-fA-F]+$/.test(j.result)) {
304+
try {
305+
return BigInt(j.result);
306+
} catch {
307+
return BigInt(0);
308+
}
309+
}
310+
if (/rate limit/i.test(String(j.result)) || /rate limit/i.test(String(j.message))) {
311+
await sleep(600);
312+
continue;
313+
}
314+
return BigInt(0);
315+
} catch {
316+
await sleep(300);
317+
}
318+
}
319+
return BigInt(0);
320+
}
321+
288322
async function morBackersByRider(ethUsd: UsdPrice): Promise<Record<string, OrbitBacker[]>> {
289323
const walletToId = new Map<string, RiderId>();
290324
for (const r of RIDER_LIST) if (r.wallet) walletToId.set(r.wallet.toLowerCase(), r.id);
@@ -426,9 +460,14 @@ async function morBackersByRider(ethUsd: UsdPrice): Promise<Record<string, Orbit
426460
}
427461

428462
/**
429-
* MOR earned for the Gnars treasury: what's already been distributed to the
430-
* Gnars Arbitrum multisig, plus Gnars' 25% share still sitting undistributed in
431-
* each staker's split. Best-effort — priced in USD via CoinGecko.
463+
* MOR earned/accruing for the Gnars treasury, in three tiers (Gnars = 25% of the
464+
* staker's rewards throughout):
465+
* 1. directRaw — already distributed to the Gnars Arbitrum multisig.
466+
* 2. in-split — claimed to a staker's split, awaiting distribution (Arbitrum).
467+
* 3. accruing — still unclaimed in the Morpheus pools (mainnet, pending).
468+
* Tier 3 keeps the figure alive: it ticks up as MOR accrues, mirroring the
469+
* (also-unrealized) vault fee, instead of reading 0 until the first claim.
470+
* Best-effort — priced in USD via CoinGecko.
432471
*/
433472
async function gnarsMorEarned(
434473
morByRider: Record<string, OrbitBacker[]>,
@@ -454,14 +493,22 @@ async function readGnarsMor(
454493

455494
// Unique (staker, athlete) pairs → the per-staker splits holding MOR.
456495
const pairs = new Map<string, [Address, Address]>();
496+
// (pool, staker) targets for the still-unclaimed MOR accruing in Morpheus.
497+
const pendTargets: Array<[Address, Address]> = [];
457498
for (const [id, backers] of Object.entries(morByRider)) {
458499
const ref = walletById.get(id);
459500
if (!ref) continue;
460-
for (const b of backers) pairs.set(`${b.address}-${ref}`.toLowerCase(), [b.address, ref]);
501+
for (const b of backers) {
502+
pairs.set(`${b.address}-${ref}`.toLowerCase(), [b.address, ref]);
503+
if (b.kind === "mor" && b.asset && ETHERSCAN_KEY) {
504+
const pool = b.asset === "steth" ? MORPHEUS_POOLS.stEth.pool : MORPHEUS_POOLS.usdc.pool;
505+
pendTargets.push([pool, b.address]);
506+
}
507+
}
461508
}
462509

463510
try {
464-
const [splitBals, directRaw, morUsd] = await Promise.all([
511+
const [splitBals, directRaw, morUsd, pendRaw] = await Promise.all([
465512
Promise.all([...pairs.values()].map(([s, a]) => splitMorBalance(s, a).catch(() => 0))),
466513
arbitrumClient
467514
.readContract({
@@ -473,9 +520,16 @@ async function readGnarsMor(
473520
.then((b) => Number(formatUnits(b, MOR_DECIMALS)))
474521
.catch(() => 0),
475522
getTokenPriceUsd(MOR_TOKEN, "arbitrum-one"),
523+
Promise.all(
524+
pendTargets.map(([p, u]) =>
525+
etherscanLatestReward(p, u, ETHERSCAN_KEY as string).catch(() => BigInt(0)),
526+
),
527+
),
476528
]);
477-
const pendingGnars = splitBals.reduce((s, v) => s + v, 0) * 0.25; // Gnars = 25% of each split
478-
const mor = pendingGnars + directRaw;
529+
const inSplitGnars = splitBals.reduce((s, v) => s + v, 0) * 0.25; // claimed, awaiting distribute
530+
const accruingGnars =
531+
pendRaw.reduce((s, v) => s + Number(formatUnits(v, MOR_DECIMALS)), 0) * 0.25; // still in Morpheus
532+
const mor = inSplitGnars + directRaw + accruingGnars;
479533
return { mor, morUsd };
480534
} catch {
481535
return { mor: 0, morUsd: null as UsdPrice };

0 commit comments

Comments
 (0)