Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 71 additions & 0 deletions fees/cap/config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { CHAIN } from "../../helpers/chains";

export const capConfig = {
[CHAIN.ETHEREUM]: {
fromBlock: 22867447,
fromTime: 1751892887,
fromDate: "2025-07-07",
infra: {
oracle: {
address: "0xcD7f45566bc0E7303fB92A93969BB4D3f6e662bb",
fromBlock: 22867447,
},
lender: {
address: "0x15622c3dbbc5614E6DFa9446603c1779647f01FC",
fromBlock: 22867447,
},
delegation: {
address: "0xF3E3Eae671000612CE3Fd15e1019154C1a4d693F",
fromBlock: 22867447,
},
},
tokens: {
cUSD: {
id: "cUSD",
coingeckoId: "cap-usd",
decimals: 18,
address: "0xcCcc62962d17b8914c62D74FfB843d73B2a3cccC",
fromBlock: 22874015,
},
stcUSD: {
id: "stcUSD",
coingeckoId: "cap-staked-usd",
decimals: 18,
address: "0x88887bE419578051FF9F4eb6C858A951921D8888",
fromBlock: 22874056,
},
},
},
} as const;

export const capABI = {
Vault: {
insuranceFund: "function insuranceFund() external view returns (address)",
interestReceiver:
"function interestReceiver() public view returns (address)",
AddAssetEvent: "event AddAsset(address asset)",
},
Lender: {
ReserveAssetAddedEvent:
"event ReserveAssetAdded(address indexed asset, address vault, address debtToken, address interestReceiver, uint256 id)",
ReserveInterestReceiverUpdatedEvent:
"event ReserveInterestReceiverUpdated(address indexed asset, address interestReceiver)",
RealizeInterestEvent:
"event RealizeInterest(address indexed asset, uint256 realizedInterest, address interestReceiver)",
RepayEvent:
"event Repay(address indexed asset, address indexed agent, (uint256 repaid, uint256 vaultRepaid, uint256 restakerRepaid, uint256 interestRepaid) details)",
},
FeeReceiver: {
FeesDistributedEvent: "event FeesDistributed(uint256 amount)",
ProtocolFeeClaimed: "event ProtocolFeeClaimed(uint256 amount)",
},
Delegation: {
DistributeReward:
"event DistributeReward(address agent, address asset, uint256 amount)",
},
} as const;

// should be ignored for revenue calculation
export const devAddresses = ["0xc1ab5a9593e6e1662a9a44f84df4f31fc8a76b52"];

export const vaultsSymbols = ["cUSD"];
109 changes: 109 additions & 0 deletions fees/cap/helpers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
import { FetchOptions } from "../../adapters/types";
import { capABI, capConfig, devAddresses, vaultsSymbols } from "./config";

export const arrayZip = <A, B>(a: A[], b: B[]) => {
const maxLength = Math.max(a.length, b.length);
return Array.from({ length: maxLength }, (_, i) => [a[i], b[i]]) as [A, B][];
};

export const isKnownVault = (options: FetchOptions, vault: string) => {
const vaults = vaultsSymbols.map(
(symbol) => capConfig[options.chain].tokens[symbol],
);
return vaults.map((vault) => vault.address.toLowerCase()).includes(vault);
};

export const fetchAssetAddresses = async (
options: FetchOptions,
chain: string,
) => {
const infra = capConfig[chain].infra;
const tokens = capConfig[chain].tokens;
const lender = infra.lender;

const cUSDVaultAssetAddresses = await options.getLogs({
eventAbi: capABI.Vault.AddAssetEvent,
target: tokens.cUSD.address,
fromBlock: tokens.cUSD.fromBlock,
});

const lenderReserveAssetAddresses = await options.getLogs({
eventAbi: capABI.Lender.ReserveAssetAddedEvent,
target: lender.address,
fromBlock: lender.fromBlock,
});

return [
...new Set([
...cUSDVaultAssetAddresses.map((event) => event.asset.toLowerCase()),
...lenderReserveAssetAddresses.map((event) => event.asset.toLowerCase()),
]),
];
};

