From 403f9cf498cbf355290d5793e21723ff27155209 Mon Sep 17 00:00:00 2001 From: Gosuto Inzasheru Date: Thu, 16 Jul 2026 10:44:01 +0200 Subject: [PATCH 1/3] fix(sor): drop swap paths exceeding buffer wrap/unwrap capacity (erc4626 maxDeposit/maxWithdraw) --- .changeset/sor-enforce-buffer-capacity.md | 5 ++ modules/sor/lib/path.ts | 54 ++++++++----- modules/sor/lib/router.test.ts | 92 +++++++++++++++++++++++ modules/sor/lib/router.ts | 6 +- 4 files changed, 138 insertions(+), 19 deletions(-) create mode 100644 .changeset/sor-enforce-buffer-capacity.md create mode 100644 modules/sor/lib/router.test.ts diff --git a/.changeset/sor-enforce-buffer-capacity.md b/.changeset/sor-enforce-buffer-capacity.md new file mode 100644 index 000000000..6e7ecd4bb --- /dev/null +++ b/.changeset/sor-enforce-buffer-capacity.md @@ -0,0 +1,5 @@ +--- +'backend': patch +--- + +SOR - drop swap paths with buffer steps that exceed wrap/unwrap capacity (erc4626 maxDeposit/maxWithdraw) instead of quoting swaps that revert onchain diff --git a/modules/sor/lib/path.ts b/modules/sor/lib/path.ts index 94260f261..f46b53dc8 100644 --- a/modules/sor/lib/path.ts +++ b/modules/sor/lib/path.ts @@ -33,6 +33,10 @@ export class PathWithAmount extends PathLocal { private readonly mutateBalances: boolean; private readonly printPath: any = []; public readonly swapStepsGreaterThanBufferLimit: number = 0; + // Steps whose amount exceeds the buffer's executable limit (buffer balance + wrap/unwrap + // capacity on the lending protocol, i.e. erc4626 maxDeposit/maxWithdraw). Such steps + // revert onchain, so paths containing them must not be quoted. + public readonly swapStepsExceedingBufferCapacity: number = 0; public constructor( tokens: Token[], @@ -65,15 +69,22 @@ export class PathWithAmount extends PathLocal { this.mutateBalances, ); amounts[i + 1] = outputAmount; - if ( - pool.poolType === 'Buffer' && - (pool as BufferPool).swapGivenInGreaterThanBufferLimit( - this.tokens[i], - this.tokens[i + 1], - amounts[i], - ) - ) { - this.swapStepsGreaterThanBufferLimit++; + if (pool.poolType === 'Buffer') { + if ( + (pool as BufferPool).swapGivenInGreaterThanBufferLimit( + this.tokens[i], + this.tokens[i + 1], + amounts[i], + ) + ) { + this.swapStepsGreaterThanBufferLimit++; + } + if ( + amounts[i].amount > + pool.getLimitAmountSwap(this.tokens[i], this.tokens[i + 1], SwapKind.GivenIn) + ) { + this.swapStepsExceedingBufferCapacity++; + } } this.printPath.push({ pool: pool.id, @@ -94,15 +105,22 @@ export class PathWithAmount extends PathLocal { amounts[i], this.mutateBalances, ); - if ( - pool.poolType === 'Buffer' && - (pool as BufferPool).swapGivenOutGreaterThanBufferLimit( - this.tokens[i - 1], - this.tokens[i], - amounts[i], - ) - ) { - this.swapStepsGreaterThanBufferLimit++; + if (pool.poolType === 'Buffer') { + if ( + (pool as BufferPool).swapGivenOutGreaterThanBufferLimit( + this.tokens[i - 1], + this.tokens[i], + amounts[i], + ) + ) { + this.swapStepsGreaterThanBufferLimit++; + } + if ( + amounts[i].amount > + pool.getLimitAmountSwap(this.tokens[i - 1], this.tokens[i], SwapKind.GivenOut) + ) { + this.swapStepsExceedingBufferCapacity++; + } } amounts[i - 1] = inputAmount; this.printPath.push({ diff --git a/modules/sor/lib/router.test.ts b/modules/sor/lib/router.test.ts new file mode 100644 index 000000000..7ad8d7439 --- /dev/null +++ b/modules/sor/lib/router.test.ts @@ -0,0 +1,92 @@ +import { SwapKind, Token, TokenAmount } from '@balancer/sdk'; +import { parseEther } from 'viem'; + +import { Router } from './router'; +import { PathLocal, PathWithAmount } from './path'; +import { BufferPool } from './poolsV3/buffer/bufferPool'; +import { BasePoolToken } from './utils/basePoolToken'; + +/** + * Regression scenario: a boosted pool swap that requires wrapping more underlying than the + * lending protocol accepts (erc4626 maxDeposit almost exhausted, e.g. an Aave market at its + * supply cap). The SOR used to quote these paths anyway and the swap reverted onchain with + * an opaque EstimateGasExecutionError. + */ +describe('Router buffer capacity limits', () => { + const chainId = 1; + const underlying = new Token(chainId, '0x000000000000000000000000000000000000aaa1', 6); + const wrapped = new Token(chainId, '0x000000000000000000000000000000000000bbb1', 6); + + // buffer holds 100/100, lending protocol has room for 696.89 more underlying + const bufferBalance = 100_000000n; + const maxDeposit = 696_890000n; + const maxWithdraw = 1_000_000_000000n; + + function createBufferPool(): BufferPool { + return new BufferPool( + '0x000000000000000000000000000000000000bbb1', + '0x000000000000000000000000000000000000bbb1', + chainId, + parseEther('1'), // 1:1 unwrap rate + new BasePoolToken(wrapped, bufferBalance, 0), + new BasePoolToken(underlying, bufferBalance, 1), + maxDeposit, + maxWithdraw, + ); + } + + function createWrapPath(): PathLocal { + return new PathLocal([underlying, wrapped], [createBufferPool()], [true]); + } + + describe('PathWithAmount.swapStepsExceedingBufferCapacity', () => { + it('flags a wrap that exceeds the lending protocol deposit capacity', () => { + const path = createWrapPath(); + const swapAmount = TokenAmount.fromRawAmount(underlying, 699_729300n); + + const pathWithAmount = new PathWithAmount(path.tokens, path.pools, path.isBuffer, swapAmount); + + expect(pathWithAmount.swapStepsExceedingBufferCapacity).toBe(1); + }); + + it('does not flag a wrap within the deposit capacity', () => { + const path = createWrapPath(); + const swapAmount = TokenAmount.fromRawAmount(underlying, 690_000000n); + + const pathWithAmount = new PathWithAmount(path.tokens, path.pools, path.isBuffer, swapAmount); + + expect(pathWithAmount.swapStepsExceedingBufferCapacity).toBe(0); + }); + }); + + describe('Router.getBestPaths', () => { + it('returns no paths when the only path exceeds buffer capacity', () => { + const router = new Router(); + const swapAmount = TokenAmount.fromRawAmount(underlying, 699_729300n); + + const bestPaths = router.getBestPaths([createWrapPath()], SwapKind.GivenIn, swapAmount); + + expect(bestPaths).toBeNull(); + }); + + it('returns a quote when the swap is within buffer capacity', () => { + const router = new Router(); + const swapAmount = TokenAmount.fromRawAmount(underlying, 690_000000n); + + const bestPaths = router.getBestPaths([createWrapPath()], SwapKind.GivenIn, swapAmount); + + expect(bestPaths).not.toBeNull(); + expect(bestPaths![0].outputAmount.amount).toBe(690_000000n); + }); + + it('returns no paths when a givenOut swap exceeds buffer capacity', () => { + const router = new Router(); + // givenOut wrap limit = buffer wrapped balance + maxDeposit room = 100 + 696.89 + const swapAmount = TokenAmount.fromRawAmount(wrapped, 800_000000n); + + const bestPaths = router.getBestPaths([createWrapPath()], SwapKind.GivenOut, swapAmount); + + expect(bestPaths).toBeNull(); + }); + }); +}); diff --git a/modules/sor/lib/router.ts b/modules/sor/lib/router.ts index 2075d303f..a2725fd70 100644 --- a/modules/sor/lib/router.ts +++ b/modules/sor/lib/router.ts @@ -84,6 +84,10 @@ export class Router { isHyperEvm && pathWithAmount.swapStepsGreaterThanBufferLimit > SWAPS_GREATER_THAN_BUFFER_LIMIT_THRESHOLD; + // remove paths with buffer steps that would revert onchain because the swap + // amount exceeds the buffer's capacity (buffer balance + erc4626 maxDeposit/maxWithdraw) + const exceedsBufferCapacity = pathWithAmount.swapStepsExceedingBufferCapacity > 0; + /** * Remove paths that return 0 amount * It usually happens when low swapAmounts are provided and return amounts rounded down to zero @@ -92,7 +96,7 @@ export class Router { pathWithAmount.swapKind === SwapKind.GivenIn ? pathWithAmount.outputAmount : pathWithAmount.inputAmount; - if (calculatedAmount.amount > 0n && !gasCostTooHigh) { + if (calculatedAmount.amount > 0n && !gasCostTooHigh && !exceedsBufferCapacity) { quotePathsByRatio[i].push(pathWithAmount); selectedPaths.push(path); } From 40df958f39f237191b77faeb0cd1e067418222ac Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Thu, 13 Aug 2026 12:58:55 +0000 Subject: [PATCH 2/3] add maple syrupUSDC and ethena sUSDe APRs on monad Reuse the existing Maple and Ethena HTTP APR feeds from mainnet, mapped to the Monad token addresses for syrupUSDC and sUSDe. Co-authored-by: gosuto.eth --- .changeset/monad-syrup-susde-apr.md | 5 +++++ config/monad.ts | 30 +++++++++++++++++++++++++++++ 2 files changed, 35 insertions(+) create mode 100644 .changeset/monad-syrup-susde-apr.md diff --git a/.changeset/monad-syrup-susde-apr.md b/.changeset/monad-syrup-susde-apr.md new file mode 100644 index 000000000..9da4f88d7 --- /dev/null +++ b/.changeset/monad-syrup-susde-apr.md @@ -0,0 +1,5 @@ +--- +'backend': patch +--- + +add maple syrupUSDC and ethena sUSDe APRs on monad diff --git a/config/monad.ts b/config/monad.ts index e7ef3be86..55cd5af50 100644 --- a/config/monad.ts +++ b/config/monad.ts @@ -285,6 +285,36 @@ export default { }, ], }, + { + url: 'https://api.maple.finance/v2/graphql', + body: JSON.stringify({ + query: `{ + syrupGlobals { + apy + } + }`, + }), + headers: { 'Content-Type': 'application/json' }, + scale: 1e30, + extractors: [ + { + type: 'path', + token: '0xab6e5a0c3799d020c790d34f7b2c02639e238af7', + path: '$.data.syrupGlobals.apy', + }, + ], + }, + { + url: 'https://ethena.fi/api/yields/protocol-and-staking-yield', + scale: 100, + extractors: [ + { + type: 'path', + token: '0x211cc4dd073734da055fbf44a2b4667d5e5fe5d2', + path: '$.stakingYield.value', + }, + ], + }, ], }, }, From e63c2e97c875e9d37b87cc44b06de9478e49ca88 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 13 Aug 2026 18:36:40 +0000 Subject: [PATCH 3/3] Version Packages --- .changeset/monad-syrup-susde-apr.md | 5 ----- .changeset/sor-enforce-buffer-capacity.md | 5 ----- CHANGELOG.md | 7 +++++++ package.json | 2 +- 4 files changed, 8 insertions(+), 11 deletions(-) delete mode 100644 .changeset/monad-syrup-susde-apr.md delete mode 100644 .changeset/sor-enforce-buffer-capacity.md diff --git a/.changeset/monad-syrup-susde-apr.md b/.changeset/monad-syrup-susde-apr.md deleted file mode 100644 index 9da4f88d7..000000000 --- a/.changeset/monad-syrup-susde-apr.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'backend': patch ---- - -add maple syrupUSDC and ethena sUSDe APRs on monad diff --git a/.changeset/sor-enforce-buffer-capacity.md b/.changeset/sor-enforce-buffer-capacity.md deleted file mode 100644 index 6e7ecd4bb..000000000 --- a/.changeset/sor-enforce-buffer-capacity.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'backend': patch ---- - -SOR - drop swap paths with buffer steps that exceed wrap/unwrap capacity (erc4626 maxDeposit/maxWithdraw) instead of quoting swaps that revert onchain diff --git a/CHANGELOG.md b/CHANGELOG.md index f0818b6d2..85976548a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # backend +## 2.1.20 + +### Patch Changes + +- 40df958: add maple syrupUSDC and ethena sUSDe APRs on monad +- 403f9cf: SOR - drop swap paths with buffer steps that exceed wrap/unwrap capacity (erc4626 maxDeposit/maxWithdraw) instead of quoting swaps that revert onchain + ## 2.1.19 ### Patch Changes diff --git a/package.json b/package.json index 1d11edec8..b9a9b4fd5 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "backend", - "version": "2.1.19", + "version": "2.1.20", "description": "Backend service for Beethoven X and Balancer", "repository": "https://github.com/balancer/backend", "author": "Beethoven X",