1- import { OrderV3 } from "./abis" ;
21import { SgOrder } from "./query" ;
32import { Span } from "@opentelemetry/api" ;
43import { hexlify } from "ethers/lib/utils" ;
54import { addWatchedToken } from "./account" ;
5+ import { orderbookAbi , OrderV3 } from "./abis" ;
66import { getTokenSymbol , shuffleArray } from "./utils" ;
7- import { decodeAbiParameters , parseAbiParameters } from "viem" ;
7+ import { decodeAbiParameters , erc20Abi , parseAbi , parseAbiParameters } from "viem" ;
88import {
99 Pair ,
1010 Order ,
11+ Vault ,
12+ OTOVMap ,
1113 ViemClient ,
14+ OwnersVaults ,
1215 TokenDetails ,
1316 BundledOrders ,
1417 OrdersProfileMap ,
1518 OwnersProfileMap ,
19+ TokensOwnersVaults ,
1620 OrderbooksOwnersProfileMap ,
1721} from "./types" ;
1822
@@ -47,6 +51,7 @@ export function toOrder(orderLog: any): Order {
4751 * Get all pairs of an order
4852 */
4953export async function getOrderPairs (
54+ orderHash : string ,
5055 orderStruct : Order ,
5156 viemClient : ViemClient ,
5257 tokens : TokenDetails [ ] ,
@@ -112,10 +117,13 @@ export async function getOrderPairs(
112117 sellTokenSymbol : _outputSymbol ,
113118 sellTokenDecimals : _output . decimals ,
114119 takeOrder : {
115- order : orderStruct ,
116- inputIOIndex : k ,
117- outputIOIndex : j ,
118- signedContext : [ ] ,
120+ id : orderHash ,
121+ takeOrder : {
122+ order : orderStruct ,
123+ inputIOIndex : k ,
124+ outputIOIndex : j ,
125+ signedContext : [ ] ,
126+ } ,
119127 } ,
120128 } ) ;
121129 }
@@ -137,6 +145,7 @@ export async function handleAddOrderbookOwnersProfileMap(
137145 const changes : Record < string , string [ ] > = { } ;
138146 for ( let i = 0 ; i < ordersDetails . length ; i ++ ) {
139147 const orderDetails = ordersDetails [ i ] ;
148+ const orderHash = orderDetails . orderHash . toLowerCase ( ) ;
140149 const orderbook = orderDetails . orderbook . id . toLowerCase ( ) ;
141150 const orderStruct = toOrder (
142151 decodeAbiParameters (
@@ -154,12 +163,13 @@ export async function handleAddOrderbookOwnersProfileMap(
154163 if ( orderbookOwnerProfileItem ) {
155164 const ownerProfile = orderbookOwnerProfileItem . get ( orderStruct . owner . toLowerCase ( ) ) ;
156165 if ( ownerProfile ) {
157- const order = ownerProfile . orders . get ( orderDetails . orderHash . toLowerCase ( ) ) ;
166+ const order = ownerProfile . orders . get ( orderHash ) ;
158167 if ( ! order ) {
159- ownerProfile . orders . set ( orderDetails . orderHash . toLowerCase ( ) , {
168+ ownerProfile . orders . set ( orderHash , {
160169 active : true ,
161170 order : orderStruct ,
162171 takeOrders : await getOrderPairs (
172+ orderHash ,
163173 orderStruct ,
164174 viemClient ,
165175 tokens ,
@@ -172,10 +182,16 @@ export async function handleAddOrderbookOwnersProfileMap(
172182 }
173183 } else {
174184 const ordersProfileMap : OrdersProfileMap = new Map ( ) ;
175- ordersProfileMap . set ( orderDetails . orderHash . toLowerCase ( ) , {
185+ ordersProfileMap . set ( orderHash , {
176186 active : true ,
177187 order : orderStruct ,
178- takeOrders : await getOrderPairs ( orderStruct , viemClient , tokens , orderDetails ) ,
188+ takeOrders : await getOrderPairs (
189+ orderHash ,
190+ orderStruct ,
191+ viemClient ,
192+ tokens ,
193+ orderDetails ,
194+ ) ,
179195 consumedTakeOrders : [ ] ,
180196 } ) ;
181197 orderbookOwnerProfileItem . set ( orderStruct . owner . toLowerCase ( ) , {
@@ -185,10 +201,16 @@ export async function handleAddOrderbookOwnersProfileMap(
185201 }
186202 } else {
187203 const ordersProfileMap : OrdersProfileMap = new Map ( ) ;
188- ordersProfileMap . set ( orderDetails . orderHash . toLowerCase ( ) , {
204+ ordersProfileMap . set ( orderHash , {
189205 active : true ,
190206 order : orderStruct ,
191- takeOrders : await getOrderPairs ( orderStruct , viemClient , tokens , orderDetails ) ,
207+ takeOrders : await getOrderPairs (
208+ orderHash ,
209+ orderStruct ,
210+ viemClient ,
211+ tokens ,
212+ orderDetails ,
213+ ) ,
192214 consumedTakeOrders : [ ] ,
193215 } ) ;
194216 const ownerProfileMap : OwnersProfileMap = new Map ( ) ;
@@ -359,10 +381,7 @@ function gatherPairs(
359381 if (
360382 ! bundleOrder . takeOrders . find ( ( v ) => v . id . toLowerCase ( ) === orderHash . toLowerCase ( ) )
361383 ) {
362- bundleOrder . takeOrders . push ( {
363- id : orderHash ,
364- takeOrder : pair . takeOrder ,
365- } ) ;
384+ bundleOrder . takeOrders . push ( pair . takeOrder ) ;
366385 }
367386 } else {
368387 bundledOrders . push ( {
@@ -373,13 +392,217 @@ function gatherPairs(
373392 sellToken : pair . sellToken ,
374393 sellTokenDecimals : pair . sellTokenDecimals ,
375394 sellTokenSymbol : pair . sellTokenSymbol ,
376- takeOrders : [
377- {
378- id : orderHash ,
379- takeOrder : pair . takeOrder ,
380- } ,
381- ] ,
395+ takeOrders : [ pair . takeOrder ] ,
396+ } ) ;
397+ }
398+ }
399+ }
400+
401+ /**
402+ * Builds a map with following form from an `OrderbooksOwnersProfileMap` instance:
403+ * `orderbook -> token -> owner -> vaults` called `OTOVMap`
404+ * This is later on used to evaluate the owners limits
405+ */
406+ export function buildOtovMap ( orderbooksOwnersProfileMap : OrderbooksOwnersProfileMap ) : OTOVMap {
407+ const result : OTOVMap = new Map ( ) ;
408+ orderbooksOwnersProfileMap . forEach ( ( ownersProfileMap , orderbook ) => {
409+ const tokensOwnersVaults : TokensOwnersVaults = new Map ( ) ;
410+ ownersProfileMap . forEach ( ( ownerProfile , owner ) => {
411+ ownerProfile . orders . forEach ( ( orderProfile ) => {
412+ orderProfile . takeOrders . forEach ( ( pair ) => {
413+ const token = pair . sellToken . toLowerCase ( ) ;
414+ const vaultId =
415+ pair . takeOrder . takeOrder . order . validOutputs [
416+ pair . takeOrder . takeOrder . outputIOIndex
417+ ] . vaultId . toLowerCase ( ) ;
418+ const ownersVaults = tokensOwnersVaults . get ( token ) ;
419+ if ( ownersVaults ) {
420+ const vaults = ownersVaults . get ( owner . toLowerCase ( ) ) ;
421+ if ( vaults ) {
422+ if ( ! vaults . find ( ( v ) => v . vaultId === vaultId ) )
423+ vaults . push ( { vaultId, balance : 0n } ) ;
424+ } else {
425+ ownersVaults . set ( owner . toLowerCase ( ) , [ { vaultId, balance : 0n } ] ) ;
426+ }
427+ } else {
428+ const newOwnersVaults : OwnersVaults = new Map ( ) ;
429+ newOwnersVaults . set ( owner . toLowerCase ( ) , [ { vaultId, balance : 0n } ] ) ;
430+ tokensOwnersVaults . set ( token , newOwnersVaults ) ;
431+ }
432+ } ) ;
433+ } ) ;
434+ } ) ;
435+ result . set ( orderbook , tokensOwnersVaults ) ;
436+ } ) ;
437+ return result ;
438+ }
439+
440+ /**
441+ * Gets vault balances of an owner's vaults of a given token
442+ */
443+ export async function fetchVaultBalances (
444+ orderbook : string ,
445+ token : string ,
446+ owner : string ,
447+ vaults : Vault [ ] ,
448+ viemClient : ViemClient ,
449+ multicallAddressOverride ?: string ,
450+ ) {
451+ const multicallResult = await viemClient . multicall ( {
452+ multicallAddress :
453+ ( multicallAddressOverride as `0x${string } ` | undefined ) ??
454+ viemClient . chain ?. contracts ?. multicall3 ?. address ,
455+ allowFailure : false ,
456+ contracts : vaults . map ( ( v ) => ( {
457+ address : orderbook as `0x${string } `,
458+ allowFailure : false ,
459+ chainId : viemClient . chain ! . id ,
460+ abi : parseAbi ( [ orderbookAbi [ 3 ] ] ) ,
461+ functionName : "vaultBalance" ,
462+ args : [ owner , token , v . vaultId ] ,
463+ } ) ) ,
464+ } ) ;
465+
466+ for ( let i = 0 ; i < multicallResult . length ; i ++ ) {
467+ vaults [ i ] . balance = multicallResult [ i ] ;
468+ }
469+ }
470+
471+ /**
472+ * Evaluates the owners limits by checking an owner vaults avg balances of a token against
473+ * other owners total balances of that token to calculate a percentage, repeats the same
474+ * process for every other token and owner and at the end ends up with map of owners with array
475+ * of percentages, then calculates an avg of all those percenatges and that is applied as a divider
476+ * factor to the owner's limit.
477+ * This ensures that if an owner has many orders/vaults and has spread their balances across those
478+ * many vaults and orders, he/she will get limited.
479+ * Owners limits that are set by bot's admin as env or cli arg, are exluded from this evaluation process
480+ */
481+ export async function evaluateOwnersLimits (
482+ orderbooksOwnersProfileMap : OrderbooksOwnersProfileMap ,
483+ otovMap : OTOVMap ,
484+ viemClient : ViemClient ,
485+ ownerLimits ?: Record < string , number > ,
486+ multicallAddressOverride ?: string ,
487+ ) {
488+ for ( const [ orderbook , tokensOwnersVaults ] of otovMap ) {
489+ const ownersProfileMap = orderbooksOwnersProfileMap . get ( orderbook ) ;
490+ if ( ownersProfileMap ) {
491+ const ownersCuts : Map < string , number [ ] > = new Map ( ) ;
492+ for ( const [ token , ownersVaults ] of tokensOwnersVaults ) {
493+ const obTokenBalance = await viemClient . readContract ( {
494+ address : token as `0x${string } `,
495+ abi : erc20Abi ,
496+ functionName : "balanceOf" ,
497+ args : [ orderbook as `0x${string } `] ,
498+ } ) ;
499+ for ( const [ owner , vaults ] of ownersVaults ) {
500+ // skip if owner limit is set by bot admin
501+ if ( typeof ownerLimits ?. [ owner . toLowerCase ( ) ] === "number" ) continue ;
502+
503+ const ownerProfile = ownersProfileMap . get ( owner ) ;
504+ if ( ownerProfile ) {
505+ await fetchVaultBalances (
506+ orderbook ,
507+ token ,
508+ owner ,
509+ vaults ,
510+ viemClient ,
511+ multicallAddressOverride ,
512+ ) ;
513+ const ownerTotalBalance = vaults . reduce (
514+ ( a , b ) => ( {
515+ balance : a . balance + b . balance ,
516+ } ) ,
517+ {
518+ balance : 0n ,
519+ } ,
520+ ) . balance ;
521+ const avgBalance = ownerTotalBalance / BigInt ( vaults . length ) ;
522+ const otherOwnersBalances = obTokenBalance - ownerTotalBalance ;
523+ const balanceRatioPercent =
524+ otherOwnersBalances === 0n
525+ ? 100n
526+ : ( avgBalance * 100n ) / otherOwnersBalances ;
527+
528+ // divide into 4 segments
529+ let ownerEvalDivideFactor = 1 ;
530+ if ( balanceRatioPercent >= 75n ) {
531+ ownerEvalDivideFactor = 1 ;
532+ } else if ( balanceRatioPercent >= 50n && balanceRatioPercent < 75n ) {
533+ ownerEvalDivideFactor = 2 ;
534+ } else if ( balanceRatioPercent >= 25n && balanceRatioPercent < 50n ) {
535+ ownerEvalDivideFactor = 3 ;
536+ } else if ( balanceRatioPercent > 0n && balanceRatioPercent < 25n ) {
537+ ownerEvalDivideFactor = 4 ;
538+ }
539+
540+ // gather owner divide factor for all of the owner's orders' tokens
541+ // to calculate an avg from them all later on
542+ const cuts = ownersCuts . get ( owner . toLowerCase ( ) ) ;
543+ if ( cuts ) {
544+ cuts . push ( ownerEvalDivideFactor ) ;
545+ } else {
546+ ownersCuts . set ( owner . toLowerCase ( ) , [ ownerEvalDivideFactor ] ) ;
547+ }
548+ }
549+ }
550+ }
551+
552+ ownersProfileMap . forEach ( ( ownerProfile , owner ) => {
553+ const cuts = ownersCuts . get ( owner ) ;
554+ if ( cuts ?. length ) {
555+ const avgCut = cuts . reduce ( ( a , b ) => a + b , 0 ) / cuts . length ;
556+ // round to nearest int, if turned out 0, set it to 1 as minimum
557+ ownerProfile . limit = Math . round ( ownerProfile . limit / avgCut ) ;
558+ if ( ownerProfile . limit === 0 ) ownerProfile . limit = 1 ;
559+ }
382560 } ) ;
383561 }
384562 }
385563}
564+
565+ /**
566+ * This is a wrapper fn around evaluating owers limits.
567+ * Provides a protection by evaluating and possibly reducing owner's limit,
568+ * this takes place by checking an owners avg vault balance of a token against
569+ * all other owners cumulative balances, the calculated ratio is used a reducing
570+ * factor for the owner limit when averaged out for all of tokens the owner has
571+ */
572+ export async function downscaleProtection (
573+ orderbooksOwnersProfileMap : OrderbooksOwnersProfileMap ,
574+ viemClient : ViemClient ,
575+ ownerLimits ?: Record < string , number > ,
576+ reset = true ,
577+ multicallAddressOverride ?: string ,
578+ ) {
579+ if ( reset ) {
580+ resetLimits ( orderbooksOwnersProfileMap , ownerLimits ) ;
581+ }
582+ const otovMap = buildOtovMap ( orderbooksOwnersProfileMap ) ;
583+ await evaluateOwnersLimits (
584+ orderbooksOwnersProfileMap ,
585+ otovMap ,
586+ viemClient ,
587+ ownerLimits ,
588+ multicallAddressOverride ,
589+ ) ;
590+ }
591+
592+ /**
593+ * Resets owners limit to default value
594+ */
595+ export async function resetLimits (
596+ orderbooksOwnersProfileMap : OrderbooksOwnersProfileMap ,
597+ ownerLimits ?: Record < string , number > ,
598+ ) {
599+ orderbooksOwnersProfileMap . forEach ( ( ownersProfileMap ) => {
600+ if ( ownersProfileMap ) {
601+ ownersProfileMap . forEach ( ( ownerProfile , owner ) => {
602+ // skip if owner limit is set by bot admin
603+ if ( typeof ownerLimits ?. [ owner . toLowerCase ( ) ] === "number" ) return ;
604+ ownerProfile . limit = DEFAULT_OWNER_LIMIT ;
605+ } ) ;
606+ }
607+ } ) ;
608+ }
0 commit comments