Skip to content

Commit 81b3059

Browse files
prevostcnoateden
andauthored
Add cap fees adapter (#4024)
* Add cap adapter * Use events to infer fees * refactor codes * Update coingecko id and cap directory * chore: trigger CI --------- Co-authored-by: Eden <[email protected]>
1 parent 7caad1d commit 81b3059

File tree

3 files changed

+298
-0
lines changed

3 files changed

+298
-0
lines changed

fees/cap/config.ts

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
import { CHAIN } from "../../helpers/chains";
2+
3+
export const capConfig = {
4+
[CHAIN.ETHEREUM]: {
5+
fromBlock: 22867447,
6+
fromTime: 1751892887,
7+
fromDate: "2025-07-07",
8+
infra: {
9+
oracle: {
10+
address: "0xcD7f45566bc0E7303fB92A93969BB4D3f6e662bb",
11+
fromBlock: 22867447,
12+
},
13+
lender: {
14+
address: "0x15622c3dbbc5614E6DFa9446603c1779647f01FC",
15+
fromBlock: 22867447,
16+
},
17+
delegation: {
18+
address: "0xF3E3Eae671000612CE3Fd15e1019154C1a4d693F",
19+
fromBlock: 22867447,
20+
},
21+
},
22+
tokens: {
23+
cUSD: {
24+
id: "cUSD",
25+
coingeckoId: "cap-usd",
26+
decimals: 18,
27+
address: "0xcCcc62962d17b8914c62D74FfB843d73B2a3cccC",
28+
fromBlock: 22874015,
29+
},
30+
stcUSD: {
31+
id: "stcUSD",
32+
coingeckoId: "cap-staked-usd",
33+
decimals: 18,
34+
address: "0x88887bE419578051FF9F4eb6C858A951921D8888",
35+
fromBlock: 22874056,
36+
},
37+
},
38+
},
39+
} as const;
40+
41+
export const capABI = {
42+
Vault: {
43+
insuranceFund: "function insuranceFund() external view returns (address)",
44+
interestReceiver:
45+
"function interestReceiver() public view returns (address)",
46+
AddAssetEvent: "event AddAsset(address asset)",
47+
},
48+
Lender: {
49+
ReserveAssetAddedEvent:
50+
"event ReserveAssetAdded(address indexed asset, address vault, address debtToken, address interestReceiver, uint256 id)",
51+
ReserveInterestReceiverUpdatedEvent:
52+
"event ReserveInterestReceiverUpdated(address indexed asset, address interestReceiver)",
53+
RealizeInterestEvent:
54+
"event RealizeInterest(address indexed asset, uint256 realizedInterest, address interestReceiver)",
55+
RepayEvent:
56+
"event Repay(address indexed asset, address indexed agent, (uint256 repaid, uint256 vaultRepaid, uint256 restakerRepaid, uint256 interestRepaid) details)",
57+
},
58+
FeeReceiver: {
59+
FeesDistributedEvent: "event FeesDistributed(uint256 amount)",
60+
ProtocolFeeClaimed: "event ProtocolFeeClaimed(uint256 amount)",
61+
},
62+
Delegation: {
63+
DistributeReward:
64+
"event DistributeReward(address agent, address asset, uint256 amount)",
65+
},
66+
} as const;
67+
68+
// should be ignored for revenue calculation
69+
export const devAddresses = ["0xc1ab5a9593e6e1662a9a44f84df4f31fc8a76b52"];
70+
71+
export const vaultsSymbols = ["cUSD"];

fees/cap/helpers.ts

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
import { FetchOptions } from "../../adapters/types";
2+
import { capABI, capConfig, devAddresses, vaultsSymbols } from "./config";
3+
4+
export const arrayZip = <A, B>(a: A[], b: B[]) => {
5+
const maxLength = Math.max(a.length, b.length);
6+
return Array.from({ length: maxLength }, (_, i) => [a[i], b[i]]) as [A, B][];
7+
};
8+
9+
export const isKnownVault = (options: FetchOptions, vault: string) => {
10+
const vaults = vaultsSymbols.map(
11+
(symbol) => capConfig[options.chain].tokens[symbol],
12+
);
13+
return vaults.map((vault) => vault.address.toLowerCase()).includes(vault);
14+
};
15+
16+
export const fetchAssetAddresses = async (
17+
options: FetchOptions,
18+
chain: string,
19+
) => {
20+
const infra = capConfig[chain].infra;
21+
const tokens = capConfig[chain].tokens;
22+
const lender = infra.lender;
23+
24+
const cUSDVaultAssetAddresses = await options.getLogs({
25+
eventAbi: capABI.Vault.AddAssetEvent,
26+
target: tokens.cUSD.address,
27+
fromBlock: tokens.cUSD.fromBlock,
28+
});
29+
30+
const lenderReserveAssetAddresses = await options.getLogs({
31+
eventAbi: capABI.Lender.ReserveAssetAddedEvent,
32+
target: lender.address,
33+
fromBlock: lender.fromBlock,
34+
});
35+
36+
return [
37+
...new Set([
38+
...cUSDVaultAssetAddresses.map((event) => event.asset.toLowerCase()),
39+
...lenderReserveAssetAddresses.map((event) => event.asset.toLowerCase()),
40+
]),
41+
];
42+
};
43+
44+
export const fetchVaultConfigs = async (options: FetchOptions) => {
45+
const infra = capConfig[options.chain].infra;
46+
47+
const assetAddedEvents = await options.getLogs({
48+
target: infra.lender.address,
49+
eventAbi: capABI.Lender.ReserveAssetAddedEvent,
50+
fromBlock: infra.lender.fromBlock,
51+
});
52+
53+
const vaultConfigsByAsset: Record<
54+
string,
55+
{ asset: string; vault: string; interestReceivers: string[] }
56+
> = {};
57+
for (const event of assetAddedEvents) {
58+
const asset = event.asset.toLowerCase();
59+
const vault = event.vault.toLowerCase();
60+
if (!isKnownVault(options, vault)) {
61+
continue;
62+
}
63+
64+
const interestReceiver = event.interestReceiver.toLowerCase();
65+
if (!vaultConfigsByAsset[asset]) {
66+
vaultConfigsByAsset[asset] = { asset, vault, interestReceivers: [] };
67+
} else if (vaultConfigsByAsset[asset].vault !== vault) {
68+
throw new Error(
69+
`Vault mismatch for asset ${asset}: ${vaultConfigsByAsset[asset].vault} !== ${vault}`,
70+
);
71+
}
72+
vaultConfigsByAsset[asset].interestReceivers.push(interestReceiver);
73+
}
74+
75+
const interestReceiverUpdatedEvents = await options.getLogs({
76+
target: infra.lender.address,
77+
eventAbi: capABI.Lender.ReserveInterestReceiverUpdatedEvent,
78+
fromBlock: infra.lender.fromBlock,
79+
});
80+
for (const event of interestReceiverUpdatedEvents) {
81+
const asset = event.asset.toLowerCase();
82+
const interestReceiver = event.interestReceiver.toLowerCase();
83+
if (!vaultConfigsByAsset[asset]) {
84+
throw new Error(`Asset ${asset} not found in vaultConfigsByAsset`);
85+
}
86+
vaultConfigsByAsset[asset].interestReceivers.push(interestReceiver);
87+
}
88+
89+
const vaultConfigs = Object.values(vaultConfigsByAsset);
90+
91+
const insuranceFunds: string[] = (
92+
await options.api.batchCall(
93+
vaultConfigs.map(({ vault }) => ({
94+
target: vault,
95+
abi: capABI.Vault.insuranceFund,
96+
})),
97+
)
98+
).map((i) => i.toLowerCase());
99+
100+
const result = arrayZip(vaultConfigs, insuranceFunds).map(
101+
([vaultConfig, insuranceFund]) => ({
102+
...vaultConfig,
103+
insuranceFund: devAddresses.includes(insuranceFund)
104+
? null
105+
: insuranceFund,
106+
}),
107+
);
108+
return result;
109+
};

fees/cap/index.ts

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
import { FetchOptions, SimpleAdapter } from "../../adapters/types";
2+
import { addTokensReceived } from "../../helpers/token";
3+
import { CHAIN } from "../../helpers/chains";
4+
import { capABI, capConfig } from "./config";
5+
import { fetchAssetAddresses, fetchVaultConfigs } from "./helpers";
6+
7+
const fetch = async (options: FetchOptions) => {
8+
const infra = capConfig[options.chain].infra;
9+
const assetAddresses = await fetchAssetAddresses(options, options.chain);
10+
const vaultConfigs = await fetchVaultConfigs(options);
11+
12+
const feesDistributedLogs = (
13+
await Promise.all(
14+
vaultConfigs
15+
.map((vaultConfig) =>
16+
vaultConfig.interestReceivers.map(async (interestReceiver) => {
17+
const logs = await options.getLogs({
18+
target: interestReceiver,
19+
eventAbi: capABI.FeeReceiver.FeesDistributedEvent,
20+
});
21+
22+
return logs.map((log) => ({
23+
feeAsset: vaultConfig.vault, // fee is collected in vault assets
24+
amount: log.amount,
25+
}));
26+
}),
27+
)
28+
.flat(),
29+
)
30+
).flat();
31+
const minterFees = options.createBalances();
32+
for (const { feeAsset, amount } of feesDistributedLogs) {
33+
minterFees.add(feeAsset, amount);
34+
}
35+
36+
const protocolFeeClaimedLogs = (
37+
await Promise.all(
38+
vaultConfigs
39+
.map((vaultConfig) =>
40+
vaultConfig.interestReceivers.map(async (interestReceiver) => {
41+
const logs = await options.getLogs({
42+
target: interestReceiver,
43+
eventAbi: capABI.FeeReceiver.ProtocolFeeClaimed,
44+
});
45+
46+
return logs.map((log) => ({
47+
feeAsset: vaultConfig.vault, // fee is collected in vault assets
48+
amount: log.amount,
49+
}));
50+
}),
51+
)
52+
.flat(),
53+
)
54+
).flat();
55+
const protocolFees = options.createBalances();
56+
for (const { feeAsset, amount } of protocolFeeClaimedLogs) {
57+
protocolFees.add(feeAsset, amount);
58+
}
59+
60+
const restakerFeesLogs = await options.getLogs({
61+
target: infra.delegation.address,
62+
eventAbi: capABI.Delegation.DistributeReward,
63+
});
64+
const restakerFees = options.createBalances();
65+
for (const log of restakerFeesLogs) {
66+
restakerFees.add(log.asset, log.amount);
67+
}
68+
69+
const insuranceFunds = vaultConfigs
70+
.map((i) => i.insuranceFund)
71+
.filter((i) => i !== null);
72+
const minters = vaultConfigs.map((i) => i.vault);
73+
const insuranceFundFees =
74+
insuranceFunds.length > 0
75+
? await addTokensReceived({
76+
options,
77+
fromAdddesses: minters,
78+
tokens: assetAddresses,
79+
targets: insuranceFunds,
80+
})
81+
: options.createBalances();
82+
83+
const dailyFees = options.createBalances();
84+
dailyFees.addBalances(minterFees);
85+
dailyFees.addBalances(protocolFees);
86+
dailyFees.addBalances(restakerFees);
87+
dailyFees.addBalances(insuranceFundFees);
88+
89+
const dailyRevenue = options.createBalances();
90+
dailyRevenue.addBalances(protocolFees);
91+
92+
const dailySupplySideRevenue = dailyFees.clone();
93+
dailySupplySideRevenue.subtract(dailyRevenue)
94+
95+
return {
96+
dailyFees,
97+
dailySupplySideRevenue,
98+
dailyRevenue: dailyRevenue,
99+
dailyProtocolRevenue: dailyRevenue,
100+
};
101+
};
102+
103+
const methodology = {
104+
Fees: "All fees paid by users for either borrowing (borrow fees + restaker fees) or minting (insurance fund fees).",
105+
Revenue: "Share of borrow fees for protocol",
106+
SupplySideRevenue: "Borrow fees distributed to stakers and restaker fees are distributed to delegators.",
107+
ProtocolRevenue: "Share of borrow fees for protocol",
108+
};
109+
110+
const adapter: SimpleAdapter = {
111+
version: 2,
112+
fetch,
113+
chains: [CHAIN.ETHEREUM],
114+
start: capConfig[CHAIN.ETHEREUM].fromDate,
115+
methodology,
116+
};
117+
118+
export default adapter;

0 commit comments

Comments
 (0)