cryptoshield 0.2.0__py3-none-any.whl
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.
- cryptoshield/__init__.py +4 -0
- cryptoshield/approvals.py +185 -0
- cryptoshield/cli.py +166 -0
- cryptoshield/db.py +50 -0
- cryptoshield/honeypot.py +219 -0
- cryptoshield/phishing.py +454 -0
- cryptoshield/rugpull.py +141 -0
- cryptoshield/solana.py +264 -0
- cryptoshield/utils.py +194 -0
- cryptoshield-0.2.0.dist-info/METADATA +157 -0
- cryptoshield-0.2.0.dist-info/RECORD +15 -0
- cryptoshield-0.2.0.dist-info/WHEEL +5 -0
- cryptoshield-0.2.0.dist-info/entry_points.txt +2 -0
- cryptoshield-0.2.0.dist-info/licenses/LICENSE +21 -0
- cryptoshield-0.2.0.dist-info/top_level.txt +1 -0
cryptoshield/phishing.py
ADDED
|
@@ -0,0 +1,454 @@
|
|
|
1
|
+
"""Phishing URL checker — detect crypto scam websites.
|
|
2
|
+
|
|
3
|
+
Checks against known scam databases and analyzes URL patterns.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import re
|
|
7
|
+
from urllib.parse import urlparse
|
|
8
|
+
from .utils import fetch_json, print_header, print_ok, print_warn, print_fail, print_info
|
|
9
|
+
|
|
10
|
+
# Known legitimate domains
|
|
11
|
+
LEGIT_DOMAINS = {
|
|
12
|
+
"uniswap.org", "app.uniswap.org",
|
|
13
|
+
"aave.com", "app.aave.com",
|
|
14
|
+
"opensea.io",
|
|
15
|
+
"blur.io",
|
|
16
|
+
"pancakeswap.finance", "pancakeswap.io",
|
|
17
|
+
"sushi.com",
|
|
18
|
+
"compound.finance",
|
|
19
|
+
"makerdao.com",
|
|
20
|
+
"lido.fi",
|
|
21
|
+
"curve.fi",
|
|
22
|
+
"1inch.io",
|
|
23
|
+
"metamask.io",
|
|
24
|
+
"phantom.app",
|
|
25
|
+
"solflare.com",
|
|
26
|
+
"walletconnect.com",
|
|
27
|
+
"rainbow.me",
|
|
28
|
+
"argent.xyz",
|
|
29
|
+
"rabby.io",
|
|
30
|
+
"paraswap.io",
|
|
31
|
+
"dydx.exchange",
|
|
32
|
+
"gmx.io",
|
|
33
|
+
"arbitrum.io",
|
|
34
|
+
"optimism.io",
|
|
35
|
+
"base.org",
|
|
36
|
+
"zksync.io",
|
|
37
|
+
"starknet.io",
|
|
38
|
+
"layerzero.network",
|
|
39
|
+
"wormhole.com",
|
|
40
|
+
"jito.network",
|
|
41
|
+
"marinade.finance",
|
|
42
|
+
"jup.ag",
|
|
43
|
+
"raydium.io",
|
|
44
|
+
"orca.so",
|
|
45
|
+
"magic-eden.io",
|
|
46
|
+
"tensorhq.xyz",
|
|
47
|
+
"pump.fun",
|
|
48
|
+
"dextools.io",
|
|
49
|
+
"dexscreener.com",
|
|
50
|
+
"etherscan.io",
|
|
51
|
+
"bscscan.com",
|
|
52
|
+
"polygonscan.com",
|
|
53
|
+
"arbiscan.io",
|
|
54
|
+
"basescan.org",
|
|
55
|
+
"solscan.io",
|
|
56
|
+
"coingecko.com",
|
|
57
|
+
"coinmarketcap.com",
|
|
58
|
+
"binance.com",
|
|
59
|
+
"coinbase.com",
|
|
60
|
+
"kraken.com",
|
|
61
|
+
"okx.com",
|
|
62
|
+
"bybit.com",
|
|
63
|
+
"kucoin.com",
|
|
64
|
+
"gate.io",
|
|
65
|
+
"huobi.com",
|
|
66
|
+
"poloniex.com",
|
|
67
|
+
"bitfinex.com",
|
|
68
|
+
"gemini.com",
|
|
69
|
+
"bitstamp.net",
|
|
70
|
+
"crypto.com",
|
|
71
|
+
"eigenlayer.xyz",
|
|
72
|
+
"stargate.finance",
|
|
73
|
+
"jumper.exchange",
|
|
74
|
+
"orbiter.finance",
|
|
75
|
+
"rhino.fi",
|
|
76
|
+
"synapseprotocol.com",
|
|
77
|
+
"multichain.org",
|
|
78
|
+
"celer.network",
|
|
79
|
+
"debridge.finance",
|
|
80
|
+
"li.fi",
|
|
81
|
+
"socket.tech",
|
|
82
|
+
"bungee.exchange",
|
|
83
|
+
"chainlist.org",
|
|
84
|
+
"revoke.cash",
|
|
85
|
+
"debank.com",
|
|
86
|
+
"zapper.fi",
|
|
87
|
+
"zerion.io",
|
|
88
|
+
"matcha.xyz",
|
|
89
|
+
"slingshot.finance",
|
|
90
|
+
"kwenta.io",
|
|
91
|
+
"velodrome.finance",
|
|
92
|
+
"aerodrome.finance",
|
|
93
|
+
"camelot.exchange",
|
|
94
|
+
"traderjoexyz.com",
|
|
95
|
+
"spookyswap.finance",
|
|
96
|
+
"spiritswap.finance",
|
|
97
|
+
"beets.fi",
|
|
98
|
+
"balancer.fi",
|
|
99
|
+
"frax.finance",
|
|
100
|
+
"convexfinance.com",
|
|
101
|
+
"yearn.finance",
|
|
102
|
+
"synthetix.io",
|
|
103
|
+
"perp.com",
|
|
104
|
+
"gains.trade",
|
|
105
|
+
"mux.network",
|
|
106
|
+
"aevo.xyz",
|
|
107
|
+
"premia.finance",
|
|
108
|
+
"lyra.finance",
|
|
109
|
+
"hegic.co",
|
|
110
|
+
"ribbon.finance",
|
|
111
|
+
"opyn.co",
|
|
112
|
+
"squeeth.com",
|
|
113
|
+
"aztec.network",
|
|
114
|
+
"scroll.io",
|
|
115
|
+
"linea.build",
|
|
116
|
+
"mantle.xyz",
|
|
117
|
+
"manta.network",
|
|
118
|
+
"connext.network",
|
|
119
|
+
"across.to",
|
|
120
|
+
"hop.exchange",
|
|
121
|
+
"satellite.axelar.network",
|
|
122
|
+
"squidrouter.com",
|
|
123
|
+
"chainge.finance",
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
# Suspicious keywords in URLs
|
|
127
|
+
SCAM_PATTERNS = [
|
|
128
|
+
r"airdrop",
|
|
129
|
+
r"claim",
|
|
130
|
+
r"free",
|
|
131
|
+
r"bonus",
|
|
132
|
+
r"reward",
|
|
133
|
+
r"gift",
|
|
134
|
+
r"presale",
|
|
135
|
+
r"pre-sale",
|
|
136
|
+
r"whitelist",
|
|
137
|
+
r"mint-free",
|
|
138
|
+
r"connect-wallet",
|
|
139
|
+
r"verify",
|
|
140
|
+
r"restore",
|
|
141
|
+
r"sync",
|
|
142
|
+
r"validate",
|
|
143
|
+
r"update.*wallet",
|
|
144
|
+
r"security.*alert",
|
|
145
|
+
r"suspended",
|
|
146
|
+
r"limited.*time",
|
|
147
|
+
r"act.*now",
|
|
148
|
+
r"urgent",
|
|
149
|
+
r"migration",
|
|
150
|
+
r"migrate",
|
|
151
|
+
r"unlock",
|
|
152
|
+
r"activate",
|
|
153
|
+
r"confirm.*seed",
|
|
154
|
+
r"seed.*phrase",
|
|
155
|
+
r"private.*key",
|
|
156
|
+
r"recovery",
|
|
157
|
+
r"support.*chat",
|
|
158
|
+
r"live.*chat",
|
|
159
|
+
r"help.*desk",
|
|
160
|
+
r"fix.*wallet",
|
|
161
|
+
r"repair",
|
|
162
|
+
r"rectify",
|
|
163
|
+
r"resolve.*issue",
|
|
164
|
+
r"pending.*transaction",
|
|
165
|
+
r"failed.*transaction",
|
|
166
|
+
r"stuck.*funds",
|
|
167
|
+
r"frozen.*account",
|
|
168
|
+
r"kyc.*verify",
|
|
169
|
+
r"identity.*verify",
|
|
170
|
+
r"document.*upload",
|
|
171
|
+
r"passport",
|
|
172
|
+
r"driver.*license",
|
|
173
|
+
r"social.*security",
|
|
174
|
+
r"tax.*refund",
|
|
175
|
+
r"gas.*fee.*refund",
|
|
176
|
+
r"rebate",
|
|
177
|
+
r"cashback",
|
|
178
|
+
r"double.*your",
|
|
179
|
+
r"multiply",
|
|
180
|
+
r"investment.*opportunity",
|
|
181
|
+
r"guaranteed.*profit",
|
|
182
|
+
r"risk.*free",
|
|
183
|
+
r"passive.*income",
|
|
184
|
+
r"financial.*freedom",
|
|
185
|
+
r"early.*access",
|
|
186
|
+
r"beta.*test",
|
|
187
|
+
r"exclusive.*offer",
|
|
188
|
+
r"limited.*supply",
|
|
189
|
+
r"last.*chance",
|
|
190
|
+
r"final.*notice",
|
|
191
|
+
r"action.*required",
|
|
192
|
+
r"immediate.*attention",
|
|
193
|
+
]
|
|
194
|
+
|
|
195
|
+
# TLDs commonly used by scammers
|
|
196
|
+
SUSPICIOUS_TLDS = [
|
|
197
|
+
".xyz", ".top", ".club", ".buzz", ".click", ".link",
|
|
198
|
+
".site", ".online", ".icu", ".work", ".fit", ".monster",
|
|
199
|
+
".cfd", ".sbs", ".surf", ".rest", ".cam", ".hair",
|
|
200
|
+
".mom", ".lol", ".bond", ".cyou", ".uno", ".sarl",
|
|
201
|
+
".mov", ".zip", ".py", ".sh", ".tk", ".ml", ".ga", ".cf", ".gq",
|
|
202
|
+
]
|
|
203
|
+
|
|
204
|
+
# Known scam domains (community-reported)
|
|
205
|
+
KNOWN_SCAM_DOMAINS = {
|
|
206
|
+
"uniswap-airdrop.com",
|
|
207
|
+
"metamask-airdrop.com",
|
|
208
|
+
"metamask-sync.com",
|
|
209
|
+
"metamask-restore.com",
|
|
210
|
+
"metamask-verify.com",
|
|
211
|
+
"metamask-update.com",
|
|
212
|
+
"phantom-airdrop.com",
|
|
213
|
+
"phantom-sync.com",
|
|
214
|
+
"phantom-restore.com",
|
|
215
|
+
"arbitrum-airdrop.com",
|
|
216
|
+
"arbitrum-claim.com",
|
|
217
|
+
"optimism-airdrop.com",
|
|
218
|
+
"starknet-airdrop.com",
|
|
219
|
+
"zksync-airdrop.com",
|
|
220
|
+
"layerzero-airdrop.com",
|
|
221
|
+
"eigenlayer-airdrop.com",
|
|
222
|
+
"jito-airdrop.com",
|
|
223
|
+
"jupiter-airdrop.com",
|
|
224
|
+
"blur-airdrop.com",
|
|
225
|
+
"pudgypenguins-airdrop.com",
|
|
226
|
+
"apecoin-claim.com",
|
|
227
|
+
"uniswapv3.com",
|
|
228
|
+
"app-uniswap.org",
|
|
229
|
+
"uniswap-app.org",
|
|
230
|
+
"metamask-download.com",
|
|
231
|
+
"metamask-extension.com",
|
|
232
|
+
"metamask-io.com",
|
|
233
|
+
"metamask-wallet.com",
|
|
234
|
+
"pancakeswap-finance.com",
|
|
235
|
+
"aave-protocol.com",
|
|
236
|
+
"opensea-io.com",
|
|
237
|
+
"opensea-nft.com",
|
|
238
|
+
"coinbase-airdrop.com",
|
|
239
|
+
"binance-airdrop.com",
|
|
240
|
+
"crypto-com-airdrop.com",
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def check_url(url: str) -> dict:
|
|
245
|
+
"""Analyze a URL for phishing indicators."""
|
|
246
|
+
# Normalize URL
|
|
247
|
+
if not url.startswith("http"):
|
|
248
|
+
url = "https://" + url
|
|
249
|
+
|
|
250
|
+
parsed = urlparse(url)
|
|
251
|
+
domain = parsed.netloc.lower()
|
|
252
|
+
if domain.startswith("www."):
|
|
253
|
+
domain = domain[4:]
|
|
254
|
+
|
|
255
|
+
report = {
|
|
256
|
+
"url": url,
|
|
257
|
+
"domain": domain,
|
|
258
|
+
"path": parsed.path,
|
|
259
|
+
"is_legit": False,
|
|
260
|
+
"is_suspicious": False,
|
|
261
|
+
"is_known_scam": False,
|
|
262
|
+
"indicators": [],
|
|
263
|
+
"risk_score": 0,
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
# 0. Check known scam domains FIRST
|
|
267
|
+
base_domain = _get_base_domain(domain)
|
|
268
|
+
if domain in KNOWN_SCAM_DOMAINS or base_domain in KNOWN_SCAM_DOMAINS:
|
|
269
|
+
report["is_known_scam"] = True
|
|
270
|
+
report["is_suspicious"] = True
|
|
271
|
+
report["indicators"].append({
|
|
272
|
+
"type": "FAIL",
|
|
273
|
+
"detail": f"KNOWN SCAM DOMAIN — {domain} is in scam database"
|
|
274
|
+
})
|
|
275
|
+
report["risk_score"] = 90
|
|
276
|
+
return report
|
|
277
|
+
|
|
278
|
+
# 1. Check if it's a known legitimate domain
|
|
279
|
+
if base_domain in LEGIT_DOMAINS or domain in LEGIT_DOMAINS:
|
|
280
|
+
report["is_legit"] = True
|
|
281
|
+
report["indicators"].append({"type": "OK", "detail": f"Known legitimate domain: {base_domain}"})
|
|
282
|
+
report["risk_score"] = 0
|
|
283
|
+
return report
|
|
284
|
+
|
|
285
|
+
report["indicators"].append({"type": "INFO", "detail": f"Unknown domain: {domain}"})
|
|
286
|
+
report["risk_score"] += 10
|
|
287
|
+
|
|
288
|
+
# 2. Check for typosquatting (similar to legit domains)
|
|
289
|
+
for legit in LEGIT_DOMAINS:
|
|
290
|
+
similarity = _domain_similarity(domain, legit)
|
|
291
|
+
if similarity > 0.7 and similarity < 1.0:
|
|
292
|
+
report["is_suspicious"] = True
|
|
293
|
+
report["indicators"].append({
|
|
294
|
+
"type": "FAIL",
|
|
295
|
+
"detail": f"Possible typosquat of {legit} (similarity: {similarity:.0%})"
|
|
296
|
+
})
|
|
297
|
+
report["risk_score"] += 30
|
|
298
|
+
|
|
299
|
+
# 3. Check for scam patterns in URL
|
|
300
|
+
full_url = url.lower()
|
|
301
|
+
matched_patterns = []
|
|
302
|
+
for pattern in SCAM_PATTERNS:
|
|
303
|
+
if re.search(pattern, full_url):
|
|
304
|
+
matched_patterns.append(pattern)
|
|
305
|
+
|
|
306
|
+
if matched_patterns:
|
|
307
|
+
report["is_suspicious"] = True
|
|
308
|
+
if len(matched_patterns) == 1:
|
|
309
|
+
report["indicators"].append({
|
|
310
|
+
"type": "WARN",
|
|
311
|
+
"detail": f"Suspicious pattern: '{matched_patterns[0]}' in URL"
|
|
312
|
+
})
|
|
313
|
+
report["risk_score"] += 10
|
|
314
|
+
else:
|
|
315
|
+
report["indicators"].append({
|
|
316
|
+
"type": "FAIL",
|
|
317
|
+
"detail": f"Multiple suspicious patterns ({len(matched_patterns)}): {', '.join(matched_patterns[:3])}"
|
|
318
|
+
})
|
|
319
|
+
report["risk_score"] += 15 * len(matched_patterns)
|
|
320
|
+
|
|
321
|
+
# 4. Check TLD
|
|
322
|
+
tld = "." + domain.split(".")[-1]
|
|
323
|
+
if tld in SUSPICIOUS_TLDS:
|
|
324
|
+
report["indicators"].append({
|
|
325
|
+
"type": "WARN",
|
|
326
|
+
"detail": f"Suspicious TLD: {tld}"
|
|
327
|
+
})
|
|
328
|
+
report["risk_score"] += 5
|
|
329
|
+
|
|
330
|
+
# 5. Check for IP address instead of domain
|
|
331
|
+
if re.match(r"^\d+\.\d+\.\d+\.\d+", domain):
|
|
332
|
+
report["is_suspicious"] = True
|
|
333
|
+
report["indicators"].append({
|
|
334
|
+
"type": "FAIL",
|
|
335
|
+
"detail": "IP address used instead of domain name"
|
|
336
|
+
})
|
|
337
|
+
report["risk_score"] += 25
|
|
338
|
+
|
|
339
|
+
# 6. Check for excessive subdomains
|
|
340
|
+
subdomain_count = domain.count(".") - 1
|
|
341
|
+
if subdomain_count > 2:
|
|
342
|
+
report["is_suspicious"] = True
|
|
343
|
+
report["indicators"].append({
|
|
344
|
+
"type": "WARN",
|
|
345
|
+
"detail": f"Excessive subdomains ({subdomain_count})"
|
|
346
|
+
})
|
|
347
|
+
report["risk_score"] += 10
|
|
348
|
+
|
|
349
|
+
# 7. Check for punycode (IDN homograph attack)
|
|
350
|
+
if "xn--" in domain:
|
|
351
|
+
report["is_suspicious"] = True
|
|
352
|
+
report["indicators"].append({
|
|
353
|
+
"type": "FAIL",
|
|
354
|
+
"detail": "Punycode domain — possible IDN homograph attack"
|
|
355
|
+
})
|
|
356
|
+
report["risk_score"] += 20
|
|
357
|
+
|
|
358
|
+
# 8. Check domain length
|
|
359
|
+
if len(domain) > 30:
|
|
360
|
+
report["indicators"].append({
|
|
361
|
+
"type": "WARN",
|
|
362
|
+
"detail": f"Very long domain name ({len(domain)} chars)"
|
|
363
|
+
})
|
|
364
|
+
report["risk_score"] += 5
|
|
365
|
+
|
|
366
|
+
# 9. Check for hyphens (common in phishing)
|
|
367
|
+
if domain.count("-") > 2:
|
|
368
|
+
report["indicators"].append({
|
|
369
|
+
"type": "WARN",
|
|
370
|
+
"detail": f"Multiple hyphens in domain ({domain.count('-')} hyphens)"
|
|
371
|
+
})
|
|
372
|
+
report["risk_score"] += 5
|
|
373
|
+
|
|
374
|
+
# 10. Check for brand names in non-brand domains
|
|
375
|
+
brand_domains = ["uniswap", "metamask", "phantom", "aave", "opensea", "blur",
|
|
376
|
+
"pancakeswap", "sushi", "compound", "lido", "curve", "binance",
|
|
377
|
+
"coinbase", "kraken", "okx", "bybit", "arbitrum", "optimism",
|
|
378
|
+
"zksync", "starknet", "eigenlayer", "jito", "jupiter", "raydium"]
|
|
379
|
+
for brand in brand_domains:
|
|
380
|
+
if brand in domain and base_domain not in LEGIT_DOMAINS:
|
|
381
|
+
report["is_suspicious"] = True
|
|
382
|
+
report["indicators"].append({
|
|
383
|
+
"type": "FAIL",
|
|
384
|
+
"detail": f"Brand name '{brand}' in non-official domain"
|
|
385
|
+
})
|
|
386
|
+
report["risk_score"] += 25
|
|
387
|
+
break
|
|
388
|
+
|
|
389
|
+
# Cap score
|
|
390
|
+
report["risk_score"] = min(report["risk_score"], 100)
|
|
391
|
+
|
|
392
|
+
if report["risk_score"] >= 40:
|
|
393
|
+
report["is_suspicious"] = True
|
|
394
|
+
|
|
395
|
+
return report
|
|
396
|
+
|
|
397
|
+
|
|
398
|
+
def print_phishing_report(report: dict):
|
|
399
|
+
"""Pretty-print URL check results."""
|
|
400
|
+
print_header(f"PHISHING CHECK — {report['domain']}")
|
|
401
|
+
|
|
402
|
+
if report.get("is_known_scam"):
|
|
403
|
+
print_fail("🚨 KNOWN SCAM DOMAIN — DO NOT VISIT")
|
|
404
|
+
for ind in report["indicators"]:
|
|
405
|
+
print_fail(ind["detail"])
|
|
406
|
+
return
|
|
407
|
+
|
|
408
|
+
if report["is_legit"]:
|
|
409
|
+
print_ok("Known legitimate domain")
|
|
410
|
+
return
|
|
411
|
+
|
|
412
|
+
for ind in report["indicators"]:
|
|
413
|
+
if ind["type"] == "OK":
|
|
414
|
+
print_ok(ind["detail"])
|
|
415
|
+
elif ind["type"] == "WARN":
|
|
416
|
+
print_warn(ind["detail"])
|
|
417
|
+
elif ind["type"] == "FAIL":
|
|
418
|
+
print_fail(ind["detail"])
|
|
419
|
+
else:
|
|
420
|
+
print_info(ind["detail"])
|
|
421
|
+
|
|
422
|
+
print()
|
|
423
|
+
score = report["risk_score"]
|
|
424
|
+
if score <= 20:
|
|
425
|
+
print_ok(f"Risk Score: {score}/100 — Likely safe")
|
|
426
|
+
elif score <= 50:
|
|
427
|
+
print_warn(f"Risk Score: {score}/100 — Suspicious — verify carefully")
|
|
428
|
+
else:
|
|
429
|
+
print_fail(f"Risk Score: {score}/100 — HIGH RISK — likely phishing")
|
|
430
|
+
|
|
431
|
+
|
|
432
|
+
def _get_base_domain(domain: str) -> str:
|
|
433
|
+
"""Extract base domain (e.g., app.uniswap.org → uniswap.org)."""
|
|
434
|
+
parts = domain.split(".")
|
|
435
|
+
if len(parts) >= 2:
|
|
436
|
+
return ".".join(parts[-2:])
|
|
437
|
+
return domain
|
|
438
|
+
|
|
439
|
+
|
|
440
|
+
def _domain_similarity(a: str, b: str) -> float:
|
|
441
|
+
"""Simple character-level similarity between two domains."""
|
|
442
|
+
a_base = _get_base_domain(a)
|
|
443
|
+
b_base = _get_base_domain(b)
|
|
444
|
+
|
|
445
|
+
if a_base == b_base:
|
|
446
|
+
return 1.0
|
|
447
|
+
|
|
448
|
+
# Levenshtein-like ratio
|
|
449
|
+
longer = max(len(a_base), len(b_base))
|
|
450
|
+
if longer == 0:
|
|
451
|
+
return 1.0
|
|
452
|
+
|
|
453
|
+
matches = sum(1 for x, y in zip(a_base, b_base) if x == y)
|
|
454
|
+
return matches / longer
|
cryptoshield/rugpull.py
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
"""Rugpull risk scorer — analyze contracts for common rug patterns.
|
|
2
|
+
|
|
3
|
+
Combines on-chain data + heuristics to calculate a risk score.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from web3 import Web3
|
|
7
|
+
from .utils import (
|
|
8
|
+
get_web3, checksum, ERC20_ABI, label_address,
|
|
9
|
+
print_header, print_ok, print_warn, print_fail, print_info,
|
|
10
|
+
)
|
|
11
|
+
from .honeypot import check_honeypot
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def analyze_rugpull(address: str, chain: str = "eth") -> dict:
|
|
15
|
+
"""Analyze a token contract for rugpull risk."""
|
|
16
|
+
w3 = get_web3(chain)
|
|
17
|
+
addr = checksum(address)
|
|
18
|
+
|
|
19
|
+
report = {
|
|
20
|
+
"address": addr,
|
|
21
|
+
"chain": chain,
|
|
22
|
+
"checks": [],
|
|
23
|
+
"score": 0,
|
|
24
|
+
"risk_level": "UNKNOWN",
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
contract = w3.eth.contract(address=addr, abi=ERC20_ABI)
|
|
28
|
+
|
|
29
|
+
# 1. Check if contract is verified (has code)
|
|
30
|
+
code = w3.eth.get_code(addr)
|
|
31
|
+
if code == b"" or code == b"0x":
|
|
32
|
+
report["checks"].append({"name": "Contract Code", "status": "FAIL", "detail": "No contract code — not a contract"})
|
|
33
|
+
report["score"] += 50
|
|
34
|
+
else:
|
|
35
|
+
report["checks"].append({"name": "Contract Code", "status": "OK", "detail": f"{len(code)} bytes deployed"})
|
|
36
|
+
|
|
37
|
+
# 2. Check if owner is renounced
|
|
38
|
+
try:
|
|
39
|
+
owner = contract.functions.owner().call()
|
|
40
|
+
zero = "0x0000000000000000000000000000000000000000"
|
|
41
|
+
dead = "0x000000000000000000000000000000000000dEaD"
|
|
42
|
+
if owner.lower() in (zero, dead):
|
|
43
|
+
report["checks"].append({"name": "Ownership", "status": "OK", "detail": "Owner renounced"})
|
|
44
|
+
else:
|
|
45
|
+
report["checks"].append({"name": "Ownership", "status": "WARN", "detail": f"Owner: {label_address(owner)}"})
|
|
46
|
+
report["score"] += 10
|
|
47
|
+
except Exception:
|
|
48
|
+
report["checks"].append({"name": "Ownership", "status": "INFO", "detail": "No owner function (could be good or bad)"})
|
|
49
|
+
|
|
50
|
+
# 3. Check total supply concentration
|
|
51
|
+
try:
|
|
52
|
+
total_supply = contract.functions.totalSupply().call()
|
|
53
|
+
if total_supply > 0:
|
|
54
|
+
report["checks"].append({"name": "Total Supply", "status": "INFO", "detail": f"{total_supply:,}"})
|
|
55
|
+
else:
|
|
56
|
+
report["checks"].append({"name": "Total Supply", "status": "FAIL", "detail": "Zero supply"})
|
|
57
|
+
report["score"] += 20
|
|
58
|
+
except Exception:
|
|
59
|
+
report["checks"].append({"name": "Total Supply", "status": "FAIL", "detail": "Cannot read supply"})
|
|
60
|
+
|
|
61
|
+
# 4. Check liquidity (look for LP tokens)
|
|
62
|
+
# This is a simplified check — real check would scan DEX pairs
|
|
63
|
+
report["checks"].append({"name": "Liquidity", "status": "INFO", "detail": "Check manually on DEX Screener"})
|
|
64
|
+
|
|
65
|
+
# 5. Get GoPlus data for additional checks
|
|
66
|
+
goplus = check_honeypot(addr, chain)
|
|
67
|
+
if "error" not in goplus:
|
|
68
|
+
if goplus.get("is_honeypot"):
|
|
69
|
+
report["score"] += 40
|
|
70
|
+
report["checks"].append({"name": "Honeypot", "status": "FAIL", "detail": "Token is a honeypot"})
|
|
71
|
+
else:
|
|
72
|
+
report["checks"].append({"name": "Honeypot", "status": "OK", "detail": "Not a honeypot"})
|
|
73
|
+
|
|
74
|
+
if not goplus.get("is_open_source"):
|
|
75
|
+
report["score"] += 15
|
|
76
|
+
report["checks"].append({"name": "Source Code", "status": "FAIL", "detail": "Not verified"})
|
|
77
|
+
else:
|
|
78
|
+
report["checks"].append({"name": "Source Code", "status": "OK", "detail": "Verified on explorer"})
|
|
79
|
+
|
|
80
|
+
if goplus.get("is_proxy"):
|
|
81
|
+
report["score"] += 5
|
|
82
|
+
report["checks"].append({"name": "Proxy", "status": "WARN", "detail": "Proxy contract — logic can change"})
|
|
83
|
+
|
|
84
|
+
if goplus.get("selfdestruct"):
|
|
85
|
+
report["score"] += 15
|
|
86
|
+
report["checks"].append({"name": "Self-Destruct", "status": "FAIL", "detail": "Has selfdestruct function"})
|
|
87
|
+
|
|
88
|
+
if goplus.get("hidden_owner"):
|
|
89
|
+
report["score"] += 10
|
|
90
|
+
report["checks"].append({"name": "Hidden Owner", "status": "FAIL", "detail": "Hidden owner detected"})
|
|
91
|
+
|
|
92
|
+
if goplus.get("owner_can_mint"):
|
|
93
|
+
report["score"] += 10
|
|
94
|
+
report["checks"].append({"name": "Mintable", "status": "WARN", "detail": "Owner can mint new tokens"})
|
|
95
|
+
|
|
96
|
+
holder_count = goplus.get("holder_count", 0)
|
|
97
|
+
if holder_count:
|
|
98
|
+
report["checks"].append({"name": "Holders", "status": "INFO", "detail": f"{holder_count:,} holders"})
|
|
99
|
+
|
|
100
|
+
# Cap score at 100
|
|
101
|
+
report["score"] = min(report["score"], 100)
|
|
102
|
+
|
|
103
|
+
# Risk level
|
|
104
|
+
if report["score"] <= 20:
|
|
105
|
+
report["risk_level"] = "LOW"
|
|
106
|
+
elif report["score"] <= 50:
|
|
107
|
+
report["risk_level"] = "MEDIUM"
|
|
108
|
+
elif report["score"] <= 75:
|
|
109
|
+
report["risk_level"] = "HIGH"
|
|
110
|
+
else:
|
|
111
|
+
report["risk_level"] = "CRITICAL"
|
|
112
|
+
|
|
113
|
+
return report
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def print_rugpull_report(report: dict):
|
|
117
|
+
"""Pretty-print rugpull analysis."""
|
|
118
|
+
print_header(f"RUGPULL SCORE — {report['address'][:6]}...{report['address'][-4:]}")
|
|
119
|
+
|
|
120
|
+
for check in report["checks"]:
|
|
121
|
+
status = check["status"]
|
|
122
|
+
name = check["name"]
|
|
123
|
+
detail = check["detail"]
|
|
124
|
+
if status == "OK":
|
|
125
|
+
print_ok(f"{name}: {detail}")
|
|
126
|
+
elif status == "WARN":
|
|
127
|
+
print_warn(f"{name}: {detail}")
|
|
128
|
+
elif status == "FAIL":
|
|
129
|
+
print_fail(f"{name}: {detail}")
|
|
130
|
+
else:
|
|
131
|
+
print_info(f"{name}: {detail}")
|
|
132
|
+
|
|
133
|
+
print()
|
|
134
|
+
score = report["score"]
|
|
135
|
+
level = report["risk_level"]
|
|
136
|
+
if level == "LOW":
|
|
137
|
+
print_ok(f"Risk Score: {score}/100 — {level} RISK")
|
|
138
|
+
elif level == "MEDIUM":
|
|
139
|
+
print_warn(f"Risk Score: {score}/100 — {level} RISK")
|
|
140
|
+
else:
|
|
141
|
+
print_fail(f"Risk Score: {score}/100 — {level} RISK")
|