Skip to content

Commit ef54e88

Browse files
committed
Support ERC-4646 token vaults
1 parent 2f3f043 commit ef54e88

5 files changed

Lines changed: 129 additions & 31 deletions

File tree

server/src/db/index.ts

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@ interface TokenRow {
3333
name: string
3434
symbol: string
3535
decimals: number
36+
erc4626AssetAddress: Address | null
37+
erc4626AssetDecimals: number | null
3638
}
3739

3840
interface BalanceRow {
@@ -71,8 +73,17 @@ async function createDatabase() {
7173
provider: migrator,
7274
})
7375

74-
await doit.migrateToLatest()
75-
console.log('Ran db migrations')
76+
const { results } = await doit.migrateToLatest()
77+
78+
if (results) {
79+
if (results.some((r) => r.status === 'Error')) {
80+
console.error('Failed migrations:', results)
81+
process.exit(1)
82+
} else {
83+
console.log('Ran db migrations')
84+
}
85+
}
86+
7687
return db
7788
}
7889

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import { Kysely } from 'kysely'
2+
3+
// Add columns to `tokens` to track ERC4626 assets
4+
// Note: SQLite doesn't seem to support adding multiple columns in a single query
5+
export const up = async (db: Kysely<any>) => {
6+
await db.schema
7+
.alterTable('tokens')
8+
.addColumn('erc4626AssetAddress', 'text')
9+
.execute()
10+
11+
await db.schema
12+
.alterTable('tokens')
13+
.addColumn('erc4626AssetDecimals', 'integer')
14+
.execute()
15+
}
16+
17+
export const down = async (db: Kysely<any>) => {
18+
await db.schema
19+
.alterTable('tokens')
20+
.dropColumn('erc4626AssetAddress')
21+
.execute()
22+
23+
await db.schema
24+
.alterTable('tokens')
25+
.dropColumn('erc4626AssetDecimals')
26+
.execute()
27+
}

server/src/db/migrator.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,9 @@ import type { MigrationProvider } from 'kysely'
55
// a directory of migrators as plain JavaScript files, which isn't true in Bun,
66
// in both dev mode and single-file executable mode.
77
import * as m1 from './migrations/001_initial_migrations'
8+
import * as m2 from './migrations/002_erc4626'
89

9-
const migrations = [m1]
10+
const migrations = [m1, m2]
1011

