Skip to content

Commit 2f62855

Browse files
authored
fix: aggregator limitations by chain (#99)
* added source swap fns for recipient per chain, aggregator limitations by chain * fix: disable hyperevm in bebop * fix: reenabled lifi & bebop for hyperevm * fix: add optional receiverAddress to fix hyperevm issue & deliberate about taker and receiver * feat: add object-arg wrappers with explicit takerAddress/receiverAddress to autochoice
1 parent 618c737 commit 2f62855

5 files changed

Lines changed: 237 additions & 14 deletions

File tree

src/xcs/autochoice.ts

Lines changed: 188 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,24 @@ import {
2424
import { Bytes } from "../types";
2525
import { Holding } from "./iface";
2626

27+
// Legacy per-holding shape used by the deprecated `*ByRecipient` positional fns. `recipient`
28+
// historically doubled as both swap executor (taker) and output destination (receiver) — that
29+
// conflation is the root of GS013-class bugs. New code should pass `HoldingWithSwapAddresses`
30+
// to the new wrappers below; this type stays for backwards compat. The optional
31+
// `receiverAddress` lets the new wrappers thread per-holding receiver through legacy code.
32+
export type HoldingWithRecipient = Holding & {
33+
recipient: Bytes;
34+
receiverAddress?: Bytes;
35+
};
36+
37+
// Per-holding shape for the new wrappers. Both `takerAddress` and `receiverAddress` are
38+
// required — even when source-side has them equal today, keeping both explicit forces every
39+
// call site to acknowledge each role.
40+
export type HoldingWithSwapAddresses = Holding & {
41+
takerAddress: Bytes;
42+
receiverAddress: Bytes;
43+
};
44+
2745
export class AutoSelectionError extends Error {}
2846
const safetyMultiplier = new Decimal("1.025");
2947

@@ -143,6 +161,10 @@ cots = [(1 COT, 1), (1 COT, 4)]
143161
5. return quotes and assets used.
144162
*/
145163

164+
/**
165+
* @deprecated Use {@link autoSelectSources} (object args; per-holding `takerAddress` and
166+
* `receiverAddress` on each `HoldingWithSwapAddresses`).
167+
*/
146168
export async function autoSelectSourcesV2(
147169
userAddress: Bytes,
148170
holdings: Holding[],
@@ -157,6 +179,32 @@ export async function autoSelectSourcesV2(
157179
idx: number;
158180
cur: Currency;
159181
}[];
182+
}> {
183+
return autoSelectSourcesV2ByRecipient(
184+
holdings.map((holding) => ({ ...holding, recipient: userAddress })),
185+
outputRequired,
186+
aggregators,
187+
commonCurrencyID,
188+
);
189+
}
190+
191+
/**
192+
* @deprecated Use {@link autoSelectSources} (object args; per-holding `takerAddress` and
193+
* `receiverAddress` on each `HoldingWithSwapAddresses`).
194+
*/
195+
export async function autoSelectSourcesV2ByRecipient(
196+
holdings: HoldingWithRecipient[],
197+
outputRequired: Decimal,
198+
aggregators: Aggregator[],
199+
commonCurrencyID: CurrencyID = CurrencyID.USDC,
200+
): Promise<{
201+
quoteResponses: QuoteResponse[];
202+
usedCOTs: {
203+
originalHolding: Holding;
204+
amountUsed: Decimal;
205+
idx: number;
206+
cur: Currency;
207+
}[];
160208
}> {
161209
// Assumption: Holding is already sorted in usage priority
162210
console.debug("XCS | SSV2:", {
@@ -217,7 +265,10 @@ export async function autoSelectSourcesV2(
217265
} else {
218266
fullLiquidationQuotes.push({
219267
req: {
220-
userAddress,
268+
userAddress: holding.recipient,
269+
// New wrappers thread per-holding receiver via this field. Falls back to taker
270+
// (`recipient`) when absent — preserves legacy positional-call behavior.
271+
receiverAddress: holding.receiverAddress,
221272
type: QuoteType.EXACT_IN,
222273
chain: chain.ChainID,
223274
inputToken: holding.tokenAddress,
@@ -460,11 +511,16 @@ export async function autoSelectSourcesV2(
460511
return { quoteResponses: final, usedCOTs };
461512
}
462513

514+
/**
515+
* @deprecated Use {@link getDestinationExactOutSwap} (object args; explicit `takerAddress` and
516+
* `receiverAddress`, both required).
517+
*/
463518
export async function determineDestinationSwaps(
464519
userAddress: Bytes,
465520
requirement: Holding,
466521
aggregators: Aggregator[],
467522
commonCurrencyID: CurrencyID = CurrencyID.USDC,
523+
receiverAddress?: Bytes,
468524
): Promise<QuoteResponse> {
469525
const chaindata = ChaindataMap.get(requirement.chainID);
470526
if (chaindata == null) {
@@ -483,6 +539,7 @@ export async function determineDestinationSwaps(
483539
type: QuoteType.EXACT_IN,
484540
chain: requirement.chainID,
485541
userAddress,
542+
receiverAddress,
486543
inputToken: requirement.tokenAddress,
487544
outputToken: COT.tokenAddress,
488545
inputAmount: requirement.amountRaw,
@@ -518,6 +575,7 @@ export async function determineDestinationSwaps(
518575
{
519576
type: QuoteType.EXACT_IN,
520577
userAddress,
578+
receiverAddress,
521579
chain: requirement.chainID,
522580
inputToken: COT.tokenAddress,
523581
outputToken: requirement.tokenAddress,
@@ -555,11 +613,34 @@ export async function determineDestinationSwaps(
555613
}
556614
}
557615

616+
/**
617+
* @deprecated Use {@link liquidateSourceHoldings} (object args; per-holding `takerAddress`
618+
* and `receiverAddress` on each `HoldingWithSwapAddresses`).
619+
*/
558620
export async function liquidateInputHoldings(
559621
userAddress: Bytes,
560622
holdings: Holding[],
561623
aggregators: Aggregator[],
562624
commonCurrencyID = CurrencyID.USDC,
625+
receiverAddress?: Bytes,
626+
): Promise<QuoteResponse[]> {
627+
return liquidateInputHoldingsByRecipient(
628+
holdings.map((holding) => ({ ...holding, recipient: userAddress })),
629+
aggregators,
630+
commonCurrencyID,
631+
receiverAddress,
632+
);
633+
}
634+
635+
/**
636+
* @deprecated Use {@link liquidateSourceHoldings} (object args; per-holding `takerAddress`
637+
* and `receiverAddress` on each `HoldingWithSwapAddresses`).
638+
*/
639+
export async function liquidateInputHoldingsByRecipient(
640+
holdings: HoldingWithRecipient[],
641+
aggregators: Aggregator[],
642+
commonCurrencyID = CurrencyID.USDC,
643+
receiverAddress?: Bytes,
563644
): Promise<QuoteResponse[]> {
564645
console.debug("XCS | LIH | Holdings:", holdings);
565646
const groupedByChainID = groupBy(holdings, (h) =>
@@ -607,7 +688,10 @@ export async function liquidateInputHoldings(
607688
}
608689
fullLiquidationQuotes.push({
609690
req: {
610-
userAddress,
691+
userAddress: holding.recipient,
692+
// Per-holding receiver wins (set by the new wrappers); shared param is the legacy
693+
// positional-call fallback.
694+
receiverAddress: holding.receiverAddress ?? receiverAddress,
611695
type: QuoteType.EXACT_IN,
612696
chain: chain.ChainID,
613697
inputToken: holding.tokenAddress,
@@ -645,13 +729,18 @@ export async function liquidateInputHoldings(
645729
return quotes;
646730
}
647731

732+
/**
733+
* @deprecated Use {@link getDestinationExactInSwap} (object args; explicit `takerAddress`
734+
* and `receiverAddress`, both required).
735+
*/
648736
export async function destinationSwapWithExactIn(
649737
userAddress: Bytes,
650738
omniChainID: OmniversalChainID,
651739
inputAmount: bigint,
652740
outputToken: Bytes,
653741
aggregators: Aggregator[],
654742
inputCurrency: CurrencyID = CurrencyID.USDC,
743+
receiverAddress?: Bytes,
655744
): Promise<QuoteResponse> {
656745
const chaindata = ChaindataMap.get(omniChainID);
657746
if (chaindata == null) {
@@ -669,6 +758,7 @@ export async function destinationSwapWithExactIn(
669758
type: QuoteType.EXACT_IN,
670759
chain: omniChainID,
671760
userAddress,
761+
receiverAddress,
672762
inputToken: COT.tokenAddress,
673763
outputToken: outputToken,
674764
inputAmount: inputAmount,
@@ -700,3 +790,99 @@ export async function destinationSwapWithExactIn(
700790
},
701791
};
702792
}
793+
794+
// =====================================================================================
795+
// Object-arg wrappers around the legacy positional functions above.
796+
//
797+
// Aggregator vocabulary:
798+
// takerAddress — on-chain executor of the swap (drives aggregator simulation /
799+
// permit / approval routing). On 7702 chains this is the ephemeral; on
800+
// non-Pectra chains it's the deployed Safe. Maps to the underlying
801+
// QuoteRequest's `userAddress`.
802+
// receiverAddress — recipient of the swap output. Maps to the underlying QuoteRequest's
803+
// `receiverAddress`. Required on all 4 wrappers — the GS013-class bug we
804+
// fixed came from forgetting this and silently defaulting to the wrong
805+
// address. Even on source side (where it equals the taker today), require
806+
// it explicitly so the type system forces every call site to acknowledge
807+
// both roles.
808+
//
809+
// Wrap-only: each wrapper delegates to the deprecated positional fn. No business logic added.
810+
// =====================================================================================
811+
812+
export async function getDestinationExactOutSwap(args: {
813+
takerAddress: Bytes;
814+
receiverAddress: Bytes;
815+
requirement: Holding;
816+
aggregators: Aggregator[];
817+
commonCurrencyID?: CurrencyID;
818+
}): Promise<QuoteResponse> {
819+
return determineDestinationSwaps(
820+
args.takerAddress,
821+
args.requirement,
822+
args.aggregators,
823+
args.commonCurrencyID,
824+
args.receiverAddress,
825+
);
826+
}
827+
828+
export async function getDestinationExactInSwap(args: {
829+
takerAddress: Bytes;
830+
receiverAddress: Bytes;
831+
chain: OmniversalChainID;
832+
inputAmount: bigint;
833+
outputToken: Bytes;
834+
aggregators: Aggregator[];
835+
inputCurrency?: CurrencyID;
836+
}): Promise<QuoteResponse> {
837+
return destinationSwapWithExactIn(
838+
args.takerAddress,
839+
args.chain,
840+
args.inputAmount,
841+
args.outputToken,
842+
args.aggregators,
843+
args.inputCurrency,
844+
args.receiverAddress,
845+
);
846+
}
847+
848+
export async function liquidateSourceHoldings(args: {
849+
holdings: HoldingWithSwapAddresses[];
850+
aggregators: Aggregator[];
851+
commonCurrencyID?: CurrencyID;
852+
}): Promise<QuoteResponse[]> {
853+
return liquidateInputHoldingsByRecipient(
854+
args.holdings.map((h) => ({
855+
...h,
856+
recipient: h.takerAddress,
857+
receiverAddress: h.receiverAddress,
858+
})),
859+
args.aggregators,
860+
args.commonCurrencyID,
861+
);
862+
}
863+
864+
export async function autoSelectSources(args: {
865+
holdings: HoldingWithSwapAddresses[];
866+
outputRequired: Decimal;
867+
aggregators: Aggregator[];
868+
commonCurrencyID?: CurrencyID;
869+
}): Promise<{
870+
quoteResponses: QuoteResponse[];
871+
usedCOTs: {
872+
originalHolding: Holding;
873+
amountUsed: Decimal;
874+
idx: number;
875+
cur: Currency;
876+
}[];
877+
}> {
878+
return autoSelectSourcesV2ByRecipient(
879+
args.holdings.map((h) => ({
880+
...h,
881+
recipient: h.takerAddress,
882+
receiverAddress: h.receiverAddress,
883+
})),
884+
args.outputRequired,
885+
args.aggregators,
886+
args.commonCurrencyID,
887+
);
888+
}

src/xcs/bebop-agg.ts

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -19,17 +19,11 @@ const ChainNameMapping = new Map(
1919
arbitrum: 42161,
2020
optimism: 10,
2121
base: 8453,
22-
taiko: 167000,
2322
bsc: 56,
24-
monadtestnet: 10143,
25-
megaethtestnet: 6342,
26-
berachain: 80094,
23+
avalanche: 43114,
2724
polygon: 137,
28-
zksync: 324,
29-
blast: 81457,
30-
mode: 34443,
3125
scroll: 534352,
32-
superseed: 5330,
26+
hyperevm: 999,
3327
}).map(([k, v]) => [bytesToHex(encodeChainID36(Universe.ETHEREUM, v)), k]),
3428
);
3529
// const erc7528Addr = Buffer.from('000000000000000000000000eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee', 'hex')
@@ -190,6 +184,10 @@ export class BebopAggregator implements Aggregator {
190184
const userAddrHex = getAddress(
191185
bytesToHex(r.userAddress.subarray(12)),
192186
);
187+
const receiverAddrHex =
188+
r.receiverAddress != null
189+
? getAddress(bytesToHex(r.receiverAddress.subarray(12)))
190+
: userAddrHex;
193191

194192
switch (r.type) {
195193
case QuoteType.EXACT_IN: {
@@ -200,6 +198,7 @@ export class BebopAggregator implements Aggregator {
200198
sell_tokens: inputTokenAddr,
201199
buy_tokens: outputTokenAddr,
202200
taker_address: userAddrHex,
201+
receiver_address: receiverAddrHex,
203202
sell_amounts: r.inputAmount.toString(),
204203
...BebopAggregator.COMMON_OPTIONS,
205204
},
@@ -214,6 +213,7 @@ export class BebopAggregator implements Aggregator {
214213
sell_tokens: inputTokenAddr,
215214
buy_tokens: outputTokenAddr,
216215
taker_address: userAddrHex,
216+
receiver_address: receiverAddrHex,
217217
buy_amounts: r.outputAmount.toString(),
218218
...BebopAggregator.COMMON_OPTIONS,
219219
},
@@ -248,7 +248,7 @@ export class BebopAggregator implements Aggregator {
248248
const outputAmountInDecimal = new Decimal(buyT.minimumAmount)
249249
.div(Decimal.pow(10, buyT.decimals))
250250
.toFixed(buyT.decimals);
251-
251+
252252
const inputAmountInDecimal = new Decimal(sellT.amount)
253253
.div(Decimal.pow(10, sellT.decimals))
254254
.toFixed(sellT.decimals);

src/xcs/fibrous-agg.ts

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ import {
2121
} from "./iface";
2222

2323
const ChainNameMapping = new ChainIDKeyedMap<string>([
24-
[new OmniversalChainID(Universe.ETHEREUM, 8453), "base"],
24+
// [new OmniversalChainID(Universe.ETHEREUM, 8453), "base"], // Disabled because of few liquidity issues
2525
[new OmniversalChainID(Universe.ETHEREUM, 999), "hyperevm"],
2626
[new OmniversalChainID(Universe.ETHEREUM, 143), "monad"],
2727
[new OmniversalChainID(Universe.ETHEREUM, 4114), "citrea"],
@@ -123,6 +123,10 @@ export class FibrousAggregator implements Aggregator {
123123
const userAddrHex = getAddress(
124124
bytesToHex(r.userAddress.subarray(12)),
125125
);
126+
const receiverAddrHex =
127+
r.receiverAddress != null
128+
? getAddress(bytesToHex(r.receiverAddress.subarray(12)))
129+
: userAddrHex;
126130

127131
let resp: AxiosResponse<FibrousResponse>;
128132
try {
@@ -134,7 +138,7 @@ export class FibrousAggregator implements Aggregator {
134138
tokenInAddress: inputTokenAddr,
135139
tokenOutAddress: outputTokenAddr,
136140
slippage: this.slippage,
137-
destination: userAddrHex,
141+
destination: receiverAddrHex,
138142
},
139143
});
140144
} catch (e) {
@@ -246,7 +250,10 @@ export class FibrousAggregator implements Aggregator {
246250
return item.value;
247251
}
248252
case "rejected": {
249-
console.error("Caught error in fetching Fibrous quotes:", item.reason);
253+
console.error(
254+
"Caught error in fetching Fibrous quotes:",
255+
item.reason,
256+
);
250257
return null;
251258
}
252259
}

src/xcs/iface.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,11 @@ export interface Quote {
5656

5757
type CommonQuoteParameters = {
5858
userAddress: Bytes;
59+
// Optional output recipient. When omitted, defaults to userAddress (output returns to
60+
// the caller — current behavior). When set, the aggregator delivers the swap output to
61+
// this address instead. Used by destination-leg swaps that want output to go straight
62+
// to the user's EOA rather than the wrapper account that signs the swap.
63+
receiverAddress?: Bytes;
5964
chain: OmniversalChainID;
6065
inputToken: Bytes;
6166
outputToken: Bytes;

0 commit comments

Comments
 (0)