Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
72 changes: 72 additions & 0 deletions rug_radar.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
# rug_radar.py
# Rug Radar Trap PoC for Drosera
#
# This script is a simple proof-of-concept to detect possible rug-pull risks
# in ERC-20 token contracts.

from web3 import Web3

# Connect to Ethereum RPC (you can change this to another RPC if you like)
w3 = Web3(Web3.HTTPProvider("https://rpc.ankr.com/eth"))

# Example token contract (replace with any ERC-20 address you want to test)
token_address = Web3.to_checksum_address("0x0000000000000000000000000000000000000000")

# Minimal ABI to check ERC-20 details
erc20_abi = [
{
"constant": True,
"inputs": [],
"name": "owner",
"outputs": [{"name": "", "type": "address"}],
"type": "function",
},
{
"constant": True,
"inputs": [],
"name": "totalSupply",
"outputs": [{"name": "", "type": "uint256"}],
"type": "function",
},
]

def scan_contract(address):
"""
Scans an ERC-20 contract for owner and totalSupply.

Parameters:
address (str): The ERC-20 token contract address.

Returns:
dict: Contains 'owner' and 'totalSupply' or None if data cannot be fetched.
"""
try:
contract = w3.eth.contract(address=address, abi=erc20_abi)

# Check owner
try:
owner = contract.functions.owner().call()
print(f"Owner address: {owner}")
except Exception:
owner = None
print("⚠️ Could not fetch owner (might be hidden or renounced)")

# Check total supply
try:
supply = contract.functions.totalSupply().call()
print(f"Total Supply: {supply}")
except Exception:
supply = None
print("⚠️ Could not fetch total supply")

print("✅ Rug Radar scan completed (PoC)")
return {"owner": owner, "totalSupply": supply}

except Exception as e:
print("Error connecting to contract:", e)
return None


# Run scan
if __name__ == "__main__":
scan_contract(token_address)
27 changes: 27 additions & 0 deletions rug_radar_trap/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# UniqueRug Radar Trap (PoC)

## Overview
This is a **proof of concept trap** idea for the Drosera ecosystem.
This trap monitors unusual changes in the **total supply** of an ERC-20 token.
A sudden drop in supply can be a red flag for manipulations or hidden risks.

## How It Works
- `collect()` → Reads `totalSupply()` from the token contract.
- `shouldRespond()` → Compares consecutive values.
- If supply decreases by **15%**, it triggers a response.

## Example Test Log
collect() #1 → totalSupply: 1,000,000
collect() #2 → totalSupply: 850,000
shouldRespond([#1, #2]) → true (drop 15%)

⚠️ *These logs are test/example values only — no live tokens were used.*

## Why It Matters
- Sudden supply drops may indicate **burn exploits, hidden logic, or rug-pull patterns**.
- Helps illustrate how Drosera traps can highlight **token integrity risks**.

## Benefits
- **Developers:** Quick detection of abnormal token behavior.
- **Auditors:** Structured on-chain data for analysis.
- **Users:** Early warning of potential manipulations.
82 changes: 82 additions & 0 deletions traps/unique_rugradar/contracts/UniqueRugRadar.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

contract UniqueRugRadar {
address public targetToken;
uint256 public thresholdPct;

event Collected(address token, address owner, bool ownerFound, uint256 totalSupply);

constructor(address _token, uint256 _thresholdPct) {
require(_token != address(0), "token = zero");
targetToken = _token;
thresholdPct = _thresholdPct;
}

function _tryOwner(address token) internal view returns (address owner, bool found) {
bytes4 sel = bytes4(keccak256("owner()"));
(bool ok, bytes memory data) = token.staticcall(abi.encodeWithSelector(sel));
if (ok && data.length >= 32) {
owner = abi.decode(data, (address));
found = true;
} else {
owner = address(0);
found = false;
}
}

function collect() external view returns (bytes memory) {
(address owner, bool ownerFound) = _tryOwner(targetToken);

uint256 ts;
bool tsFound;
(bool ok, bytes memory data) = targetToken.staticcall(abi.encodeWithSelector(bytes4(keccak256("totalSupply()"))));
if (ok && data.length >= 32) {
ts = abi.decode(data, (uint256));
tsFound = true;
} else {
ts = 0;
tsFound = false;
}

emit Collected(targetToken, owner, ownerFound, ts);
return abi.encode(ownerFound, owner, tsFound, ts);
}

function shouldRespond(bytes[] calldata data) external pure returns (bool, bytes memory) {
if (data.length < 2) {
return (false, abi.encode(uint256(0)));
}

(, , bool tsFoundNew, uint256 tsNew) = abi.decode(data[0], (bool, address, bool, uint256));
(, , bool tsFoundPrev, uint256 tsPrev) = abi.decode(data[1], (bool, address, bool, uint256));

if (!tsFoundNew || !tsFoundPrev) {
return (false, abi.encode(uint256(0)));
}

if (tsPrev == 0) {
return (false, abi.encode(uint256(0)));
}

if (tsNew >= tsPrev) {
return (false, abi.encode(uint256(0)));
}

uint256 drop = ((tsPrev - tsNew) * 100) / tsPrev;

if (drop >= 10) {
return (true, abi.encode(drop));
}

return (false, abi.encode(drop));
}

function setTargetToken(address _token) external {
targetToken = _token;
}

function setThreshold(uint256 _pct) external {
thresholdPct = _pct;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
name: UniqueRugRadar
description: "Test trap that collects owner() and totalSupply() for a test token and fires when totalSupply drops >= 10%."
version: 0.1.0
entrypoint: contracts/UniqueRugRadar.sol
author: tollynft
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
chain = "holesky"
ethereum_rpc = "https://ethereum-holesky-rpc.publicnode.com"
response_contract = ""
response_function = ""
path = "contracts/UniqueRugRadar.sol"
# Use DROSERA_PRIVATE_KEY env var locally when running drosera apply