export const fetchVaultConfigs = async (options: FetchOptions) => {
const infra = capConfig[options.chain].infra;

const assetAddedEvents = await options.getLogs({
target: infra.lender.address,
eventAbi: capABI.Lender.ReserveAssetAddedEvent,
fromBlock: infra.lender.fromBlock,
});

const vaultConfigsByAsset: Record<
string,
{ asset: string; vault: string; interestReceivers: string[] }
> = {};
for (const event of assetAddedEvents) {
const asset = event.asset.toLowerCase();
const vault = event.vault.toLowerCase();
if (!isKnownVault(options, vault)) {
continue;
}

const interestReceiver = event.interestReceiver.toLowerCase();
if (!vaultConfigsByAsset[asset]) {
vaultConfigsByAsset[asset] = { asset, vault, interestReceivers: [] };
} else if (vaultConfigsByAsset[asset].vault !== vault) {
throw new Error(
`Vault mismatch for asset ${asset}: ${vaultConfigsByAsset[asset].vault} !== ${vault}`,
);
}
vaultConfigsByAsset[asset].interestReceivers.push(interestReceiver);
}

const interestReceiverUpdatedEvents = await options.getLogs({
target: infra.lender.address,
eventAbi: capABI.Lender.ReserveInterestReceiverUpdatedEvent,
fromBlock: infra.lender.fromBlock,
});
for (const event of interestReceiverUpdatedEvents) {
const asset = event.asset.toLowerCase();
const interestReceiver = event.interestReceiver.toLowerCase();
if (!vaultConfigsByAsset[asset]) {
throw new Error(`Asset ${asset} not found in vaultConfigsByAsset`);
}
vaultConfigsByAsset[asset].interestReceivers.push(interestReceiver);
}

const vaultConfigs = Object.values(vaultConfigsByAsset);

const insuranceFunds: string[] = (
await options.api.batchCall(
vaultConfigs.map(({ vault }) => ({
target: vault,
abi: capABI.Vault.insuranceFund,
})),
)
).map((i) => i.toLowerCase());

const result = arrayZip(vaultConfigs, insuranceFunds).map(
([vaultConfig, insuranceFund]) => ({
...vaultConfig,
insuranceFund: devAddresses.includes(insuranceFund)
? null
: insuranceFund,
}),
);
return result;
};
118 changes: 118 additions & 0 deletions fees/cap/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
import { FetchOptions, SimpleAdapter } from "../../adapters/types";
import { addTokensReceived } from "../../helpers/token";
import { CHAIN } from "../../helpers/chains";
import { capABI, capConfig } from "./config";
import { fetchAssetAddresses, fetchVaultConfigs } from "./helpers";

const fetch = async (options: FetchOptions) => {
const infra = capConfig[options.chain].infra;
const assetAddresses = await fetchAssetAddresses(options, options.chain);
const vaultConfigs = await fetchVaultConfigs(options);

const feesDistributedLogs = (
await Promise.all(
vaultConfigs
.map((vaultConfig) =>
vaultConfig.interestReceivers.map(async (interestReceiver) => {
const logs = await options.getLogs({
target: interestReceiver,
eventAbi: capABI.FeeReceiver.FeesDistributedEvent,
});

return logs.map((log) => ({
feeAsset: vaultConfig.vault, // fee is collected in vault assets
amount: log.amount,
}));
}),
)
.flat(),
)
).flat();
const minterFees = options.createBalances();
for (const { feeAsset, amount } of feesDistributedLogs) {
minterFees.add(feeAsset, amount);
}

const protocolFeeClaimedLogs = (
await Promise.all(
vaultConfigs
.map((vaultConfig) =>
vaultConfig.interestReceivers.map(async (interestReceiver) => {
const logs = await options.getLogs({
target: interestReceiver,
eventAbi: capABI.FeeReceiver.ProtocolFeeClaimed,
});

return logs.map((log) => ({
feeAsset: vaultConfig.vault, // fee is collected in vault assets
amount: log.amount,
}));
}),
)
.flat(),
)
).flat();
const protocolFees = options.createBalances();
for (const { feeAsset, amount } of protocolFeeClaimedLogs) {
protocolFees.add(feeAsset, amount);
}

const restakerFeesLogs = await options.getLogs({
target: infra.delegation.address,
eventAbi: capABI.Delegation.DistributeReward,
});
const restakerFees = options.createBalances();
for (const log of restakerFeesLogs) {
restakerFees.add(log.asset, log.amount);
}

const insuranceFunds = vaultConfigs
.map((i) => i.insuranceFund)
.filter((i) => i !== null);
const minters = vaultConfigs.map((i) => i.vault);
const insuranceFundFees =
insuranceFunds.length > 0
? await addTokensReceived({
options,
fromAdddesses: minters,
tokens: assetAddresses,
targets: insuranceFunds,
})
: options.createBalances();

const dailyFees = options.createBalances();
dailyFees.addBalances(minterFees);
dailyFees.addBalances(protocolFees);
dailyFees.addBalances(restakerFees);
dailyFees.addBalances(insuranceFundFees);

const dailyRevenue = options.createBalances();
dailyRevenue.addBalances(protocolFees);

const dailySupplySideRevenue = dailyFees.clone();
dailySupplySideRevenue.subtract(dailyRevenue)

return {
dailyFees,
dailySupplySideRevenue,
dailyRevenue: dailyRevenue,
dailyProtocolRevenue: dailyRevenue,
};
};

const methodology = {
Fees: "All fees paid by users for either borrowing (borrow fees + restaker fees) or minting (insurance fund fees).",
Revenue: "Share of borrow fees for protocol",
SupplySideRevenue: "Borrow fees distributed to stakers and restaker fees are distributed to delegators.",
ProtocolRevenue: "Share of borrow fees for protocol",
};

const adapter: SimpleAdapter = {
version: 2,
fetch,
chains: [CHAIN.ETHEREUM],
start: capConfig[CHAIN.ETHEREUM].fromDate,
methodology,
};

export default adapter;
Loading