Development of Anti-Rug-Pull Protection System
Rug pull — when project team intentionally withdraws liquidity and/or ETH/tokens from contract, leaving holders with worthless tokens. Not technical bug — intentional. But technique enabling rug pull is concrete and predictable. Protection builds on two levels: on-chain mechanics preventing team from withdrawing liquidity, and off-chain monitoring alerting before execution.
Classification of rug pull vectors
Liquidity removal
Most common vector. Team adds liquidity to Uniswap/PancakeSwap, launches marketing, price rises → team withdraws liquidity. LP tokens owned by team, no restrictions.
Unrestricted mint
Team deploys token with mint(address, amount) accessible to owner. Anytime can mint billions and sell on market.
// DANGEROUS PATTERN
contract HoneypotToken is ERC20, Ownable {
function mint(address to, uint256 amount) external onlyOwner {
_mint(to, amount); // Unrestricted mint for owner
}
}
Hidden transfer restrictions
Contract allows only certain addresses to sell (sell restriction), blocking regular holders from selling. Owner can add/remove addresses from blacklist or whitelist.
Fee manipulation
Standard deflationary token with fee. Team can change fee to 99% — any sale returns almost everything to team.
uint256 public sellFee = 3; // 3% — seems reasonable
// Later:
function setSellFee(uint256 fee) external onlyOwner {
sellFee = fee; // Can become 99%
}
On-chain protective mechanics
Locked liquidity via Unicrypt / PinkLock
Simplest protection: LP tokens locked in smart contract with timelock. Team can't physically withdraw liquidity until expiry.
// Unicrypt Liquidity Locker interface
interface IUnicryptLocker {
function lockLPToken(
address lpToken,
uint256 amount,
uint256 unlockDate, // UNIX timestamp
address payable referral,
bool feeInEth,
address payable withdrawer
) external payable;
}
// On launch: lock LP for minimum 1 year
function lockInitialLiquidity(address lpToken, uint256 amount) external onlyOwner {
IERC20(lpToken).approve(address(unicryptLocker), amount);
unicryptLocker.lockLPToken{value: lockFee}(
lpToken,
amount,
block.timestamp + 365 days,
payable(address(0)),
true,
payable(msg.sender)
);
}
Check for users: on Unicrypt or Team.Finance can check lock by token or LP token address.
Renounced ownership and Ownable2Step
If team renounces ownership — no one can call onlyOwner functions. Maximum protection but makes contract immutable completely, including useful functions.
Compromise — limited owner via Ownable2Step with restricted powers:
contract ProtectedToken is ERC20, Ownable2Step {
// Owner can ONLY change marketing wallet and fee parameters
// within predefined limits
uint256 public constant MAX_SELL_FEE = 500; // max 5% — hardcoded, immutable
uint256 public sellFee = 300; // current 3%
function setSellFee(uint256 newFee) external onlyOwner {
require(newFee <= MAX_SELL_FEE, "Fee exceeds maximum");
sellFee = newFee;
}
// No mint function
// No blacklist/whitelist
// No transfer restrictions function
}
Mint cap and fixed supply
Limit maximum supply hardcoded — not via variable but via constant:
uint256 public constant MAX_SUPPLY = 1_000_000_000 * 1e18; // 1 billion, immutable
function mint(address to, uint256 amount) external onlyMinter {
require(totalSupply() + amount <= MAX_SUPPLY, "Exceeds max supply");
_mint(to, amount);
}
// After TGE — minter role revoked
function renounceMinterRole() external onlyOwner {
minter = address(0);
}
Timelock for critical functions
Any critical parameter changes must go through timelock — delay with public announcement:
contract TimelockProtectedToken is ERC20 {
uint256 public constant TIMELOCK_DURATION = 48 hours;
function proposeFeeChange(uint256 newFee) external onlyOwner {
require(newFee <= 500, "Too high");
bytes32 changeId = keccak256(abi.encodePacked("fee", newFee, block.timestamp));
pendingChanges[changeId] = PendingChange({
changeType: "fee",
newValue: newFee,
executableAt: block.timestamp + TIMELOCK_DURATION,
executed: false
});
emit FeeChangeProposed(changeId, newFee, block.timestamp + TIMELOCK_DURATION);
}
function executeFeeChange(bytes32 changeId) external onlyOwner {
PendingChange storage change = pendingChanges[changeId];
require(!change.executed, "Already executed");
require(block.timestamp >= change.executableAt, "Timelock not passed");
change.executed = true;
sellFee = change.newValue;
emit FeeChanged(change.newValue);
}
}
Timelock gives community 48 hours to see change and exit before it applies.
Off-chain monitoring: early detection
Contract analysis before purchase
Automatic scanner checks contract before interaction:
interface ContractRiskAssessment {
address: string;
risks: RiskItem[];
score: number; // 0 (safe) to 100 (high risk)
}
async function analyzeToken(tokenAddress: string): Promise<ContractRiskAssessment> {
const risks: RiskItem[] = [];
// 1. Check for mint function
const hasMintFunction = await checkFunctionExists(tokenAddress, 'mint(address,uint256)');
if (hasMintFunction) {
const mintRestricted = await checkMintHasSupplyCap(tokenAddress);
if (!mintRestricted) {
risks.push({
severity: 'critical',
type: 'UNLIMITED_MINT',
description: 'Owner can mint unlimited tokens'
});
}
}
// 2. Check owner
const owner = await getContractOwner(tokenAddress);
const ownerRenounced = owner === ZERO_ADDRESS;
if (!ownerRenounced) {
const ownerCapabilities = await getOwnerCapabilities(tokenAddress);
if (ownerCapabilities.canChangeTransferRestrictions) {
risks.push({
severity: 'high',
type: 'TRANSFER_RESTRICTION',
description: 'Owner can restrict token transfers (potential honeypot)'
});
}
}
// 3. LP lock status
const lpToken = await getUniswapLPToken(tokenAddress);
if (lpToken) {
const lockStatus = await checkLiquidityLock(lpToken);
if (!lockStatus.isLocked) {
risks.push({
severity: 'high',
type: 'UNLOCKED_LIQUIDITY',
description: 'Liquidity is not locked'
});
}
}
// 4. Proxy check
const isProxy = await isProxyContract(tokenAddress);
if (isProxy) {
const proxyAdmin = await getProxyAdmin(tokenAddress);
const proxyAdminIsTimelocked = await isTimelocked(proxyAdmin);
if (!proxyAdminIsTimelocked) {
risks.push({
severity: 'high',
type: 'UPGRADEABLE_NO_TIMELOCK',
description: 'Upgradeable contract without timelock on admin'
});
}
}
const score = calculateRiskScore(risks);
return { address: tokenAddress, risks, score };
}
Honeypot detection: simulation of sale
Best way to check honeypot — simulate sell transaction via fork:
async function simulateSell(
tokenAddress: string,
amount: bigint,
holderAddress: string
): Promise<{ canSell: boolean; receivedAmount: bigint; errorReason?: string }> {
// Fork mainnet locally
const anvil = await startAnvil({ forkUrl: MAINNET_RPC, forkBlockNumber: 'latest' });
try {
// Impersonate holder address
await anvil.impersonateAccount(holderAddress);
const router = getContract({ address: UNISWAP_V2_ROUTER, abi: ROUTER_ABI });
const token = getContract({ address: tokenAddress, abi: ERC20_ABI });
// Approve
await token.write.approve([UNISWAP_V2_ROUTER, amount], { account: holderAddress });
// Simulate sell
const ethBalanceBefore = await anvil.getBalance(holderAddress);
await router.write.swapExactTokensForETHSupportingFeeOnTransferTokens(
[amount, 0n, [tokenAddress, WETH], holderAddress, BigInt(Date.now()) + 1000n],
{ account: holderAddress }
);
const ethBalanceAfter = await anvil.getBalance(holderAddress);
const receivedETH = ethBalanceAfter - ethBalanceBefore;
return { canSell: true, receivedAmount: receivedETH };
} catch (error) {
return { canSell: false, receivedAmount: 0n, errorReason: error.message };
} finally {
await anvil.close();
}
}
This catches honeypot allowing transaction to pass but with 99% transfer fee.
Integration into product
System can be:
- Browser extension — warning on risky token approve or swap
- DEX frontend middleware — check before swap confirmation
- Portfolio tracker — monitor already purchased tokens
- Telegram/Discord bot — alerts for watchlist tokens
No automatic tool gives 100% protection — experienced scammer can create contract passing all checks. But raising effort barrier makes attacks less profitable and filters most opportunistic rug pulls.
Components and timeline
| Component | Description | Timeline |
|---|---|---|
| Contract analyzer | Static bytecode + ABI analysis | 3–4 weeks |
| Honeypot simulator | Anvil fork + sale simulation | 2–3 weeks |
| Real-time monitor | WebSocket event listener + alerts | 2–3 weeks |
| LP lock checker | Unicrypt, PinkLock, Team.Finance integration | 1–2 weeks |
| 3rd party integration | GoPlus, Token Sniffer API | 1 week |
| Frontend/bot | UI or Telegram bot for alerts | 2–4 weeks |
| Database | Check history, caching | 1–2 weeks |







