diff --git a/app/api/dao-overview/route.ts b/app/api/dao-overview/route.ts index 56d284a..0510b0a 100644 --- a/app/api/dao-overview/route.ts +++ b/app/api/dao-overview/route.ts @@ -37,6 +37,16 @@ const QUORUM_ABI = parseAbi([ "function quorumNumerator() view returns (uint256)", "function quorumDenominator() view returns (uint256)", ]); +// LightChain's Governor overrides quorum() to use INativeVotes.getTotalVotingPower +// (the STAKED-EXCLUDED supply), not IVotes.getPastTotalSupply. So the real quorum +// is 3% of the votable base, not 3% of raw supply. Read both to show the truth. +const NATIVE_QUORUM_ABI = parseAbi([ + "function quorum(uint256 timepoint) view returns (uint256)", + "function clock() view returns (uint48)", +]); +const NATIVE_VOTES_ABI = parseAbi([ + "function getTotalVotingPower(uint256 timepoint) view returns (uint256)", +]); const SCHEDULE_ABI = parseAbi([ "function votingDelay() view returns (uint256)", "function votingPeriod() view returns (uint256)", @@ -107,16 +117,30 @@ async function readEthereumOverview(pub: Pub) { async function readLightchainOverview(pub: Pub) { const a = DAO_ADDRESSES.lightchain; // Native balances: treasury + FeePool hold native LCAI (no ERC-20 wrapper). - const [treasuryWei, feePoolWei, quorum] = await Promise.all([ + const [treasuryWei, feePoolWei, quorum, clockRaw] = await Promise.all([ pub.getBalance({ address: a.treasury }).catch(() => 0n), pub.getBalance({ address: FEE_POOL }).catch(() => 0n), readQuorumConfig(pub, a.governor), + pub.readContract({ address: a.governor, abi: NATIVE_QUORUM_ABI, functionName: "clock" }).catch(() => 0n), + ]); + // Evaluate the real quorum + votable base at the latest finalized timepoint. + const clock = BigInt(clockRaw as bigint | number); + const ref = clock > 0n ? clock - 1n : 0n; + const [quorumWei, votableWei] = await Promise.all([ + pub.readContract({ address: a.governor, abi: NATIVE_QUORUM_ABI, functionName: "quorum", args: [ref] }).catch(() => null), + a.ballots + ? pub.readContract({ address: a.ballots, abi: NATIVE_VOTES_ABI, functionName: "getTotalVotingPower", args: [ref] }).catch(() => null) + : Promise.resolve(null), ]); return { treasuryWei: treasuryWei.toString(), feePoolWei: feePoolWei.toString(), voteToken: { address: a.ballots, symbol: "LCAI", totalSupplyWei: null as string | null }, quorum, + // The actual quorum threshold + the staked-excluded base it applies to. + quorumWei: quorumWei != null ? (quorumWei as bigint).toString() : null, + votableSupplyWei: votableWei != null ? (votableWei as bigint).toString() : null, + stakeExcludedFromQuorum: true as boolean, }; } diff --git a/app/build/dao/treasury-bar.tsx b/app/build/dao/treasury-bar.tsx index 1c46b12..12c0b81 100644 --- a/app/build/dao/treasury-bar.tsx +++ b/app/build/dao/treasury-bar.tsx @@ -21,6 +21,11 @@ interface Overview { feePoolWei: string | null; voteToken: { address: string; symbol: string; totalSupplyWei: string | null }; quorum: { numerator: string; denominator: string }; + // LightChain only: the actual on-chain quorum threshold + the staked-excluded + // base it applies to (worker + validator stake are not in the votable supply). + quorumWei?: string | null; + votableSupplyWei?: string | null; + stakeExcludedFromQuorum?: boolean; schedule?: Schedule; error?: string; } @@ -89,10 +94,21 @@ export function TreasuryBar({ chain }: { chain: DaoChain }) { const qPct = quorumPercent(data.quorum.numerator, data.quorum.denominator); const supply = data.voteToken.totalSupplyWei ? `${formatLcaiWei(BigInt(data.voteToken.totalSupplyWei), 0)} ${sym}` : "native stake"; - const quorumValue = qPct ? `${qPct % 1 === 0 ? qPct : qPct.toFixed(1)}% of supply` : "n/a"; - const quorumHint = data.voteToken.totalSupplyWei - ? `${quorumValue} - needs ${formatLcaiWei((BigInt(data.voteToken.totalSupplyWei) * BigInt(data.quorum.numerator)) / BigInt(data.quorum.denominator || "1"), 0)} ${VOTE_SYMBOL[chain]} of For+Abstain to be valid` - : `${quorumValue} of the native vote supply`; + const pctLabel = qPct ? `${qPct % 1 === 0 ? qPct : qPct.toFixed(1)}%` : "3%"; + // LightChain reports the REAL quorum (quorumWei): 3% of the staked-excluded + // votable supply, not "% of supply". Worker + validator stake are not in the + // votable base. Fall back to the "% of supply" label only where no real quorum + // amount is available (e.g. Ethereum's ERC20Votes reads). + const quorumValue = data.quorumWei + ? `${formatLcaiWei(BigInt(data.quorumWei), 0)} ${VOTE_SYMBOL[chain]}` + : qPct + ? `${pctLabel} of supply` + : "n/a"; + const quorumHint = data.quorumWei + ? `${pctLabel} of the votable supply${data.stakeExcludedFromQuorum ? " (staked LCAI excluded)" : ""}${data.votableSupplyWei ? ` = ${formatLcaiWei(BigInt(data.votableSupplyWei), 0)} ${VOTE_SYMBOL[chain]} base` : ""} - needs this much For+Abstain to pass` + : data.voteToken.totalSupplyWei + ? `${quorumValue} - needs ${formatLcaiWei((BigInt(data.voteToken.totalSupplyWei) * BigInt(data.quorum.numerator)) / BigInt(data.quorum.denominator || "1"), 0)} ${VOTE_SYMBOL[chain]} of For+Abstain to be valid` + : `${quorumValue} of the native vote supply`; return (