/**
* Security Module Tests
* Tests for token security analysis, rug pull detection, honeypot checks
* @author nich
* @github github.com/nirholas
* @license Apache-2.0
*/
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"
import type { Address, Hex } from "viem"
// Mock fetch for external security APIs (GoPlus, etc.)
const mockFetch = vi.fn()
global.fetch = mockFetch
// Mock the clients module
vi.mock("@/evm/services/clients.js", () => ({
getPublicClient: vi.fn(() => mockPublicClient),
getWalletClient: vi.fn(() => mockWalletClient)
}))
// Mock public client
const mockPublicClient = {
getChainId: vi.fn().mockResolvedValue(1),
getCode: vi.fn(),
readContract: vi.fn(),
getStorageAt: vi.fn(),
getBlock: vi.fn(),
getTransaction: vi.fn()
}
// Mock wallet client
const mockWalletClient = {
account: { address: "0x1234567890123456789012345678901234567890" as Address }
}
describe("Security Module", () => {
beforeEach(() => {
vi.clearAllMocks()
mockFetch.mockReset()
})
afterEach(() => {
vi.restoreAllMocks()
})
// Common test addresses
const safeTokenAddress = "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48" as Address // USDC
const riskyTokenAddress = "0xdead000000000000000000000000000000000000" as Address
const userWallet = "0x742d35Cc6634C0532925a3b844Bc9e7595f0Ab12" as Address
describe("Token Security Analysis", () => {
it("should detect safe token with renounced ownership", async () => {
mockPublicClient.getCode.mockResolvedValue("0x608060405234801561001057600080fd5b50")
mockPublicClient.readContract.mockResolvedValueOnce("0x40252CFDF8B20Ed757D61ff157719F33Ec332402") // owner = zero address
const code = await mockPublicClient.getCode({ address: safeTokenAddress })
expect(code).not.toBe("0x")
const owner = await mockPublicClient.readContract({
address: safeTokenAddress,
functionName: "owner"
})
expect(owner).toBe("0x40252CFDF8B20Ed757D61ff157719F33Ec332402")
})
it("should detect contract with active owner (centralization risk)", async () => {
const ownerAddress = "0xOwner1234567890123456789012345678901234" as Address
mockPublicClient.readContract.mockResolvedValueOnce(ownerAddress)
const owner = await mockPublicClient.readContract({
address: riskyTokenAddress,
functionName: "owner"
})
expect(owner).not.toBe("0x40252CFDF8B20Ed757D61ff157719F33Ec332402")
})
it("should detect pausable contracts", async () => {
mockPublicClient.readContract.mockResolvedValue(true) // paused = true
const paused = await mockPublicClient.readContract({
address: riskyTokenAddress,
functionName: "paused"
})
expect(paused).toBe(true)
})
it("should detect high tax tokens", async () => {
mockPublicClient.readContract.mockResolvedValue(BigInt(25)) // 25% tax
const taxFee = await mockPublicClient.readContract({
address: riskyTokenAddress,
functionName: "_taxFee"
})
expect(Number(taxFee)).toBeGreaterThan(10)
})
it("should identify no contract at address", async () => {
mockPublicClient.getCode.mockResolvedValue("0x")
const code = await mockPublicClient.getCode({
address: "0x0000000000000000000000000000000000000001" as Address
})
expect(code).toBe("0x")
})
})
describe("Rug Pull Detection", () => {
it("should identify rug pull risk indicators", () => {
const RUG_PULL_INDICATORS = {
hiddenMint: { risk: "critical", description: "Contract can mint unlimited tokens" },
hiddenOwner: { risk: "critical", description: "Hidden owner functions accessible" },
honeypot: { risk: "critical", description: "Buy allowed but sell blocked" },
highTax: { risk: "high", description: "Tax/fee exceeds 10%" },
pausable: { risk: "medium", description: "Trading can be paused" },
blacklist: { risk: "medium", description: "Addresses can be blacklisted" }
}
expect(RUG_PULL_INDICATORS.hiddenMint.risk).toBe("critical")
expect(RUG_PULL_INDICATORS.honeypot.risk).toBe("critical")
expect(RUG_PULL_INDICATORS.pausable.risk).toBe("medium")
})
it("should calculate risk score from multiple indicators", () => {
const risks = [
{ type: "centralization", severity: "medium" },
{ type: "high_tax", severity: "high" },
{ type: "pausable", severity: "medium" }
]
const riskScore = risks.reduce((score, risk) => {
if (risk.severity === "high") return score + 30
if (risk.severity === "medium") return score + 15
return score + 5
}, 0)
expect(riskScore).toBe(60) // 15 + 30 + 15
expect(riskScore >= 60 ? "high" : riskScore >= 30 ? "medium" : "low").toBe("high")
})
})
describe("Honeypot Detection", () => {
it("should detect honeypot via buy/sell fee disparity", async () => {
mockPublicClient.readContract
.mockResolvedValueOnce(BigInt(5)) // buyFee = 5%
.mockResolvedValueOnce(BigInt(99)) // sellFee = 99%
const buyFee = await mockPublicClient.readContract({
address: riskyTokenAddress,
functionName: "_buyFee"
})
const sellFee = await mockPublicClient.readContract({
address: riskyTokenAddress,
functionName: "_sellFee"
})
expect(Number(sellFee)).toBeGreaterThan(Number(buyFee) * 3)
})
it("should detect trading disabled", async () => {
mockPublicClient.readContract.mockResolvedValue(false) // tradingEnabled = false
const tradingEnabled = await mockPublicClient.readContract({
address: riskyTokenAddress,
functionName: "tradingEnabled"
})
expect(tradingEnabled).toBe(false)
})
})
describe("Approval Risk Analysis", () => {
it("should detect unlimited token approvals", async () => {
const MAX_UINT256 = BigInt("0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff")
mockPublicClient.readContract.mockResolvedValue(MAX_UINT256)
const allowance = await mockPublicClient.readContract({
address: safeTokenAddress,
functionName: "allowance",
args: [userWallet, "0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D" as Address]
})
const isUnlimited = allowance >= MAX_UINT256 / 2n
expect(isUnlimited).toBe(true)
})
it("should identify common spender contracts", () => {
const COMMON_SPENDERS: Record<string, Address> = {
"Uniswap V2": "0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D",
"Uniswap V3": "0xE592427A0AEce92De3Edee1F18E0157C05861564",
"1inch": "0x1111111254fb6c44bAC0beD2854e76F90643097d"
}
expect(COMMON_SPENDERS["Uniswap V2"]).toBe("0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D")
})
})
describe("GoPlus Security API Integration", () => {
it("should fetch token security data from GoPlus", async () => {
mockFetch.mockResolvedValueOnce({
ok: true,
json: () => Promise.resolve({
code: 1,
result: {
[safeTokenAddress.toLowerCase()]: {
is_honeypot: "0",
is_blacklisted: "0",
buy_tax: "0",
sell_tax: "0",
is_mintable: "0",
can_take_back_ownership: "0"
}
}
})
})
const response = await fetch(
`https://api.gopluslabs.io/api/v1/token_security/1?contract_addresses=${safeTokenAddress}`
)
const data = await response.json()
expect(data.code).toBe(1)
expect(data.result[safeTokenAddress.toLowerCase()].is_honeypot).toBe("0")
})
it("should handle GoPlus API rate limiting", async () => {
mockFetch.mockResolvedValueOnce({
ok: false,
status: 429,
json: () => Promise.resolve({ error: "Rate limit exceeded" })
})
const response = await fetch(
`https://api.gopluslabs.io/api/v1/token_security/1?contract_addresses=${riskyTokenAddress}`
)
expect(response.ok).toBe(false)
expect(response.status).toBe(429)
})
it("should identify risky token via GoPlus", async () => {
mockFetch.mockResolvedValueOnce({
ok: true,
json: () => Promise.resolve({
code: 1,
result: {
[riskyTokenAddress.toLowerCase()]: {
is_honeypot: "1",
is_blacklisted: "1",
buy_tax: "0.05",
sell_tax: "0.99",
is_mintable: "1",
can_take_back_ownership: "1"
}
}
})
})
const response = await fetch(
`https://api.gopluslabs.io/api/v1/token_security/1?contract_addresses=${riskyTokenAddress}`
)
const data = await response.json()
const tokenData = data.result[riskyTokenAddress.toLowerCase()]
expect(tokenData.is_honeypot).toBe("1")
expect(tokenData.is_mintable).toBe("1")
expect(Number(tokenData.sell_tax)).toBeGreaterThan(0.5)
})
})
describe("Liquidity Lock Verification", () => {
it("should verify liquidity lock on known lockers", () => {
const LIQUIDITY_LOCKERS: Record<string, Address> = {
"Unicrypt": "0x663A5C229c09b049E36dCc11a9B0d4a8Eb9db214",
"Team.Finance": "0xE2fE530C047f2d85298b07D9333C05737f1435fB",
"PinkLock": "0x71B5759d73262FBb223956913ecF4ecC51057641"
}
expect(LIQUIDITY_LOCKERS["Unicrypt"]).toBeDefined()
expect(LIQUIDITY_LOCKERS["PinkLock"]).toBeDefined()
})
it("should check LP token lock status", async () => {
mockPublicClient.readContract.mockResolvedValue(BigInt(1000000))
const lockedAmount = await mockPublicClient.readContract({
address: "0x663A5C229c09b049E36dCc11a9B0d4a8Eb9db214" as Address, // Unicrypt
functionName: "getUserNumLockedTokens",
args: ["0xDeployer12345678901234567890123456789012" as Address]
})
expect(Number(lockedAmount)).toBeGreaterThan(0)
})
})
describe("Contract Bytecode Analysis", () => {
it("should identify proxy contracts", async () => {
const IMPLEMENTATION_SLOT = "0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc"
mockPublicClient.getStorageAt.mockResolvedValue(
"0x000000000000000000000000abcdef1234567890abcdef1234567890abcdef12"
)
const implementationSlot = await mockPublicClient.getStorageAt({
address: riskyTokenAddress,
slot: IMPLEMENTATION_SLOT as Hex
})
expect(implementationSlot).not.toBe("0x40252CFDF8B20Ed757D61ff157719F33Ec332402000000000000000000000000")
})
it("should check contract code size", async () => {
mockPublicClient.getCode.mockResolvedValue("0x" + "60".repeat(5000)) // ~5KB contract
const code = await mockPublicClient.getCode({ address: safeTokenAddress })
const codeSize = (code.length - 2) / 2 // Remove 0x prefix, each byte is 2 hex chars
expect(codeSize).toBe(5000)
})
})
describe("Blacklist Detection", () => {
it("should check if address is blacklisted", async () => {
mockPublicClient.readContract.mockResolvedValue(true)
const isBlacklisted = await mockPublicClient.readContract({
address: riskyTokenAddress,
functionName: "isBlacklisted",
args: [userWallet]
})
expect(isBlacklisted).toBe(true)
})
it("should handle contracts without blacklist functionality", async () => {
mockPublicClient.readContract.mockRejectedValue(
new Error("Function not found")
)
await expect(
mockPublicClient.readContract({
address: safeTokenAddress,
functionName: "isBlacklisted",
args: [userWallet]
})
).rejects.toThrow()
})
})
describe("Edge Cases", () => {
it("should handle network errors gracefully", async () => {
mockPublicClient.getCode.mockRejectedValue(
new Error("RPC request failed")
)
await expect(
mockPublicClient.getCode({ address: safeTokenAddress })
).rejects.toThrow("RPC request failed")
})
it("should handle invalid addresses", async () => {
const invalidAddress = "0xinvalid"
// Type system should catch this, but runtime might throw
expect(() => {
// Address validation would happen at viem level
if (!invalidAddress.match(/^0x[a-fA-F0-9]{40}$/)) {
throw new Error("Invalid address format")
}
}).toThrow("Invalid address format")
})
it("should handle self-destructed contracts", async () => {
mockPublicClient.getCode.mockResolvedValue("0x")
const code = await mockPublicClient.getCode({ address: riskyTokenAddress })
expect(code).toBe("0x")
})
it("should calculate risk level correctly for edge scores", () => {
const calculateRiskLevel = (score: number) => {
if (score >= 60) return "high"
if (score >= 30) return "medium"
return "low"
}
expect(calculateRiskLevel(0)).toBe("low")
expect(calculateRiskLevel(29)).toBe("low")
expect(calculateRiskLevel(30)).toBe("medium")
expect(calculateRiskLevel(59)).toBe("medium")
expect(calculateRiskLevel(60)).toBe("high")
expect(calculateRiskLevel(100)).toBe("high")
})
})
})