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
9 changes: 9 additions & 0 deletions assets/SOLV.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 2 additions & 2 deletions sdk/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion sdk/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@gobob/bob-sdk",
"version": "4.2.2",
"version": "4.2.3",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"scripts": {
Expand Down
160 changes: 139 additions & 21 deletions sdk/src/gateway/strategy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import {
type Token,
} from './types';
import { aaveV2AtokenAbi, compoundV2CTokenAbi } from './abi';
import { bob, optimism } from 'viem/chains';
import { bob, optimism, mainnet } from 'viem/chains';

const projectPointsIncentives: Map<string, PointsIncentive[]> = new Map([
[
Expand Down Expand Up @@ -160,6 +160,21 @@ const tokenToDefiLlamaPoolIdMap = new Map<string, string>([
// ],
]);

const tokenToSolvStrategyMap = new Map<string, string>([
[
// Solv BTC+
'0x4ca70811e831db42072cba1f0d03496ef126faad',
'BTC+',
],
[
// SolvBTC.JUP
'0x6b062aa7f5fc52b530cb13967ae2e6bc0d8dd3e4',
'Jupiter',
],
]);

const hybridBTCEndDate = new Date('2025-09-08T00:00:00.000Z');

export default class StrategyClient {
private viemClient: PublicClient;

Expand All @@ -180,39 +195,83 @@ export default class StrategyClient {
async getTokensIncentives(
tokens: string[]
): Promise<Pick<EnrichedToken, 'apyBase' | 'apyReward' | 'rewardTokens' | 'points'>[]> {
const res = await fetch('https://yields.llama.fi/pools');

const defillamaPools: DefiLlamaPool[] = res.ok ? (await res.json()).data : [];
const defillamaPoolMap = new Map<string, DefiLlamaPool>(
defillamaPools.filter((pool) => pool.chain === 'Bob').map((pool) => [pool.pool, pool])
);
const [defillamaPoolMap, solvAPYs] = await Promise.all([this.getDefillamaPools(), this.getSolvAPYs()]);

return tokens.map((token) => {
const tokenAddress = token.toLowerCase();

const strategyType = tokenToStrategyTypeMap.get(tokenAddress) ?? 'bob';
const points = [
...(projectPointsIncentives.get(strategyType) ?? []),
...(tokensPointsIncentives.get(tokenAddress) ?? []),
];

const defillamaPoolId = tokenToDefiLlamaPoolIdMap.get(tokenAddress) || '';
const defillamaPool = defillamaPoolMap.get(defillamaPoolId);

if (defillamaPool) {
return {
// HACK: set HybridBTC APY to 2% until 2025-09-08
apyBase:
defillamaPoolId === 'e8bfea35-ff6d-48db-aa08-51599b363219' && new Date() < hybridBTCEndDate
? 2
: (defillamaPool?.apyBase ?? 0),
apyReward: defillamaPool?.apyReward ?? 0,
rewardTokens: this.resolveTokens(defillamaPool?.rewardTokens),
points,
};
}

const solvStrategy = tokenToSolvStrategyMap.get(tokenAddress);

if (solvStrategy) {
return {
apyBase: solvAPYs[solvStrategy].apyBase,
apyReward: solvAPYs[solvStrategy].apyReward,
rewardTokens: this.resolveTokens(solvAPYs[solvStrategy].rewardTokens),
points,
};
}

return {
// HACK: set HybridBTC APY to 2%
apyBase: defillamaPoolId === 'e8bfea35-ff6d-48db-aa08-51599b363219' ? 2 : (defillamaPool?.apyBase ?? 0),
apyReward: defillamaPool?.apyReward ?? 0,
rewardTokens: (defillamaPool?.rewardTokens ?? [])
.map(
(addr) =>
ADDRESS_LOOKUP[bob.id][addr.toLowerCase()] ||
ADDRESS_LOOKUP[optimism.id][addr.toLowerCase()]
)
.filter(Boolean),
points: [
...(projectPointsIncentives.get(strategyType) ?? []),
...(tokensPointsIncentives.get(tokenAddress) ?? []),
],
apyBase: 0,
apyReward: 0,
rewardTokens: [],
points,
};
});
}

private async getDefillamaPools() {
try {
const res = await fetch('https://yields.llama.fi/pools');

const defillamaPools: DefiLlamaPool[] = res.ok ? (await res.json()).data : [];
const defillamaPoolMap = new Map<string, DefiLlamaPool>(
defillamaPools.filter((pool) => pool.chain === 'Bob').map((pool) => [pool.pool, pool])
);
return defillamaPoolMap;
} catch (err) {
console.error('Failed to fetch DefiLlama pools', err);
return new Map();
}
}

private resolveTokens(tokens: string[] | undefined | null): Token[] {
if (!tokens) {
return [];
}

return tokens
.map(
(addr) =>
ADDRESS_LOOKUP[bob.id]?.[addr.toLowerCase()] ??
ADDRESS_LOOKUP[optimism.id]?.[addr.toLowerCase()] ??
ADDRESS_LOOKUP[mainnet.id]?.[addr.toLowerCase()]
)
.filter((t): t is Token => Boolean(t));
}

async getStrategyAssetState(token: Token): Promise<StrategyAssetState> {
const strategyType = tokenToStrategyTypeMap.get(token.address.toLowerCase()) ?? 'bob';

Expand Down Expand Up @@ -356,4 +415,63 @@ export default class StrategyClient {
totalUnderlying: BigInt(Math.floor(tvl)),
};
}

private async getSolvAPYs() {
try {
const query = `
query SolvAPYs {
btcPlusStats(stageNo: 1) {
baseApy
rewardApy
tvl
}
lsts {
details {
protocol
apy
estApy
tvlUsd
}
}
}
`
.split('\n')
.join(' ');

const res = await fetch('https://sft-api.com/graphql', {
headers: {
'content-type': 'application/json',
Authorization: 'solv',
},
body: `{
"operationName": "SolvAPYs",
"variables": {},
"query": "${query}"
}`,
method: 'POST',
});

const data = await res.json();

const apys = {
'BTC+': {
apyBase: Number(data.data.btcPlusStats.baseApy),
apyReward: Number(data.data.btcPlusStats.rewardApy),
rewardTokens: ['0x04830a96a23ea718faa695a5aae74695aae3a23f'],
},
};

data.data.lsts.details.forEach((pool) => {
apys[pool.protocol] = {
apyBase: Number(pool.apy),
apyReward: 0,
};
});

return apys;
} catch (err) {
console.error('Failed to fetch APY data from Solv', err);
return {};
}
}
}
26 changes: 25 additions & 1 deletion sdk/src/gateway/tokens.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Token } from './types';
import { Address, isAddress } from 'viem';
import { bob, bobSepolia, optimism } from 'viem/chains';
import { bob, bobSepolia, mainnet, optimism } from 'viem/chains';

// TODO: re-write to use superchain tokenlist
const bobTokens = [
Expand Down Expand Up @@ -166,6 +166,20 @@ const optimismTokens = [
},
];

const ethereumTokens = [
{
name: 'Solv',
symbol: 'SOLV',
decimals: 18,
tokens: {
ethereum: {
address: '0x04830a96a23ea718faa695a5aae74695aae3a23f',
},
},
logoURI: 'https://raw.githubusercontent.com/bob-collective/bob/master/assets/SOLV.svg',
},
];
Comment on lines +169 to +181
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

Verify SOLV mainnet address/decimals before merge

Please confirm 0x04830a96a23ea718faa695a5aae74695aae3a23f and decimals: 18 are correct for SOLV on Ethereum.


🌐 Web query:

Confirm the official Ethereum mainnet contract address and decimals for the SOLV token (Solv Protocol).

💡 Result:

The Solv Protocol SOLV token on Ethereum (as listed on Etherscan) is: 0xD1D8c3f9a5609027A1c7cEF070aB9c51b99DCFf7 — decimals: 18. [1]

Note: there are other unrelated contracts named "SOLV" on Etherscan (e.g. 0xeb6b6...); Solv's docs state SOLV governance operates on BNB Chain (so please confirm which chain/token you mean if you need official project confirmation). [2]


Incorrect SOLV token address on Ethereum mainnet

The address configured in sdk/src/gateway/tokens.ts (0x04830a96a23ea718faa695a5aae74695aae3a23f) does not match the official SOLV (Solv Protocol) contract on Ethereum mainnet, which is 0xD1D8c3f9a5609027A1c7cEF070aB9c51b99DCFf7. The decimals: 18 value is correct.

🤖 Prompt for AI Agents
In sdk/src/gateway/tokens.ts around lines 169-181, the SOLV token entry uses an
incorrect Ethereum mainnet address; update the ethereum.tokens.ethereum.address
value to the official SOLV mainnet contract
0xD1D8c3f9a5609027A1c7cEF070aB9c51b99DCFf7 (preserve the checksummed casing),
keep decimals: 18 and existing metadata (name, symbol, logoURI) unchanged, then
run any token/address validation tests or linting to ensure the new address is
valid in the codebase.


const shoebillTokens = [
{
name: 'sb tBTC v2',
Expand Down Expand Up @@ -341,12 +355,16 @@ const TOKENS: Array<{
optimism?: {
address: string;
};
ethereum?: {
address: string;
};
};
logoURI: string;
}> = [
...bobTokens,
...bobSepoliaTokens,
...optimismTokens,
...ethereumTokens,
...shoebillTokens,
...segmentTokens,
...avalonTokens,
Expand All @@ -361,10 +379,12 @@ export const ADDRESS_LOOKUP: { [key in number]: { [key in string]: Token } } = {
SYMBOL_LOOKUP[bob.id] = {};
SYMBOL_LOOKUP[bobSepolia.id] = {};
SYMBOL_LOOKUP[optimism.id] = {};
SYMBOL_LOOKUP[mainnet.id] = {};

ADDRESS_LOOKUP[bob.id] = {};
ADDRESS_LOOKUP[bobSepolia.id] = {};
ADDRESS_LOOKUP[optimism.id] = {};
ADDRESS_LOOKUP[mainnet.id] = {};

function addToken(address: string, token: (typeof TOKENS)[number], chainId: number) {
const lowerAddress = address.toLowerCase();
Expand Down Expand Up @@ -393,6 +413,10 @@ for (const token of TOKENS) {
if (token.tokens.optimism) {
addToken(token.tokens.optimism.address, token, optimism.id);
}

if (token.tokens.ethereum) {
addToken(token.tokens.ethereum.address, token, mainnet.id);
}
Comment on lines +417 to +419
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

Mainnet registration path: LGTM. Ensure strategy wiring covers SOLV

The registration works. Please verify the new Solv strategy map consumes these mainnet entries.


🏁 Script executed:

#!/bin/bash
# Verify any strategy maps include the SOLV mainnet address.
rg -nC2 -i "0x04830a96a23ea718faa695a5aae74695aae3a23f|tokenToSolvStrategyMap|solv" --type ts

Length of output: 42793


Add SOLV mapping to tokenToSolvStrategyMap

tokenToSolvStrategyMap in sdk/src/gateway/strategy.ts currently only includes SolvBTC+ and Jupiter. It’s missing the SOLV mainnet address (0x04830a96a23ea718faa695a5aae74695aae3a23f), so SOLV won’t receive its APY strategy. Add an entry mapping that address to the correct Solv strategy ID.

🤖 Prompt for AI Agents
In sdk/src/gateway/tokens.ts around lines 417 to 419, tokenToSolvStrategyMap is
missing the SOLV mainnet address mapping; add an entry that maps the SOLV
mainnet address 0x04830a96a23ea718faa695a5aae74695aae3a23f to the correct Solv
strategy ID (use the existing Solv strategy constant/ID used for Solv tokens),
ensure the address is normalized the same way other keys are (lowercase or
checksummed) and add/update any related exports/tests so SOLV receives its APY
strategy.

}

export const tokenToStrategyTypeMap = new Map([
Expand Down
Loading