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/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', + }, + ], + }, ], }, }, 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); } 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",