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
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
30 changes: 30 additions & 0 deletions config/monad.ts
Original file line number Diff line number Diff line change
Expand Up @@ -285,6 +285,36 @@ export default <NetworkData>{
},
],
},
{
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',
},
],
},
],
},
},
Expand Down
54 changes: 36 additions & 18 deletions modules/sor/lib/path.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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[],
Expand Down Expand Up @@ -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,
Expand All @@ -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({
Expand Down
92 changes: 92 additions & 0 deletions modules/sor/lib/router.test.ts
Original file line number Diff line number Diff line change
@@ -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();
});
});
});
6 changes: 5 additions & 1 deletion modules/sor/lib/router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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);
}
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
Loading