Skip to content

feat: add feeConfig() fallback and LockedYvUSD locker metadata#367

Merged
matheus1lva merged 4 commits intomainfrom
feat/yvusd-stablecoin-support
Mar 20, 2026
Merged

feat: add feeConfig() fallback and LockedYvUSD locker metadata#367
matheus1lva merged 4 commits intomainfrom
feat/yvusd-stablecoin-support

Conversation

@matheus1lva
Copy link
Copy Markdown
Collaborator

@matheus1lva matheus1lva commented Mar 17, 2026

Summary

yvUSD's accountant (LockedYvUSD) exposes fees via feeConfig() instead of the standard getVaultConfig(vault) / defaultConfig() methods. Kong's extractFeesBps silently fell through to 0/0 when both standard calls reverted. This adds feeConfig() as a third fallback so fees are read correctly if they ever change.

Also indexes LockedYvUSD-specific metadata (cooldownDuration, withdrawalWindow, lockerBonus) from the accountant and exposes it as a locker field on the vault snapshot and GraphQL schema.

Closes #360

How to review

  • packages/ingest/abis/yearn/3/vault/snapshot/hook.ts
    • extractFeesBps() now tries defaultConfig() and then feeConfig() before falling back
    • new extractLockerMeta() helper reads cooldownDuration(), withdrawalWindow(), and feeConfig()[2]
    • process() now includes locker in the returned snapshot hook
  • packages/web/app/api/gql/typeDefs/vault.ts
    • adds Locker GraphQL type
    • adds locker: Locker to Vault

Manual validation

  1. Confirm the source-of-truth contract behavior for the yvUSD accountant.
cast call 0x696d02Db93291651ED510704c9b286841d506987 "accountant()(address)"
cast call 0xAaaFEa48472f77563961Cdb53291DEDfB46F9040 "feeConfig()(uint16,uint16,uint16)"
cast call 0xAaaFEa48472f77563961Cdb53291DEDfB46F9040 "cooldownDuration()(uint256)"
cast call 0xAaaFEa48472f77563961Cdb53291DEDfB46F9040 "withdrawalWindow()(uint256)"
cast call 0xAaaFEa48472f77563961Cdb53291DEDfB46F9040 "getVaultConfig(address)(uint16,uint16,uint16)" 0x696d02Db93291651ED510704c9b286841d506987
cast call 0xAaaFEa48472f77563961Cdb53291DEDfB46F9040 "defaultConfig()(uint16,uint16,uint16)"

Expected:

  • accountant() returns 0xAaaFEa48472f77563961Cdb53291DEDfB46F9040
  • feeConfig() returns 0 0 1000
  • cooldownDuration() returns 1209600
  • withdrawalWindow() returns 432000
  • getVaultConfig(...) reverts
  • defaultConfig() reverts
  1. Write config/abis.local.yaml before starting Kong so the local process boots with the narrowed ABI set.
cat > config/abis.local.yaml <<'EOF'
cron:
  name: AbiFanout
  queue: fanout
  job: abis
  schedule: '*/15 * * * *'
  start: false

abis:
  - abiPath: 'yearn/3/vault'
    sources:
      - { chainId: 1, address: '0x696d02Db93291651ED510704c9b286841d506987', inceptBlock: 24271831 }
      - { chainId: 1, address: '0xAaaFEa48472f77563961Cdb53291DEDfB46F9040', inceptBlock: 24329199 }

  - abiPath: 'yearn/3/accountant'
    sources:
      - { chainId: 1, address: '0xAaaFEa48472f77563961Cdb53291DEDfB46F9040', inceptBlock: 24329199 }
EOF

config/abis.local.yaml is a full local override for config/abis.yaml, so keep it minimal and delete it when you are done validating.

  1. Start Kong locally and wait for the API to come up.
make dev

Wait until the app is serving successfully and /api/mq responds.

  1. Trigger ABI fanout so only those entries are loaded or re-processed.

In the terminal UI:

  • Ingest
  • fanout abis
  • confirm the action

This fanout run will now use config/abis.local.yaml, so it should only queue work for the yvUSD vault and the LockedYvUSD accountant.

  1. Query the yvUSD vault through GraphQL.
