Skip to content

Commit a74635d

Browse files
authored
Merge pull request #269 from rainlanguage/2024-11-29-downscale-sybil
downscale sybil
2 parents 912d4c3 + c708160 commit a74635d

File tree

5 files changed

+518
-30
lines changed

5 files changed

+518
-30
lines changed

src/cli.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import {
2222
getBatchEthBalance,
2323
} from "./account";
2424
import {
25+
downscaleProtection,
2526
prepareOrdersForRound,
2627
getOrderbookOwnersProfileMapFromSg,
2728
handleAddOrderbookOwnersProfileMap,
@@ -802,6 +803,7 @@ export const main = async (argv: any, version?: string) => {
802803
startTime: lastReadOrdersTimestamp,
803804
}),
804805
);
806+
let ordersDidChange = false;
805807
const results = await Promise.allSettled(
806808
lastReadOrdersMap.map((v) =>
807809
getOrderChanges(
@@ -816,6 +818,9 @@ export const main = async (argv: any, version?: string) => {
816818
for (let i = 0; i < results.length; i++) {
817819
const res = results[i];
818820
if (res.status === "fulfilled") {
821+
if (res.value.addOrders.length || res.value.removeOrders.length) {
822+
ordersDidChange = true;
823+
}
819824
lastReadOrdersMap[i].skip += res.value.count;
820825
try {
821826
await handleAddOrderbookOwnersProfileMap(
@@ -840,6 +845,15 @@ export const main = async (argv: any, version?: string) => {
840845
}
841846
}
842847
}
848+
849+
// in case there are new orders or removed order, re evaluate owners limits
850+
if (ordersDidChange) {
851+
await downscaleProtection(
852+
orderbooksOwnersProfileMap,
853+
config.viemClient as any as ViemClient,
854+
options.ownerProfile,
855+
);
856+
}
843857
} catch {
844858
/**/
845859
}

src/order.ts

Lines changed: 245 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,22 @@
1-
import { OrderV3 } from "./abis";
21
import { SgOrder } from "./query";
32
import { Span } from "@opentelemetry/api";
43
import { hexlify } from "ethers/lib/utils";
54
import { addWatchedToken } from "./account";
5+
import { orderbookAbi, OrderV3 } from "./abis";
66
import { getTokenSymbol, shuffleArray } from "./utils";
7-
import { decodeAbiParameters, parseAbiParameters } from "viem";
7+
import { decodeAbiParameters, erc20Abi, parseAbi, parseAbiParameters } from "viem";
88
import {
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
*/
4953
export 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

Comments
 (0)