solana-privacy-scanner-core 0.3.0 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +593 -100
- package/dist/index.cjs.map +4 -4
- package/dist/index.js +591 -100
- package/dist/index.js.map +4 -4
- package/package.json +3 -3
package/dist/index.cjs
CHANGED
|
@@ -38,12 +38,14 @@ __export(index_exports, {
|
|
|
38
38
|
collectTransactionData: () => collectTransactionData,
|
|
39
39
|
collectWalletData: () => collectWalletData,
|
|
40
40
|
createDefaultLabelProvider: () => createDefaultLabelProvider,
|
|
41
|
+
detectAddressReuse: () => detectAddressReuse,
|
|
41
42
|
detectAmountReuse: () => detectAmountReuse,
|
|
42
43
|
detectBalanceTraceability: () => detectBalanceTraceability,
|
|
43
44
|
detectCounterpartyReuse: () => detectCounterpartyReuse,
|
|
44
45
|
detectFeePayerReuse: () => detectFeePayerReuse,
|
|
45
46
|
detectInstructionFingerprinting: () => detectInstructionFingerprinting,
|
|
46
47
|
detectKnownEntityInteraction: () => detectKnownEntityInteraction,
|
|
48
|
+
detectMemoExposure: () => detectMemoExposure,
|
|
47
49
|
detectSignerOverlap: () => detectSignerOverlap,
|
|
48
50
|
detectTimingPatterns: () => detectTimingPatterns,
|
|
49
51
|
detectTokenAccountLifecycle: () => detectTokenAccountLifecycle,
|
|
@@ -1152,8 +1154,8 @@ function detectAmountReuse(context) {
|
|
|
1152
1154
|
severity: "LOW",
|
|
1153
1155
|
category: "behavioral",
|
|
1154
1156
|
reason: `${roundNumbers.length} round-number transfers detected (e.g., 1 SOL, 10 SOL).`,
|
|
1155
|
-
impact: "Round numbers are common on Solana
|
|
1156
|
-
mitigation: "Vary amounts slightly if possible, but this is low priority
|
|
1157
|
+
impact: "Round numbers are common on Solana. Combined with other patterns, they can contribute to fingerprinting.",
|
|
1158
|
+
mitigation: "Vary amounts slightly if possible, but this is low priority.",
|
|
1157
1159
|
evidence: [{
|
|
1158
1160
|
description: `${roundNumbers.length} round-number transfers: ${roundNumbers.slice(0, 5).join(", ")}...`,
|
|
1159
1161
|
severity: "LOW",
|
|
@@ -1206,7 +1208,7 @@ function detectAmountReuse(context) {
|
|
|
1206
1208
|
severity: "LOW",
|
|
1207
1209
|
category: "behavioral",
|
|
1208
1210
|
reason: `${signerReuse.length} amount(s) are reused multiple times with consistent signers.`,
|
|
1209
|
-
impact: "Amount reuse alone is relatively weak
|
|
1211
|
+
impact: "Amount reuse alone is relatively weak, but combined with other signals it contributes to behavioral fingerprinting.",
|
|
1210
1212
|
mitigation: "Vary transaction amounts to reduce pattern visibility.",
|
|
1211
1213
|
evidence
|
|
1212
1214
|
});
|
|
@@ -1240,62 +1242,128 @@ function detectAmountReuse(context) {
|
|
|
1240
1242
|
|
|
1241
1243
|
// src/heuristics/timing-patterns.ts
|
|
1242
1244
|
function detectTimingPatterns(context) {
|
|
1245
|
+
const signals = [];
|
|
1243
1246
|
if (!context.timeRange.earliest || !context.timeRange.latest) {
|
|
1244
|
-
return
|
|
1247
|
+
return signals;
|
|
1245
1248
|
}
|
|
1246
1249
|
if (context.transactionCount < 3) {
|
|
1247
|
-
return
|
|
1250
|
+
return signals;
|
|
1248
1251
|
}
|
|
1249
1252
|
const timeSpanSeconds = context.timeRange.latest - context.timeRange.earliest;
|
|
1250
1253
|
const timeSpanHours = timeSpanSeconds / 3600;
|
|
1251
1254
|
if (timeSpanHours === 0) {
|
|
1252
|
-
return
|
|
1255
|
+
return signals;
|
|
1253
1256
|
}
|
|
1254
1257
|
const txRate = context.transactionCount / timeSpanHours;
|
|
1255
|
-
let severity = "LOW";
|
|
1256
1258
|
let isBurst = false;
|
|
1259
|
+
let burstSeverity = "LOW";
|
|
1257
1260
|
if (txRate > 10) {
|
|
1258
|
-
|
|
1261
|
+
burstSeverity = "HIGH";
|
|
1259
1262
|
isBurst = true;
|
|
1260
1263
|
} else if (txRate > 5) {
|
|
1261
|
-
|
|
1264
|
+
burstSeverity = "MEDIUM";
|
|
1262
1265
|
isBurst = true;
|
|
1263
1266
|
} else if (timeSpanHours < 1 && context.transactionCount >= 3) {
|
|
1264
|
-
|
|
1267
|
+
burstSeverity = "MEDIUM";
|
|
1265
1268
|
isBurst = true;
|
|
1266
1269
|
}
|
|
1267
|
-
if (
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1270
|
+
if (isBurst) {
|
|
1271
|
+
signals.push({
|
|
1272
|
+
id: "timing-burst",
|
|
1273
|
+
name: "Transaction Burst Pattern",
|
|
1274
|
+
severity: burstSeverity,
|
|
1275
|
+
confidence: 0.8,
|
|
1276
|
+
category: "behavioral",
|
|
1277
|
+
reason: `Concentrated activity: ${context.transactionCount} transactions in ${timeSpanHours.toFixed(1)} hours`,
|
|
1278
|
+
impact: "Concentrated transaction activity creates timing fingerprints that can be used to correlate your transactions and link them to specific events or behaviors.",
|
|
1279
|
+
mitigation: "Spread transactions over longer time periods, use scheduled transactions, or batch operations to reduce timing correlation.",
|
|
1280
|
+
evidence: [{
|
|
1281
|
+
description: `${context.transactionCount} transactions in ${timeSpanHours.toFixed(1)} hours (${txRate.toFixed(2)} tx/hour)`,
|
|
1282
|
+
severity: burstSeverity,
|
|
1283
|
+
reference: void 0
|
|
1284
|
+
}]
|
|
1285
|
+
});
|
|
1286
|
+
}
|
|
1287
|
+
if (context.transactions && context.transactions.length >= 5) {
|
|
1288
|
+
const timestamps = context.transactions.map((tx) => tx.blockTime).filter((time) => time !== void 0).sort((a, b) => a - b);
|
|
1289
|
+
if (timestamps.length >= 5) {
|
|
1290
|
+
const gaps = [];
|
|
1291
|
+
for (let i = 1; i < timestamps.length; i++) {
|
|
1292
|
+
gaps.push(timestamps[i] - timestamps[i - 1]);
|
|
1293
|
+
}
|
|
1294
|
+
const avgGap = gaps.reduce((sum, gap) => sum + gap, 0) / gaps.length;
|
|
1295
|
+
const variance = gaps.reduce((sum, gap) => sum + Math.pow(gap - avgGap, 2), 0) / gaps.length;
|
|
1296
|
+
const stdDev = Math.sqrt(variance);
|
|
1297
|
+
const coefficientOfVariation = stdDev / avgGap;
|
|
1298
|
+
if (coefficientOfVariation < 0.3 && avgGap > 60) {
|
|
1299
|
+
const intervalMinutes = Math.round(avgGap / 60);
|
|
1300
|
+
const intervalHours = avgGap / 3600;
|
|
1301
|
+
let severity = "LOW";
|
|
1302
|
+
if (intervalHours >= 23 && intervalHours <= 25) {
|
|
1303
|
+
severity = "HIGH";
|
|
1304
|
+
} else if (intervalHours >= 0.9 && intervalHours <= 1.1) {
|
|
1305
|
+
severity = "HIGH";
|
|
1306
|
+
} else if (gaps.length >= 10) {
|
|
1307
|
+
severity = "MEDIUM";
|
|
1308
|
+
}
|
|
1309
|
+
signals.push({
|
|
1310
|
+
id: "timing-regular-interval",
|
|
1311
|
+
name: "Regular Transaction Interval",
|
|
1312
|
+
severity,
|
|
1313
|
+
confidence: 0.85,
|
|
1314
|
+
category: "behavioral",
|
|
1315
|
+
reason: `Transactions occur at regular ${intervalMinutes < 60 ? `${intervalMinutes}-minute` : `${intervalHours.toFixed(1)}-hour`} intervals.`,
|
|
1316
|
+
impact: "Regular timing patterns are highly distinctive fingerprints. They suggest automated behavior and can reveal timezone, schedule, or bot configuration.",
|
|
1317
|
+
mitigation: "Add random delays between transactions. Vary the timing to avoid predictable patterns.",
|
|
1318
|
+
evidence: [{
|
|
1319
|
+
description: `${gaps.length} transactions with average ${intervalMinutes}-minute intervals (${(coefficientOfVariation * 100).toFixed(1)}% variation)`,
|
|
1320
|
+
severity,
|
|
1321
|
+
reference: void 0
|
|
1322
|
+
}]
|
|
1323
|
+
});
|
|
1278
1324
|
}
|
|
1279
1325
|
}
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1326
|
+
}
|
|
1327
|
+
if (context.transactions && context.transactions.length >= 10) {
|
|
1328
|
+
const timestamps = context.transactions.map((tx) => tx.blockTime).filter((time) => time !== void 0);
|
|
1329
|
+
if (timestamps.length >= 10) {
|
|
1330
|
+
const hours = timestamps.map((ts) => new Date(ts * 1e3).getUTCHours());
|
|
1331
|
+
const hourCounts = /* @__PURE__ */ new Map();
|
|
1332
|
+
hours.forEach((hour) => {
|
|
1333
|
+
hourCounts.set(hour, (hourCounts.get(hour) || 0) + 1);
|
|
1334
|
+
});
|
|
1335
|
+
const maxCount = Math.max(...Array.from(hourCounts.values()));
|
|
1336
|
+
const concentration = maxCount / hours.length;
|
|
1337
|
+
if (concentration > 0.4) {
|
|
1338
|
+
const mostActiveHours = Array.from(hourCounts.entries()).filter(([_, count]) => count >= maxCount * 0.8).map(([hour]) => hour).sort((a, b) => a - b);
|
|
1339
|
+
signals.push({
|
|
1340
|
+
id: "timing-timezone-pattern",
|
|
1341
|
+
name: "Consistent Time-of-Day Pattern",
|
|
1342
|
+
severity: "MEDIUM",
|
|
1343
|
+
confidence: 0.7,
|
|
1344
|
+
category: "behavioral",
|
|
1345
|
+
reason: `${Math.round(concentration * 100)}% of transactions occur during specific hours (${mostActiveHours.map((h) => `${h}:00`).join(", ")} UTC).`,
|
|
1346
|
+
impact: "Time-of-day patterns can reveal timezone or daily schedule, contributing to identity fingerprinting.",
|
|
1347
|
+
mitigation: "Vary transaction times across different hours of the day. Use scheduled transactions or automation to obscure your timezone.",
|
|
1348
|
+
evidence: [{
|
|
1349
|
+
description: `${maxCount}/${hours.length} transactions during ${mostActiveHours.length} hour(s)`,
|
|
1350
|
+
severity: "MEDIUM",
|
|
1351
|
+
reference: void 0
|
|
1352
|
+
}]
|
|
1353
|
+
});
|
|
1354
|
+
}
|
|
1355
|
+
}
|
|
1356
|
+
}
|
|
1357
|
+
return signals;
|
|
1291
1358
|
}
|
|
1292
1359
|
|
|
1293
1360
|
// src/heuristics/known-entity.ts
|
|
1294
1361
|
function detectKnownEntityInteraction(context) {
|
|
1362
|
+
const signals = [];
|
|
1295
1363
|
if (context.labels.size === 0) {
|
|
1296
|
-
return
|
|
1364
|
+
return signals;
|
|
1297
1365
|
}
|
|
1298
|
-
const
|
|
1366
|
+
const entityTypeGroups = /* @__PURE__ */ new Map();
|
|
1299
1367
|
for (const [address, label] of context.labels.entries()) {
|
|
1300
1368
|
let interactionCount = 0;
|
|
1301
1369
|
const relatedTxs = [];
|
|
@@ -1308,101 +1376,243 @@ function detectKnownEntityInteraction(context) {
|
|
|
1308
1376
|
}
|
|
1309
1377
|
}
|
|
1310
1378
|
if (interactionCount > 0) {
|
|
1311
|
-
|
|
1312
|
-
type
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
transactions: relatedTxs
|
|
1320
|
-
},
|
|
1321
|
-
reference: address
|
|
1379
|
+
if (!entityTypeGroups.has(label.type)) {
|
|
1380
|
+
entityTypeGroups.set(label.type, []);
|
|
1381
|
+
}
|
|
1382
|
+
entityTypeGroups.get(label.type).push({
|
|
1383
|
+
address,
|
|
1384
|
+
label,
|
|
1385
|
+
count: interactionCount,
|
|
1386
|
+
txs: relatedTxs
|
|
1322
1387
|
});
|
|
1323
1388
|
}
|
|
1324
1389
|
}
|
|
1325
|
-
if (
|
|
1326
|
-
return
|
|
1390
|
+
if (entityTypeGroups.size === 0) {
|
|
1391
|
+
return signals;
|
|
1327
1392
|
}
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1393
|
+
const exchanges = entityTypeGroups.get("exchange");
|
|
1394
|
+
if (exchanges && exchanges.length > 0) {
|
|
1395
|
+
const evidence = exchanges.map((entity) => ({
|
|
1396
|
+
description: `${entity.count} interaction(s) with ${entity.label.name}`,
|
|
1397
|
+
severity: "HIGH",
|
|
1398
|
+
reference: entity.address
|
|
1399
|
+
}));
|
|
1400
|
+
const totalExchangeTxs = exchanges.reduce((sum, e) => sum + e.count, 0);
|
|
1401
|
+
signals.push({
|
|
1402
|
+
id: "known-entity-exchange",
|
|
1403
|
+
name: "Centralized Exchange Interaction",
|
|
1404
|
+
severity: "HIGH",
|
|
1405
|
+
confidence: 0.95,
|
|
1406
|
+
category: "identity-linkage",
|
|
1407
|
+
reason: `Wallet interacted with ${exchanges.length} centralized exchange(s) in ${totalExchangeTxs} transaction(s).`,
|
|
1408
|
+
impact: "Centralized exchanges have KYC data. Direct interactions can link your on-chain address to your real-world identity through account records, IP addresses, and withdrawal/deposit patterns.",
|
|
1409
|
+
mitigation: "Use intermediate wallets to break the direct link. Deposit to privacy protocols before going to CEX. Consider DEXs for better privacy.",
|
|
1410
|
+
evidence
|
|
1411
|
+
});
|
|
1334
1412
|
}
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1413
|
+
const bridges = entityTypeGroups.get("bridge");
|
|
1414
|
+
if (bridges && bridges.length > 0) {
|
|
1415
|
+
const evidence = bridges.map((entity) => ({
|
|
1416
|
+
description: `${entity.count} interaction(s) with ${entity.label.name}`,
|
|
1417
|
+
severity: "MEDIUM",
|
|
1418
|
+
reference: entity.address
|
|
1419
|
+
}));
|
|
1420
|
+
signals.push({
|
|
1421
|
+
id: "known-entity-bridge",
|
|
1422
|
+
name: "Bridge Protocol Interaction",
|
|
1423
|
+
severity: "MEDIUM",
|
|
1424
|
+
confidence: 0.85,
|
|
1425
|
+
category: "identity-linkage",
|
|
1426
|
+
reason: `Wallet interacted with ${bridges.length} bridge protocol(s).`,
|
|
1427
|
+
impact: "Bridge transactions can link your Solana address to addresses on other chains, expanding the tracking surface.",
|
|
1428
|
+
mitigation: "Use privacy-preserving bridges when available. Create separate addresses for cross-chain activity.",
|
|
1429
|
+
evidence
|
|
1430
|
+
});
|
|
1431
|
+
}
|
|
1432
|
+
const others = Array.from(entityTypeGroups.entries()).filter(([type]) => type !== "exchange" && type !== "bridge");
|
|
1433
|
+
if (others.length > 0) {
|
|
1434
|
+
const allOtherEntities = others.flatMap(([_, entities]) => entities);
|
|
1435
|
+
const evidence = allOtherEntities.slice(0, 5).map((entity) => ({
|
|
1436
|
+
description: `${entity.count} interaction(s) with ${entity.label.name} (${entity.label.type})`,
|
|
1437
|
+
severity: "LOW",
|
|
1438
|
+
reference: entity.address
|
|
1439
|
+
}));
|
|
1440
|
+
const totalOtherTxs = allOtherEntities.reduce((sum, e) => sum + e.count, 0);
|
|
1441
|
+
signals.push({
|
|
1442
|
+
id: "known-entity-other",
|
|
1443
|
+
name: "Known Entity Interactions",
|
|
1444
|
+
severity: "LOW",
|
|
1445
|
+
confidence: 0.75,
|
|
1446
|
+
category: "behavioral",
|
|
1447
|
+
reason: `Wallet interacted with ${allOtherEntities.length} known entit${allOtherEntities.length === 1 ? "y" : "ies"} (${totalOtherTxs} transactions).`,
|
|
1448
|
+
impact: "Interactions with known entities create reference points in your transaction history. These can be used to correlate activity and build behavioral profiles.",
|
|
1449
|
+
mitigation: "While interacting with known protocols is often necessary, be aware it creates public association with those services.",
|
|
1450
|
+
evidence
|
|
1451
|
+
});
|
|
1452
|
+
}
|
|
1453
|
+
for (const [address, label] of context.labels.entries()) {
|
|
1454
|
+
let interactionCount = 0;
|
|
1455
|
+
for (const transfer of context.transfers) {
|
|
1456
|
+
if (transfer.from === address || transfer.to === address) {
|
|
1457
|
+
interactionCount++;
|
|
1458
|
+
}
|
|
1459
|
+
}
|
|
1460
|
+
const concentration = interactionCount / context.transfers.length;
|
|
1461
|
+
if (concentration > 0.3 && interactionCount >= 5) {
|
|
1462
|
+
signals.push({
|
|
1463
|
+
id: `known-entity-frequent-${address.slice(0, 8)}`,
|
|
1464
|
+
name: "Frequent Single Entity Interaction",
|
|
1465
|
+
severity: label.type === "exchange" ? "HIGH" : "MEDIUM",
|
|
1466
|
+
confidence: 0.85,
|
|
1467
|
+
category: "behavioral",
|
|
1468
|
+
reason: `${Math.round(concentration * 100)}% of transfers (${interactionCount}/${context.transfers.length}) involve ${label.name}.`,
|
|
1469
|
+
impact: "Heavy concentration of activity with one entity creates a strong link and behavioral dependency that is easily identified.",
|
|
1470
|
+
mitigation: "Diversify your interactions across multiple services. Use different addresses for different service providers.",
|
|
1471
|
+
evidence: [{
|
|
1472
|
+
description: `${interactionCount} transfers with ${label.name} (${label.type})`,
|
|
1473
|
+
severity: label.type === "exchange" ? "HIGH" : "MEDIUM",
|
|
1474
|
+
reference: address
|
|
1475
|
+
}]
|
|
1476
|
+
});
|
|
1477
|
+
}
|
|
1478
|
+
}
|
|
1479
|
+
return signals;
|
|
1345
1480
|
}
|
|
1346
1481
|
|
|
1347
1482
|
// src/heuristics/balance-traceability.ts
|
|
1348
1483
|
function detectBalanceTraceability(context) {
|
|
1484
|
+
const signals = [];
|
|
1349
1485
|
if (context.targetType !== "wallet" || context.transfers.length < 2) {
|
|
1350
|
-
return
|
|
1486
|
+
return signals;
|
|
1351
1487
|
}
|
|
1352
|
-
const fullBalanceTransfers = [];
|
|
1353
|
-
const suspiciousPatterns = [];
|
|
1354
1488
|
const amountPairs = /* @__PURE__ */ new Map();
|
|
1355
1489
|
for (const transfer of context.transfers) {
|
|
1356
1490
|
const amountKey = transfer.amount.toFixed(6);
|
|
1357
1491
|
amountPairs.set(amountKey, (amountPairs.get(amountKey) || 0) + 1);
|
|
1358
1492
|
}
|
|
1359
|
-
const matchingPairs = Array.from(amountPairs.entries()).filter(([_, count]) => count >= 2);
|
|
1493
|
+
const matchingPairs = Array.from(amountPairs.entries()).filter(([_, count]) => count >= 2).sort((a, b) => b[1] - a[1]);
|
|
1360
1494
|
if (matchingPairs.length >= 2) {
|
|
1361
|
-
|
|
1495
|
+
const evidence = matchingPairs.slice(0, 5).map(([amount, count]) => ({
|
|
1496
|
+
description: `Amount ${amount} appears in ${count} transfers`,
|
|
1497
|
+
severity: count >= 4 ? "HIGH" : "MEDIUM",
|
|
1498
|
+
reference: void 0
|
|
1499
|
+
}));
|
|
1500
|
+
const severity = matchingPairs.length >= 4 ? "HIGH" : matchingPairs.length >= 3 ? "MEDIUM" : "LOW";
|
|
1501
|
+
signals.push({
|
|
1502
|
+
id: "balance-matching-pairs",
|
|
1503
|
+
name: "Matching Send/Receive Amounts",
|
|
1504
|
+
severity,
|
|
1505
|
+
confidence: 0.7,
|
|
1506
|
+
category: "traceability",
|
|
1507
|
+
reason: `${matchingPairs.length} amount(s) appear in multiple transfers, suggesting balance movements.`,
|
|
1508
|
+
impact: "Matching amounts can be used to trace balance flows. If you receive X and later send X, observers can link these transactions.",
|
|
1509
|
+
mitigation: "Split large transfers into multiple smaller ones with varied amounts. Avoid sending exact amounts you received.",
|
|
1510
|
+
evidence
|
|
1511
|
+
});
|
|
1362
1512
|
}
|
|
1513
|
+
const sequentialPairs = [];
|
|
1363
1514
|
for (let i = 0; i < context.transfers.length - 1; i++) {
|
|
1364
1515
|
const current = context.transfers[i];
|
|
1365
1516
|
const next = context.transfers[i + 1];
|
|
1366
1517
|
if (current.blockTime && next.blockTime) {
|
|
1367
1518
|
const timeDiff = Math.abs(next.blockTime - current.blockTime);
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1519
|
+
const amountDiff = Math.abs(current.amount - next.amount);
|
|
1520
|
+
const percentDiff = amountDiff / Math.max(current.amount, next.amount);
|
|
1521
|
+
if (timeDiff < 3600 && percentDiff < 0.1 && current.amount > 0.1) {
|
|
1522
|
+
sequentialPairs.push({
|
|
1523
|
+
index: i,
|
|
1524
|
+
amount1: current.amount,
|
|
1525
|
+
amount2: next.amount,
|
|
1526
|
+
timeDiff
|
|
1527
|
+
});
|
|
1371
1528
|
}
|
|
1372
1529
|
}
|
|
1373
1530
|
}
|
|
1374
|
-
if (
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1531
|
+
if (sequentialPairs.length >= 2) {
|
|
1532
|
+
const evidence = sequentialPairs.slice(0, 3).map((pair) => ({
|
|
1533
|
+
description: `${pair.amount1.toFixed(4)} \u2192 ${pair.amount2.toFixed(4)} (${Math.round(pair.timeDiff / 60)} minutes apart)`,
|
|
1534
|
+
severity: pair.timeDiff < 600 ? "HIGH" : "MEDIUM",
|
|
1535
|
+
reference: void 0
|
|
1536
|
+
}));
|
|
1537
|
+
signals.push({
|
|
1538
|
+
id: "balance-sequential-similar",
|
|
1539
|
+
name: "Sequential Similar Amount Transfers",
|
|
1540
|
+
severity: "MEDIUM",
|
|
1541
|
+
confidence: 0.65,
|
|
1542
|
+
category: "traceability",
|
|
1543
|
+
reason: `${sequentialPairs.length} instance(s) of similar amounts transferred in quick succession.`,
|
|
1544
|
+
impact: "Sequential similar amounts suggest balance movements and make it easy to trace funds through intermediate addresses.",
|
|
1545
|
+
mitigation: "Add random delays between transactions. Vary amounts to obscure the flow path.",
|
|
1546
|
+
evidence
|
|
1383
1547
|
});
|
|
1384
1548
|
}
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1549
|
+
const roundNumbers = context.transfers.filter((t) => {
|
|
1550
|
+
const amount = t.amount;
|
|
1551
|
+
if (amount === 0) return false;
|
|
1552
|
+
return (amount === Math.floor(amount) || // Whole number
|
|
1553
|
+
amount * 10 === Math.floor(amount * 10) || // One decimal
|
|
1554
|
+
amount * 100 === Math.floor(amount * 100)) && (amount % 1 === 0 || // 1, 10, 100
|
|
1555
|
+
amount * 10 % 1 === 0 || // 0.1, 0.5
|
|
1556
|
+
amount * 100 % 1 === 0);
|
|
1557
|
+
});
|
|
1558
|
+
const roundNumberRatio = roundNumbers.length / context.transfers.length;
|
|
1559
|
+
if (roundNumberRatio > 0.7 && context.transfers.length >= 5) {
|
|
1560
|
+
signals.push({
|
|
1561
|
+
id: "balance-round-numbers",
|
|
1562
|
+
name: "High Proportion of Round Number Transfers",
|
|
1563
|
+
severity: "LOW",
|
|
1564
|
+
confidence: 0.6,
|
|
1565
|
+
category: "behavioral",
|
|
1566
|
+
reason: `${Math.round(roundNumberRatio * 100)}% of transfers use round numbers.`,
|
|
1567
|
+
impact: "Round numbers are easier to remember and track. They can contribute to balance traceability when combined with other patterns.",
|
|
1568
|
+
mitigation: "Use more varied amounts. Add small random values to make amounts less predictable.",
|
|
1569
|
+
evidence: [{
|
|
1570
|
+
description: `${roundNumbers.length}/${context.transfers.length} transfers are round numbers`,
|
|
1571
|
+
severity: "LOW",
|
|
1572
|
+
reference: void 0
|
|
1573
|
+
}]
|
|
1390
1574
|
});
|
|
1391
1575
|
}
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1576
|
+
const tokenTransfers = /* @__PURE__ */ new Map();
|
|
1577
|
+
for (const transfer of context.transfers) {
|
|
1578
|
+
const token = transfer.token || "SOL";
|
|
1579
|
+
if (!tokenTransfers.has(token)) {
|
|
1580
|
+
tokenTransfers.set(token, []);
|
|
1581
|
+
}
|
|
1582
|
+
tokenTransfers.get(token).push(transfer);
|
|
1583
|
+
}
|
|
1584
|
+
for (const [token, transfers] of tokenTransfers) {
|
|
1585
|
+
if (transfers.length < 2) continue;
|
|
1586
|
+
const receives = transfers.filter((t) => t.to === context.target);
|
|
1587
|
+
const sends = transfers.filter((t) => t.from === context.target);
|
|
1588
|
+
for (const receive of receives) {
|
|
1589
|
+
for (const send of sends) {
|
|
1590
|
+
if (!receive.blockTime || !send.blockTime) continue;
|
|
1591
|
+
if (send.blockTime <= receive.blockTime) continue;
|
|
1592
|
+
const timeDiff = send.blockTime - receive.blockTime;
|
|
1593
|
+
const percentDiff = Math.abs(send.amount - receive.amount) / receive.amount;
|
|
1594
|
+
if (percentDiff < 0.05 && timeDiff < 86400 && receive.amount > 1) {
|
|
1595
|
+
signals.push({
|
|
1596
|
+
id: `balance-full-movement-${token}`,
|
|
1597
|
+
name: "Full Balance Movement Detected",
|
|
1598
|
+
severity: "HIGH",
|
|
1599
|
+
confidence: 0.8,
|
|
1600
|
+
category: "traceability",
|
|
1601
|
+
reason: `Received ${receive.amount.toFixed(4)} ${token}, then sent ${send.amount.toFixed(4)} ${token} shortly after.`,
|
|
1602
|
+
impact: "Moving entire received balances makes fund flow trivially traceable. The path from source to destination is clear.",
|
|
1603
|
+
mitigation: "Split received funds before sending. Mix with other funds. Add delays and intermediate steps.",
|
|
1604
|
+
evidence: [{
|
|
1605
|
+
description: `Received ${receive.amount.toFixed(4)} \u2192 Sent ${send.amount.toFixed(4)} (${Math.round(timeDiff / 60)} minutes later)`,
|
|
1606
|
+
severity: "HIGH",
|
|
1607
|
+
reference: void 0
|
|
1608
|
+
}]
|
|
1609
|
+
});
|
|
1610
|
+
break;
|
|
1611
|
+
}
|
|
1612
|
+
}
|
|
1613
|
+
}
|
|
1395
1614
|
}
|
|
1396
|
-
return
|
|
1397
|
-
id: "balance-traceability",
|
|
1398
|
-
name: "Balance Traceability",
|
|
1399
|
-
severity,
|
|
1400
|
-
reason: "Wallet shows patterns that enable balance tracking",
|
|
1401
|
-
impact: "Traceable balance movements allow observers to follow funds through the blockchain, linking your transactions and revealing your financial activity.",
|
|
1402
|
-
evidence,
|
|
1403
|
-
mitigation: "Split large transfers into multiple smaller ones, introduce timing delays, or use privacy protocols that obscure amounts.",
|
|
1404
|
-
confidence: 0.7
|
|
1405
|
-
};
|
|
1615
|
+
return signals;
|
|
1406
1616
|
}
|
|
1407
1617
|
|
|
1408
1618
|
// src/heuristics/fee-payer-reuse.ts
|
|
@@ -1931,12 +2141,284 @@ function detectTokenAccountLifecycle(context) {
|
|
|
1931
2141
|
return signals;
|
|
1932
2142
|
}
|
|
1933
2143
|
|
|
2144
|
+
// src/heuristics/memo-exposure.ts
|
|
2145
|
+
function detectMemoExposure(context) {
|
|
2146
|
+
const signals = [];
|
|
2147
|
+
if (context.transactionCount === 0) {
|
|
2148
|
+
return signals;
|
|
2149
|
+
}
|
|
2150
|
+
if (!context.transactions || context.transactions.length === 0) {
|
|
2151
|
+
return signals;
|
|
2152
|
+
}
|
|
2153
|
+
const MEMO_PROGRAM = "MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr";
|
|
2154
|
+
const MEMO_PROGRAM_V1 = "Memo1UhkJRfHyvLMcVucJwxXeuD728EqVDDwQDxFMNo";
|
|
2155
|
+
const memoInstructions = context.instructions.filter(
|
|
2156
|
+
(inst) => inst.programId === MEMO_PROGRAM || inst.programId === MEMO_PROGRAM_V1
|
|
2157
|
+
);
|
|
2158
|
+
if (memoInstructions.length === 0) {
|
|
2159
|
+
return signals;
|
|
2160
|
+
}
|
|
2161
|
+
const suspiciousMemos = [];
|
|
2162
|
+
for (const inst of memoInstructions) {
|
|
2163
|
+
if (!inst.data || typeof inst.data !== "string") continue;
|
|
2164
|
+
const memoText = inst.data.trim();
|
|
2165
|
+
if (memoText.length === 0) continue;
|
|
2166
|
+
let severity = "LOW";
|
|
2167
|
+
const patterns = [];
|
|
2168
|
+
if (/[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/.test(memoText)) {
|
|
2169
|
+
patterns.push("email address");
|
|
2170
|
+
severity = "HIGH";
|
|
2171
|
+
}
|
|
2172
|
+
if (/https?:\/\/[^\s]+/.test(memoText)) {
|
|
2173
|
+
patterns.push("URL");
|
|
2174
|
+
severity = severity === "HIGH" ? "HIGH" : "MEDIUM";
|
|
2175
|
+
}
|
|
2176
|
+
if (/(\+\d{1,3}[-.\s]?)?\(?\d{3}\)?[-.\s]?\d{3}[-.\s]?\d{4}/.test(memoText)) {
|
|
2177
|
+
patterns.push("phone number");
|
|
2178
|
+
severity = "HIGH";
|
|
2179
|
+
}
|
|
2180
|
+
const capitalizedWords = memoText.match(/\b[A-Z][a-z]+(\s+[A-Z][a-z]+)+\b/g);
|
|
2181
|
+
if (capitalizedWords && capitalizedWords.length > 0) {
|
|
2182
|
+
patterns.push("likely name(s)");
|
|
2183
|
+
severity = severity === "HIGH" ? "HIGH" : "MEDIUM";
|
|
2184
|
+
}
|
|
2185
|
+
if (memoText.length > 50 && !patterns.length) {
|
|
2186
|
+
patterns.push("long descriptive text");
|
|
2187
|
+
severity = "MEDIUM";
|
|
2188
|
+
}
|
|
2189
|
+
if (/invoice|payment|order|transaction|ref|reference|id|bill/i.test(memoText)) {
|
|
2190
|
+
patterns.push("payment reference");
|
|
2191
|
+
severity = severity === "HIGH" ? "HIGH" : "MEDIUM";
|
|
2192
|
+
}
|
|
2193
|
+
if (patterns.length > 0) {
|
|
2194
|
+
suspiciousMemos.push({
|
|
2195
|
+
content: memoText.length > 100 ? memoText.slice(0, 100) + "..." : memoText,
|
|
2196
|
+
signature: inst.signature,
|
|
2197
|
+
severity
|
|
2198
|
+
});
|
|
2199
|
+
}
|
|
2200
|
+
}
|
|
2201
|
+
if (suspiciousMemos.length === 0) {
|
|
2202
|
+
signals.push({
|
|
2203
|
+
id: "memo-usage",
|
|
2204
|
+
name: "Memo Program Usage",
|
|
2205
|
+
severity: "LOW",
|
|
2206
|
+
confidence: 0.6,
|
|
2207
|
+
category: "information-leak",
|
|
2208
|
+
reason: `${memoInstructions.length} transaction(s) use memo program. Memos are permanently visible on-chain.`,
|
|
2209
|
+
impact: "Memo data is public and permanent. Even non-sensitive memos can contribute to behavioral fingerprinting.",
|
|
2210
|
+
mitigation: "Avoid using memos unless necessary. Never include personal information.",
|
|
2211
|
+
evidence: [{
|
|
2212
|
+
description: `${memoInstructions.length} transaction(s) with memos`,
|
|
2213
|
+
severity: "LOW",
|
|
2214
|
+
reference: void 0
|
|
2215
|
+
}]
|
|
2216
|
+
});
|
|
2217
|
+
} else {
|
|
2218
|
+
const highSeverity = suspiciousMemos.filter((m) => m.severity === "HIGH");
|
|
2219
|
+
const mediumSeverity = suspiciousMemos.filter((m) => m.severity === "MEDIUM");
|
|
2220
|
+
if (highSeverity.length > 0) {
|
|
2221
|
+
const evidence = highSeverity.map((memo) => ({
|
|
2222
|
+
description: `"${memo.content}"`,
|
|
2223
|
+
severity: "HIGH",
|
|
2224
|
+
reference: `https://solscan.io/tx/${memo.signature}`
|
|
2225
|
+
}));
|
|
2226
|
+
signals.push({
|
|
2227
|
+
id: "memo-pii-exposure",
|
|
2228
|
+
name: "Personal Information in Memo",
|
|
2229
|
+
severity: "HIGH",
|
|
2230
|
+
confidence: 0.9,
|
|
2231
|
+
category: "information-leak",
|
|
2232
|
+
reason: `${highSeverity.length} memo(s) contain personal identifying information (email, phone, name).`,
|
|
2233
|
+
impact: "CRITICAL: Personal information in memos is permanently public. This can directly link your wallet to your real-world identity.",
|
|
2234
|
+
mitigation: "Never put personal information in memos. Contact addresses involved to stop this practice if possible.",
|
|
2235
|
+
evidence
|
|
2236
|
+
});
|
|
2237
|
+
}
|
|
2238
|
+
if (mediumSeverity.length > 0) {
|
|
2239
|
+
const evidence = mediumSeverity.slice(0, 5).map((memo) => ({
|
|
2240
|
+
description: `"${memo.content}"`,
|
|
2241
|
+
severity: "MEDIUM",
|
|
2242
|
+
reference: `https://solscan.io/tx/${memo.signature}`
|
|
2243
|
+
}));
|
|
2244
|
+
signals.push({
|
|
2245
|
+
id: "memo-descriptive-content",
|
|
2246
|
+
name: "Descriptive Content in Memo",
|
|
2247
|
+
severity: "MEDIUM",
|
|
2248
|
+
confidence: 0.7,
|
|
2249
|
+
category: "information-leak",
|
|
2250
|
+
reason: `${mediumSeverity.length} memo(s) contain descriptive or identifying content.`,
|
|
2251
|
+
impact: "Descriptive memos create behavioral fingerprints and can indirectly reveal identity or transaction purpose.",
|
|
2252
|
+
mitigation: "Minimize memo usage. Use generic or coded references instead of descriptive text.",
|
|
2253
|
+
evidence
|
|
2254
|
+
});
|
|
2255
|
+
}
|
|
2256
|
+
}
|
|
2257
|
+
return signals;
|
|
2258
|
+
}
|
|
2259
|
+
|
|
2260
|
+
// src/heuristics/address-reuse.ts
|
|
2261
|
+
function detectAddressReuse(context) {
|
|
2262
|
+
const signals = [];
|
|
2263
|
+
if (context.targetType !== "wallet" || context.transactionCount < 5) {
|
|
2264
|
+
return signals;
|
|
2265
|
+
}
|
|
2266
|
+
const activityTypes = /* @__PURE__ */ new Set();
|
|
2267
|
+
const activityDetails = /* @__PURE__ */ new Map();
|
|
2268
|
+
const DEFI_PROGRAMS = /* @__PURE__ */ new Set([
|
|
2269
|
+
"JUP",
|
|
2270
|
+
"Raydium",
|
|
2271
|
+
"Orca",
|
|
2272
|
+
"Marinade",
|
|
2273
|
+
"Lido",
|
|
2274
|
+
"Lifinity",
|
|
2275
|
+
"Serum"
|
|
2276
|
+
// Add more as needed
|
|
2277
|
+
]);
|
|
2278
|
+
const NFT_PROGRAMS = /* @__PURE__ */ new Set([
|
|
2279
|
+
"Magic Eden",
|
|
2280
|
+
"Tensor",
|
|
2281
|
+
"OpenSea",
|
|
2282
|
+
"Metaplex"
|
|
2283
|
+
]);
|
|
2284
|
+
const GAMING_PROGRAMS = /* @__PURE__ */ new Set([
|
|
2285
|
+
"Star Atlas",
|
|
2286
|
+
"Genopets",
|
|
2287
|
+
"Aurory"
|
|
2288
|
+
]);
|
|
2289
|
+
const DAO_PROGRAMS = /* @__PURE__ */ new Set([
|
|
2290
|
+
"Realms",
|
|
2291
|
+
"Squads",
|
|
2292
|
+
"Tribeca"
|
|
2293
|
+
]);
|
|
2294
|
+
for (const inst of context.instructions) {
|
|
2295
|
+
const label = context.labels.get(inst.programId);
|
|
2296
|
+
const programName = label?.name || "";
|
|
2297
|
+
if (DEFI_PROGRAMS.has(programName) || /swap|pool|stake|lend|borrow/i.test(programName)) {
|
|
2298
|
+
activityTypes.add("DeFi");
|
|
2299
|
+
if (!activityDetails.has("DeFi")) {
|
|
2300
|
+
activityDetails.set("DeFi", { count: 0, programs: /* @__PURE__ */ new Set() });
|
|
2301
|
+
}
|
|
2302
|
+
const details = activityDetails.get("DeFi");
|
|
2303
|
+
details.count++;
|
|
2304
|
+
details.programs.add(programName || inst.programId.slice(0, 8));
|
|
2305
|
+
}
|
|
2306
|
+
if (NFT_PROGRAMS.has(programName) || /nft|marketplace|mint/i.test(programName)) {
|
|
2307
|
+
activityTypes.add("NFT");
|
|
2308
|
+
if (!activityDetails.has("NFT")) {
|
|
2309
|
+
activityDetails.set("NFT", { count: 0, programs: /* @__PURE__ */ new Set() });
|
|
2310
|
+
}
|
|
2311
|
+
const details = activityDetails.get("NFT");
|
|
2312
|
+
details.count++;
|
|
2313
|
+
details.programs.add(programName || inst.programId.slice(0, 8));
|
|
2314
|
+
}
|
|
2315
|
+
if (GAMING_PROGRAMS.has(programName) || /game|play/i.test(programName)) {
|
|
2316
|
+
activityTypes.add("Gaming");
|
|
2317
|
+
if (!activityDetails.has("Gaming")) {
|
|
2318
|
+
activityDetails.set("Gaming", { count: 0, programs: /* @__PURE__ */ new Set() });
|
|
2319
|
+
}
|
|
2320
|
+
const details = activityDetails.get("Gaming");
|
|
2321
|
+
details.count++;
|
|
2322
|
+
details.programs.add(programName || inst.programId.slice(0, 8));
|
|
2323
|
+
}
|
|
2324
|
+
if (DAO_PROGRAMS.has(programName) || /dao|governance|vote/i.test(programName)) {
|
|
2325
|
+
activityTypes.add("DAO");
|
|
2326
|
+
if (!activityDetails.has("DAO")) {
|
|
2327
|
+
activityDetails.set("DAO", { count: 0, programs: /* @__PURE__ */ new Set() });
|
|
2328
|
+
}
|
|
2329
|
+
const details = activityDetails.get("DAO");
|
|
2330
|
+
details.count++;
|
|
2331
|
+
details.programs.add(programName || inst.programId.slice(0, 8));
|
|
2332
|
+
}
|
|
2333
|
+
}
|
|
2334
|
+
const hasExchangeInteraction = Array.from(context.labels.values()).some((label) => label.type === "exchange");
|
|
2335
|
+
if (hasExchangeInteraction) {
|
|
2336
|
+
activityTypes.add("Exchange");
|
|
2337
|
+
activityDetails.set("Exchange", {
|
|
2338
|
+
count: context.transfers.filter(
|
|
2339
|
+
(t) => context.labels.has(t.from) || context.labels.has(t.to)
|
|
2340
|
+
).length,
|
|
2341
|
+
programs: /* @__PURE__ */ new Set(["CEX"])
|
|
2342
|
+
});
|
|
2343
|
+
}
|
|
2344
|
+
const simpleTransfers = context.transfers.filter((t) => {
|
|
2345
|
+
const tx = context.transactions?.find((tx2) => tx2.signature === t.signature);
|
|
2346
|
+
return tx && tx.programs && tx.programs.length <= 2;
|
|
2347
|
+
});
|
|
2348
|
+
if (simpleTransfers.length >= 3) {
|
|
2349
|
+
activityTypes.add("P2P Transfers");
|
|
2350
|
+
activityDetails.set("P2P Transfers", {
|
|
2351
|
+
count: simpleTransfers.length,
|
|
2352
|
+
programs: /* @__PURE__ */ new Set(["Direct"])
|
|
2353
|
+
});
|
|
2354
|
+
}
|
|
2355
|
+
const diversityCount = activityTypes.size;
|
|
2356
|
+
if (diversityCount >= 4) {
|
|
2357
|
+
const evidence = Array.from(activityDetails.entries()).map(([type, details]) => ({
|
|
2358
|
+
description: `${type}: ${details.count} transaction(s) across ${details.programs.size} program(s)`,
|
|
2359
|
+
severity: "HIGH",
|
|
2360
|
+
reference: void 0
|
|
2361
|
+
}));
|
|
2362
|
+
signals.push({
|
|
2363
|
+
id: "address-high-diversity",
|
|
2364
|
+
name: "High Activity Diversity on Single Address",
|
|
2365
|
+
severity: "HIGH",
|
|
2366
|
+
confidence: 0.85,
|
|
2367
|
+
category: "linkability",
|
|
2368
|
+
reason: `This address is used for ${diversityCount} distinct activity types: ${Array.from(activityTypes).join(", ")}.`,
|
|
2369
|
+
impact: "Using one address for multiple unrelated activities links them all together. This creates a comprehensive behavioral profile.",
|
|
2370
|
+
mitigation: "Use separate addresses for different purposes: one for DeFi, one for NFTs, one for DAO participation, etc. This isolates activities and prevents cross-linkage.",
|
|
2371
|
+
evidence
|
|
2372
|
+
});
|
|
2373
|
+
} else if (diversityCount === 3) {
|
|
2374
|
+
const evidence = Array.from(activityDetails.entries()).map(([type, details]) => ({
|
|
2375
|
+
description: `${type}: ${details.count} transaction(s)`,
|
|
2376
|
+
severity: "MEDIUM",
|
|
2377
|
+
reference: void 0
|
|
2378
|
+
}));
|
|
2379
|
+
signals.push({
|
|
2380
|
+
id: "address-moderate-diversity",
|
|
2381
|
+
name: "Moderate Activity Diversity on Single Address",
|
|
2382
|
+
severity: "MEDIUM",
|
|
2383
|
+
confidence: 0.7,
|
|
2384
|
+
category: "linkability",
|
|
2385
|
+
reason: `This address is used for ${diversityCount} activity types: ${Array.from(activityTypes).join(", ")}.`,
|
|
2386
|
+
impact: "Multiple activity types on one address create linkage between otherwise separate behaviors.",
|
|
2387
|
+
mitigation: "Consider using separate addresses for different activities to improve privacy compartmentalization.",
|
|
2388
|
+
evidence
|
|
2389
|
+
});
|
|
2390
|
+
}
|
|
2391
|
+
if (context.timeRange.earliest && context.timeRange.latest) {
|
|
2392
|
+
const timeSpanDays = (context.timeRange.latest - context.timeRange.earliest) / (60 * 60 * 24);
|
|
2393
|
+
if (timeSpanDays > 180 && context.transactionCount > 50) {
|
|
2394
|
+
signals.push({
|
|
2395
|
+
id: "address-long-term-usage",
|
|
2396
|
+
name: "Long-Term Single Address Usage",
|
|
2397
|
+
severity: "MEDIUM",
|
|
2398
|
+
confidence: 0.75,
|
|
2399
|
+
category: "behavioral",
|
|
2400
|
+
reason: `This address has been actively used for ${Math.round(timeSpanDays)} days with ${context.transactionCount} transactions.`,
|
|
2401
|
+
impact: "Long-term address usage accumulates a rich behavioral history. All activities over time are permanently linked.",
|
|
2402
|
+
mitigation: "Periodically rotate to new addresses to compartmentalize different time periods of activity.",
|
|
2403
|
+
evidence: [{
|
|
2404
|
+
description: `${context.transactionCount} transactions over ${Math.round(timeSpanDays)} days`,
|
|
2405
|
+
severity: "MEDIUM",
|
|
2406
|
+
reference: void 0
|
|
2407
|
+
}]
|
|
2408
|
+
});
|
|
2409
|
+
}
|
|
2410
|
+
}
|
|
2411
|
+
return signals;
|
|
2412
|
+
}
|
|
2413
|
+
|
|
1934
2414
|
// src/scanner/index.ts
|
|
1935
2415
|
var REPORT_VERSION = "1.0.0";
|
|
1936
2416
|
var HEURISTICS = [
|
|
1937
2417
|
// Solana-specific (highest priority)
|
|
1938
2418
|
detectFeePayerReuse,
|
|
1939
2419
|
detectSignerOverlap,
|
|
2420
|
+
detectMemoExposure,
|
|
2421
|
+
detectAddressReuse,
|
|
1940
2422
|
detectKnownEntityInteraction,
|
|
1941
2423
|
detectCounterpartyReuse,
|
|
1942
2424
|
detectInstructionFingerprinting,
|
|
@@ -1980,15 +2462,24 @@ function generateMitigations(signals) {
|
|
|
1980
2462
|
if (signalIds.has("token-account-churn") || signalIds.has("rent-refund-clustering")) {
|
|
1981
2463
|
mitigations.add("Avoid closing token accounts if privacy is important - the rent refund creates linkage.");
|
|
1982
2464
|
}
|
|
1983
|
-
if (signalIds.has("
|
|
2465
|
+
if (signalIds.has("memo-pii-exposure") || signalIds.has("memo-descriptive-content") || signalIds.has("memo-usage")) {
|
|
2466
|
+
mitigations.add("Never include personal information in transaction memos - they are permanently public.");
|
|
2467
|
+
}
|
|
2468
|
+
if (signalIds.has("address-high-diversity") || signalIds.has("address-moderate-diversity") || signalIds.has("address-long-term-usage")) {
|
|
2469
|
+
mitigations.add("Use separate addresses for different activity types to compartmentalize your behavior.");
|
|
2470
|
+
}
|
|
2471
|
+
if (signalIds.has("known-entity-exchange") || signalIds.has("known-entity-bridge") || signalIds.has("known-entity-other") || signalIds.has("known-entity-interaction")) {
|
|
1984
2472
|
mitigations.add("Avoid direct interactions between privacy-sensitive wallets and KYC services.");
|
|
1985
2473
|
}
|
|
1986
2474
|
if (signalIds.has("counterparty-reuse") || signalIds.has("pda-reuse")) {
|
|
1987
2475
|
mitigations.add("Use different addresses for different counterparties or contexts.");
|
|
1988
2476
|
}
|
|
1989
|
-
if (signalIds.has("timing-
|
|
2477
|
+
if (signalIds.has("timing-burst") || signalIds.has("timing-regular-interval") || signalIds.has("timing-timezone-pattern") || signalIds.has("timing-correlation")) {
|
|
1990
2478
|
mitigations.add("Introduce timing delays and vary transaction patterns to reduce correlation.");
|
|
1991
2479
|
}
|
|
2480
|
+
if (signalIds.has("balance-matching-pairs") || signalIds.has("balance-sequential-similar") || signalIds.has("balance-full-movement") || signalIds.has("balance-traceability")) {
|
|
2481
|
+
mitigations.add("Vary transfer amounts and add delays to reduce balance traceability.");
|
|
2482
|
+
}
|
|
1992
2483
|
if (signalIds.has("amount-reuse")) {
|
|
1993
2484
|
mitigations.add("Vary transaction amounts to avoid creating fingerprints.");
|
|
1994
2485
|
}
|
|
@@ -2128,12 +2619,14 @@ function createDefaultLabelProvider() {
|
|
|
2128
2619
|
collectTransactionData,
|
|
2129
2620
|
collectWalletData,
|
|
2130
2621
|
createDefaultLabelProvider,
|
|
2622
|
+
detectAddressReuse,
|
|
2131
2623
|
detectAmountReuse,
|
|
2132
2624
|
detectBalanceTraceability,
|
|
2133
2625
|
detectCounterpartyReuse,
|
|
2134
2626
|
detectFeePayerReuse,
|
|
2135
2627
|
detectInstructionFingerprinting,
|
|
2136
2628
|
detectKnownEntityInteraction,
|
|
2629
|
+
detectMemoExposure,
|
|
2137
2630
|
detectSignerOverlap,
|
|
2138
2631
|
detectTimingPatterns,
|
|
2139
2632
|
detectTokenAccountLifecycle,
|