1112
export const migrator: MigrationProvider = {
1213
async getMigrations() {

server/src/handlers/tokens.ts

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
import type { Context } from 'hono'
2-
import { type Address, erc20Abi, isAddress, zeroAddress } from 'viem'
2+
import {
3+
type Address,
4+
erc20Abi,
5+
erc4626Abi,
6+
isAddress,
7+
zeroAddress,
8+
} from 'viem'
39
import { z } from 'zod'
410

511
import { getViemClient } from '../chains'
@@ -54,14 +60,15 @@ export async function addToken(c: Context) {
5460

5561
const contract = {
5662
address,
57-
abi: erc20Abi,
63+
abi: [...erc20Abi, ...erc4626Abi],
5864
}
5965

60-
const [name, symbol, decimals] = await client.multicall({
66+
const [name, symbol, decimals, erc4626Asset] = await client.multicall({
6167
contracts: [
6268
{ ...contract, functionName: 'name' },
6369
{ ...contract, functionName: 'symbol' },
6470
{ ...contract, functionName: 'decimals' },
71+
{ ...contract, functionName: 'asset' },
6572
],
6673
// This is needed when a `chain` object is not provided to viem
6774
// Using deployments from https://github.com/mds1/multicall3
@@ -72,6 +79,16 @@ export async function addToken(c: Context) {
7279
return c.json({ error: 'Failed to fetch token data' }, 409)
7380
}
7481

82+
let erc4626AssetDecimals: number | null = null
83+
84+
if (erc4626Asset.result) {
85+
erc4626AssetDecimals = await client.readContract({
86+
abi: erc20Abi,
87+
address: erc4626Asset.result,
88+
functionName: 'decimals',
89+
})
90+
}
91+
7592
await db
7693
.insertInto('tokens')
7794
.values({
@@ -80,6 +97,8 @@ export async function addToken(c: Context) {
8097
name: name.result,
8198
symbol: symbol.result,
8299
decimals: decimals.result,
100+
erc4626AssetAddress: erc4626Asset.result,
101+
erc4626AssetDecimals,
83102
})
84103
.onConflict((oc) => oc.doNothing())
85104
.execute()

server/src/queues/workers/erc20.ts

Lines changed: 65 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import type { Job } from 'bullmq'
22
import type { Insertable } from 'kysely'
3-
import { type Address, erc20Abi, formatUnits } from 'viem'
3+
import { type Address, erc20Abi, erc4626Abi, formatUnits } from 'viem'
44

55
import { getViemClient } from '../../chains'
66
import { type Tables, db } from '../../db'
@@ -19,12 +19,13 @@ type JobData = {
1919
export const erc20Queue = createQueue<JobData>('erc20')
2020
createWorker<JobData>(erc20Queue, processJob)
2121

22+
// TODO: Refactor this code such that offchain accounts can support ERC-4626. Currently it only supports onchain accounts.
2223
async function processJob(job: Job<JobData>) {
2324
const client = await getViemClient(job.data.chainId)
2425

2526
const token = await db
2627
.selectFrom('tokens')
27-
.select(['id', 'decimals'])
28+
.select(['id', 'decimals', 'erc4626AssetAddress', 'erc4626AssetDecimals'])
2829
.where('address', '=', job.data.token)
2930
.where('chain', '=', job.data.chainId)
3031
.executeTakeFirst()
@@ -36,6 +37,11 @@ async function processJob(job: Job<JobData>) {
3637
// Formatted balance, not the full bigint
3738
let balance: number
3839

40+
// Handle ERC4626
41+
const isErc4626 = !!token.erc4626AssetAddress
42+
const effectiveAddress = token.erc4626AssetAddress ?? job.data.token
43+
const effectiveDecimals = token.erc4626AssetDecimals ?? token.decimals
44+
3945
if (job.data.owner.address) {
4046
// Handle onchain account
4147
const rawBalance = await client.readContract({
@@ -45,7 +51,41 @@ async function processJob(job: Job<JobData>) {
4551
args: [job.data.owner.address],
4652
})
4753

48-
balance = Number(formatUnits(rawBalance, token.decimals))
54+
if (isErc4626) {
55+
// Handle ERC-4626 which provides a standard way to get the underlying asset of a yield-bearing token
56+
const underlyingAssetBalance = await client.readContract({
57+
abi: erc4626Abi,
58+
address: job.data.token,
59+
functionName: 'convertToAssets',
60+
args: [rawBalance],
61+
})
62+
balance = Number(formatUnits(underlyingAssetBalance, effectiveDecimals))
63+
} else {
64+
balance = Number(formatUnits(rawBalance, effectiveDecimals))
65+
}
66+
67+
const rateToEth = await getRateToEth({
68+
address: effectiveAddress,
69+
chainId: job.data.chainId,
70+
decimals: effectiveDecimals,
71+
})
72+
73+
const data: Insertable<Tables['balances']> = {
74+
token: token.id,
75+
owner: job.data.owner.id,
76+
balance,
77+
ethValue: balance * rateToEth,
78+
}
79+
80+
await db
81+
.insertInto('balances')
82+
.values(data)
83+
.onConflict((oc) =>
84+
oc
85+
.columns(['token', 'owner'])
86+
.doUpdateSet({ ...data, updatedAt: new Date().toISOString() })
87+
)
88+
.execute()
4989
} else {
5090
// Handle manual account (without an address)
5191
const balanceFromDb = await db
@@ -60,28 +100,28 @@ async function processJob(job: Job<JobData>) {
60100
}
61101

62102
balance = balanceFromDb.balance
63-
}
64103

65-
const rateToEth = await getRateToEth({
66-
address: job.data.token,
67-
chainId: job.data.chainId,
68-
decimals: token.decimals,
69-
})
70-
71-
const data: Insertable<Tables['balances']> = {
72-
token: token.id,
73-
owner: job.data.owner.id,
74-
balance,
75-
ethValue: balance * rateToEth,
76-
}
104+
const rateToEth = await getRateToEth({
105+
address: job.data.token,
106+
chainId: job.data.chainId,
107+
decimals: token.decimals,
108+
})
77109

78-
await db
79-
.insertInto('balances')
80-
.values(data)
81-
.onConflict((oc) =>
82-
oc
83-
.columns(['token', 'owner'])
84-
.doUpdateSet({ ...data, updatedAt: new Date().toISOString() })
85-
)
86-
.execute()
110+
const data: Insertable<Tables['balances']> = {
111+
token: token.id,
112+
owner: job.data.owner.id,
113+
balance,
114+
ethValue: balance * rateToEth,
115+
}
116+
117+
await db
118+
.insertInto('balances')
119+
.values(data)
120+
.onConflict((oc) =>
121+
oc
122+
.columns(['token', 'owner'])
123+
.doUpdateSet({ ...data, updatedAt: new Date().toISOString() })
124+
)
125+
.execute()
126+
}
87127
}

0 commit comments

Comments
 (0)