Skip to content

Commit 9f4a2e4

Browse files
authored
Merge pull request #2230 from pawell24/feat/accountable
Add Accountable integration for loan vaults and yield calculations
2 parents 3e7b51c + 1750a60 commit 9f4a2e4

File tree

1 file changed

+174
-0
lines changed

1 file changed

+174
-0
lines changed

src/adaptors/accountable/index.js

Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
const sdk = require('@defillama/sdk');
2+
const axios = require('axios');
3+
const utils = require('../utils');
4+
5+
const API_URL = 'https://yield.accountable.capital/api/loan';
6+
const MERKL_API_URL = 'https://api.merkl.xyz/v4/opportunities?explorerAddress=';
7+
const chainIdToName = { 143: 'monad' };
8+
const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000';
9+
10+
const abis = {
11+
asset: 'function asset() view returns (address)',
12+
convertToAssets: 'function convertToAssets(uint256 shares) view returns (uint256)',
13+
};
14+
15+
const basisPointsToPercent = (value) => Number(value) / 1e4;
16+
const formatAmount = (value, decimals = 18) => (value == null ? null : Number(value) / 10 ** decimals);
17+
18+
const fetchVaultsByLoanIds = async(loanIds) => {
19+
const results = await Promise.allSettled(
20+
loanIds.map((id) => utils.getData(`${API_URL}/${id}`))
21+
);
22+
23+
return results.reduce((acc, res, idx) => {
24+
if (res.status !== 'fulfilled') return acc;
25+
const vault = res.value.on_chain_loan.loan.vault;
26+
if (vault && vault !== ZERO_ADDRESS)
27+
acc[loanIds[idx]] = vault.toLowerCase();
28+
return acc;
29+
}, {});
30+
};
31+
32+
const getVaultAddressesFromApi = async() => {
33+
const { items } = await utils.getData(API_URL);
34+
const vaults = items
35+
.map((item) => item.on_chain_loan.loan.vault)
36+
.filter((addr) => addr && addr !== ZERO_ADDRESS)
37+
.map((addr) => addr.toLowerCase());
38+
return Array.from(new Set(vaults));
39+
};
40+
41+
const getVaultStats = async(vaults, chain = 'monad') => {
42+
if (!vaults.length) return {};
43+
44+
const [suppliesRes, underlyingsRes] = await Promise.all([
45+
sdk.api.abi.multiCall({
46+
chain,
47+
abi: 'erc20:totalSupply',
48+
calls: vaults.map((vault) => ({ target: vault })),
49+
permitFailure: true,
50+
}),
51+
sdk.api.abi.multiCall({
52+
chain,
53+
abi: abis.asset,
54+
calls: vaults.map((vault) => ({ target: vault })),
55+
permitFailure: true,
56+
}),
57+
]);
58+
59+
const supplies = suppliesRes.output.map((o) => o.output);
60+
const underlyings = underlyingsRes.output.map((o) => o.output);
61+
62+
const [totalAssetsRes, liquidityRes] = await Promise.all([
63+
sdk.api.abi.multiCall({
64+
chain,
65+
abi: abis.convertToAssets,
66+
calls: vaults.map((vault, i) => ({
67+
target: vault,
68+
params: [supplies[i]],
69+
})),
70+
permitFailure: true,
71+
}),
72+
sdk.api.abi.multiCall({
73+
chain,
74+
abi: 'erc20:balanceOf',
75+
calls: vaults.map((vault, i) => ({
76+
target: underlyings[i],
77+
params: vault,
78+
})),
79+
permitFailure: true,
80+
}),
81+
]);
82+
83+
const totalAssets = totalAssetsRes.output.map((o) => o.output);
84+
const liquidity = liquidityRes.output.map((o) => o.output);
85+
return vaults.reduce((acc, address, i) => {
86+
acc[address] = {
87+
totalSupplied: supplies[i],
88+
totalBorrowed: Number(totalAssets[i]) - Number(liquidity[i] || 0),
89+
tvl: liquidity[i],
90+
};
91+
return acc;
92+
}, {});
93+
};
94+
95+
const fetchMerklRewards = async(vaultAddress) => {
96+
try {
97+
const { data } = await axios.get(`${MERKL_API_URL}${vaultAddress}`);
98+
const opp =
99+
Array.isArray(data) &&
100+
(data.find((item) => item?.explorerAddress?.toLowerCase() === vaultAddress.toLowerCase()) || data[0]);
101+
if (!opp) return [];
102+
return (
103+
opp.rewardsRecord?.breakdowns
104+
?.map((b) => b?.token?.address?.toLowerCase())
105+
.filter(Boolean) || []
106+
);
107+
} catch (e) {
108+
return [];
109+
}
110+
};
111+
112+
const fetchBreakdowns = async(loanIds) => {
113+
const results = await Promise.allSettled(
114+
loanIds.map((id) => utils.getData(`${API_URL}/${id}/apy/breakdown`))
115+
);
116+
117+
return results.reduce((acc, res, idx) => {
118+
if (res.status !== 'fulfilled') return acc;
119+
acc[loanIds[idx]] = res.value || {};
120+
return acc;
121+
}, {});
122+
};
123+
124+
const apy = async() => {
125+
const { items } = await utils.getData(API_URL);
126+
const activeLoans = items.filter((item) => item.loan_state === 3);
127+
const loanIds = activeLoans.map((item) => item.id);
128+
129+
const loanVaultMap = await fetchVaultsByLoanIds(loanIds);
130+
const vaultAddresses = Object.values(loanVaultMap);
131+
const vaultStats = await getVaultStats(vaultAddresses);
132+
const breakdowns = await fetchBreakdowns(loanIds);
133+
134+
return Promise.all(
135+
activeLoans.map(async(item) => {
136+
const chainName = chainIdToName[item.chain_id] || 'unknown';
137+
const vaultAddress = loanVaultMap[item.id];
138+
const stats = vaultAddress ? vaultStats[vaultAddress] || {} : {};
139+
const pointBoosts = item?.all_points_apy_boost?.boosts_by_points || [];
140+
const pointRewardApy = pointBoosts.reduce((sum, b) => sum + Number(b?.apy_boost_percent || 0), 0);
141+
const pointRewardTokens = pointBoosts.map((b) => b?.point_name).filter(Boolean);
142+
143+
const breakdown = breakdowns[item.id] || {};
144+
const merklApy = breakdown?.merkle_apy ?? 0;
145+
const nativeApy = breakdown?.native_apy ?? basisPointsToPercent(item.apy);
146+
const perfFee = breakdown?.performance_fee ?? 0;
147+
148+
const totalApyReward = (merklApy ?? 0) + pointRewardApy || null;
149+
const merklTokens = vaultAddress ? await fetchMerklRewards(vaultAddress) : [];
150+
const combinedRewardTokens = Array.from(
151+
new Set([...(merklTokens || []), ...pointRewardTokens])
152+
);
153+
154+
return {
155+
pool: `${item.loan_address}-${chainName}`.toLowerCase(),
156+
chain: utils.formatChain(chainName),
157+
project: 'accountable',
158+
symbol: utils.formatSymbol(item.asset_symbol),
159+
tvlUsd: formatAmount(stats.tvl, 6),
160+
apyBase: nativeApy + perfFee,
161+
apyReward: totalApyReward,
162+
rewardTokens: combinedRewardTokens,
163+
url: `https://yield.accountable.capital/vaults/${item.loan_address}`,
164+
totalSupplyUsd: formatAmount(stats.totalSupplied, 6),
165+
totalBorrowUsd: formatAmount(stats.totalBorrowed, 6),
166+
};
167+
})
168+
);
169+
};
170+
171+
module.exports = {
172+
timetravel: false,
173+
apy,
174+
};

0 commit comments

Comments
 (0)