diff --git a/rug_radar.py b/rug_radar.py new file mode 100644 index 0000000..732a144 --- /dev/null +++ b/rug_radar.py @@ -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) diff --git a/rug_radar_trap/README.md b/rug_radar_trap/README.md new file mode 100644 index 0000000..722bb80 --- /dev/null +++ b/rug_radar_trap/README.md @@ -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. diff --git a/traps/unique_rugradar/contracts/UniqueRugRadar.sol b/traps/unique_rugradar/contracts/UniqueRugRadar.sol new file mode 100644 index 0000000..bb53545 --- /dev/null +++ b/traps/unique_rugradar/contracts/UniqueRugRadar.sol @@ -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; + } +} diff --git a/traps/unique_rugradar/contracts/traps/unique_rugradar/trap.yml b/traps/unique_rugradar/contracts/traps/unique_rugradar/trap.yml new file mode 100644 index 0000000..4a80222 --- /dev/null +++ b/traps/unique_rugradar/contracts/traps/unique_rugradar/trap.yml @@ -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 diff --git a/traps/unique_rugradar/contracts/traps/unique_rugradar/traps/unique_rugradar/drosera.toml b/traps/unique_rugradar/contracts/traps/unique_rugradar/traps/unique_rugradar/drosera.toml new file mode 100644 index 0000000..eb3d836 --- /dev/null +++ b/traps/unique_rugradar/contracts/traps/unique_rugradar/traps/unique_rugradar/drosera.toml @@ -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