curl -sS http://localhost:3000/api/gql \
  -H 'Content-Type: application/json' \
  --data '{"query":"query { vault(chainId: 1, address: \"0x696d02Db93291651ED510704c9b286841d506987\") { address name symbol accountant fees { managementFee performanceFee } locker { cooldownDuration withdrawalWindow lockerBonus } } }"}' \
  | jq '.data.vault'

Expected:

{
  "address": "0x696d02Db93291651ED510704c9b286841d506987",
  "name": "USD yVault",
  "symbol": "yvUSD",
  "accountant": "0xAaaFEa48472f77563961Cdb53291DEDfB46F9040",
  "fees": {
    "managementFee": 0,
    "performanceFee": 0
  },
  "locker": {
    "cooldownDuration": 1209600,
    "withdrawalWindow": 432000,
    "lockerBonus": 1000
  }
}
  1. Sanity-check the behavior being fixed.

The important part is that fees are still resolved correctly even though the accountant does not implement the standard getVaultConfig(vault) / defaultConfig() interface, and that the locker-specific fields are now exposed on the vault response.

Note: issue #360 listed withdrawalWindow = 604800, but the live contract currently returns 432000. The implementation is intentionally following the on-chain value.

  1. Remove the local override when finished.
rm config/abis.local.yaml

If you want to go back to the full ABI set in the same local environment, restart Kong after removing the file.

Risk / impact

  • extractLockerMeta() is additive and returns undefined for non-locker accountants
  • feeConfig() fallback only runs when both standard accountant methods revert, so existing standard accountants keep their current behavior

yvUSD's accountant (LockedYvUSD) exposes fees via feeConfig() instead of
the standard getVaultConfig()/defaultConfig() methods. Add feeConfig() as a
third fallback in extractFeesBps so fees are read correctly if they change
from the current 0/0.

Also introduce extractLockerMeta() which probes cooldownDuration(),
withdrawalWindow(), and feeConfig()[2] (lockerBonus) on the accountant.
Returns undefined for non-locker accountants. Exposes the locker object in
the vault snapshot and GraphQL schema.

Closes #360
@vercel
Copy link
Copy Markdown

vercel bot commented Mar 17, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
kong Ready Ready Preview, Comment Mar 20, 2026 7:54pm

Request Review

@matheus1lva matheus1lva marked this pull request as ready for review March 18, 2026 21:24
@matheus1lva matheus1lva requested a review from murderteeth March 18, 2026 21:29
Copy link
Copy Markdown
Contributor

@murderteeth murderteeth left a comment

Choose a reason for hiding this comment

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

Overall: Approve with suggestions

The fee fallback chain and locker metadata extraction are correct and well-guarded. Two suggestions to tighten up the RPC footprint.


1. Gate extractLockerMeta on whether the accountant is also a vault

packages/ingest/abis/yearn/3/vault/snapshot/hook.ts:106

Currently extractLockerMeta runs for every vault with a non-zero accountant, adding 3 RPC probes that revert for standard accountants. Since LockedYvUSD is itself a vault, you can gate on that:

const locker = snapshot.accountant && snapshot.accountant !== zeroAddress
  && await things.exist(chainId, snapshot.accountant, 'vault')
  ? await extractLockerMeta(chainId, snapshot.accountant)
  : undefined

Zero extra RPCs for non-locker vaults, no hardcoded addresses.

2. Use multicall in extractLockerMeta

packages/ingest/abis/yearn/3/vault/snapshot/hook.ts:525

The three separate readContract calls can be a single multicall:

async function extractLockerMeta(chainId: number, accountant: `0x${string}`) {
  const results = await rpcs.next(chainId).multicall({
    contracts: [
      { address: accountant, abi: parseAbi(['function cooldownDuration() view returns (uint256)']), functionName: 'cooldownDuration' },
      { address: accountant, abi: parseAbi(['function withdrawalWindow() view returns (uint256)']), functionName: 'withdrawalWindow' },
      { address: accountant, abi: parseAbi(['function feeConfig() view returns (uint16, uint16, uint16)']), functionName: 'feeConfig' },
    ],
    allowFailure: true,
  })
  if (results[0].status === 'failure') return undefined
  return {
    cooldownDuration: Number(results[0].result),
    withdrawalWindow: Number(results[1].result),
    lockerBonus: results[2].status === 'success' ? results[2].result[2] : 0,
  }
}

One RPC call instead of three.

Copy link
Copy Markdown
Contributor

@murderteeth murderteeth left a comment

Choose a reason for hiding this comment

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

Updating previous review — these should be request for changes, not approve.

1. Gate extractLockerMeta on whether the accountant is also a vault

packages/ingest/abis/yearn/3/vault/snapshot/hook.ts:106

Currently extractLockerMeta runs for every vault with a non-zero accountant, adding 3 RPC probes that revert for standard accountants. Since LockedYvUSD is itself a vault, you can gate on that:

const locker = snapshot.accountant && snapshot.accountant !== zeroAddress
  && await things.exist(chainId, snapshot.accountant, 'vault')
  ? await extractLockerMeta(chainId, snapshot.accountant)
  : undefined

Zero extra RPCs for non-locker vaults, no hardcoded addresses.

2. Use multicall in extractLockerMeta

packages/ingest/abis/yearn/3/vault/snapshot/hook.ts:525

The three separate readContract calls can be a single multicall:

async function extractLockerMeta(chainId: number, accountant: `0x${string}`) {
  const results = await rpcs.next(chainId).multicall({
    contracts: [
      { address: accountant, abi: parseAbi(['function cooldownDuration() view returns (uint256)']), functionName: 'cooldownDuration' },
      { address: accountant, abi: parseAbi(['function withdrawalWindow() view returns (uint256)']), functionName: 'withdrawalWindow' },
      { address: accountant, abi: parseAbi(['function feeConfig() view returns (uint16, uint16, uint16)']), functionName: 'feeConfig' },
    ],
    allowFailure: true,
  })
  if (results[0].status === 'failure') return undefined
  return {
    cooldownDuration: Number(results[0].result),
    withdrawalWindow: Number(results[1].result),
    lockerBonus: results[2].status === 'success' ? results[2].result[2] : 0,
  }
}

One RPC call instead of three.

Copy link
Copy Markdown
Contributor

@murderteeth murderteeth left a comment

Choose a reason for hiding this comment

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

Previous suggestions (vault gate + multicall) are addressed in 9837bc2.

Lint errors in extractLockerMeta

packages/ingest/abis/yearn/3/vault/snapshot/hook.ts:531-543

The contract objects in the multicall array use 8-space indentation but the linter expects 10. This causes 9 indent errors and fails bun --filter ingest lint. Fix the indentation inside each contract object in the contracts array:

const [cooldownDuration, withdrawalWindow, feeConfig] = await rpcs.next(chainId).multicall({
  contracts: [
    {
      address: accountant,
      abi: parseAbi(['function cooldownDuration() view returns (uint256)']),
      functionName: 'cooldownDuration',
    },
    {
      address: accountant,
      abi: parseAbi(['function withdrawalWindow() view returns (uint256)']),
      functionName: 'withdrawalWindow',
    },
    {
      address: accountant,
      abi: parseAbi(['function feeConfig() view returns (uint16, uint16, uint16)']),
      functionName: 'feeConfig',
    },
  ],
  allowFailure: true,
})

@matheus1lva
Copy link
Copy Markdown
Collaborator Author

Previous suggestions (vault gate + multicall) are addressed in 9837bc2.

Lint errors in extractLockerMeta

packages/ingest/abis/yearn/3/vault/snapshot/hook.ts:531-543

The contract objects in the multicall array use 8-space indentation but the linter expects 10. This causes 9 indent errors and fails bun --filter ingest lint. Fix the indentation inside each contract object in the contracts array:

const [cooldownDuration, withdrawalWindow, feeConfig] = await rpcs.next(chainId).multicall({
  contracts: [
    {
      address: accountant,
      abi: parseAbi(['function cooldownDuration() view returns (uint256)']),
      functionName: 'cooldownDuration',
    },
    {
      address: accountant,
      abi: parseAbi(['function withdrawalWindow() view returns (uint256)']),
      functionName: 'withdrawalWindow',
    },
    {
      address: accountant,
      abi: parseAbi(['function feeConfig() view returns (uint16, uint16, uint16)']),
      functionName: 'feeConfig',
    },
  ],
  allowFailure: true,
})

Lint fixed!

@matheus1lva matheus1lva merged commit 0df705a into main Mar 20, 2026
2 checks passed
@matheus1lva matheus1lva deleted the feat/yvusd-stablecoin-support branch March 20, 2026 20:35
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

yvUSD/LockedYvUSD: accountant uses feeConfig() not getVaultConfig(), plus missing locker metadata

2 participants