├── asset-drained ├── .env ├── .dockerignore ├── .gitignore ├── .prettierrc ├── src │ ├── address-type.js │ ├── storage.js │ └── agent-performance.spec.js ├── Dockerfile ├── package.json ├── README.md └── LICENSE ├── ice-phishing ├── .env ├── .dockerignore ├── .gitignore ├── forta.config.json ├── .prettierrc ├── src │ ├── errorCache.js │ ├── address-type.js │ ├── storage.js │ ├── config.js │ ├── persistence.helper.js │ └── persistence.helper.spec.js ├── bot-config.json ├── Dockerfile ├── bot-config.json.example ├── package.json └── LICENSE ├── README.md ├── flashloan-detector ├── .env ├── .dockerignore ├── .gitignore ├── .prettierrc ├── src │ ├── detectors │ │ ├── maker-detector.js │ │ ├── balancer-detector.js │ │ ├── aave-v2-detector.js │ │ ├── aave-v3-detector.js │ │ ├── iron-bank-detector.js │ │ ├── dodo-detector.js │ │ ├── uniswap-v3-detector.js │ │ ├── euler-detector.js │ │ ├── dydx-detector.js │ │ └── uniswap-v2-detector.js │ ├── storage.js │ ├── flashloan-detector.js │ ├── persistence.helper.js │ ├── helper.spec.js │ ├── flashloan-detector.spec.js │ └── persistence.helper.spec.js ├── Dockerfile ├── package.json └── README.md ├── large-balance-decrease ├── .env ├── .dockerignore ├── .gitignore ├── .prettierrc ├── bot-config.json.example ├── Dockerfile ├── SETUP.md ├── package.json ├── src │ ├── persistence.helper.js │ └── persistence.helper.spec.js └── README.md ├── flashbots-transactions-detector ├── .env ├── .dockerignore ├── .gitignore ├── .prettierrc ├── Dockerfile ├── src │ ├── storage.js │ ├── persistence.helper.js │ └── persistence.helper.spec.js ├── package.json └── README.md ├── forta-tornado-cash-starter-kit ├── .env ├── .dockerignore ├── .gitignore ├── .prettierrc ├── Dockerfile ├── src │ ├── storage.js │ ├── helper.js │ └── agent.js ├── package.json └── README.md ├── .gitignore ├── gov-sneak-proposal ├── .dockerignore ├── .gitignore ├── src │ ├── agent.config.js │ └── bot-config.example.json ├── Dockerfile ├── README.md └── package.json ├── gov-voting-power-change ├── .dockerignore ├── .gitignore ├── Dockerfile ├── bot-config.example.json ├── src │ └── agent.config.js ├── README.md └── package.json ├── malicious-gov-proposal ├── .dockerignore ├── .gitignore ├── .eslintrc.js ├── Dockerfile ├── bot-config.json.comp.example ├── README.md ├── SETUP.md ├── bot-config.json.aragon.example ├── package.json └── src │ └── agent.js ├── bridge-balance-difference ├── .dockerignore ├── .gitignore ├── .eslintrc.js ├── Dockerfile ├── bot-config.json.example ├── README.md ├── package.json └── src │ ├── agent.js │ └── agent.spec.js ├── drastic-price-change-template ├── .dockerignore ├── .gitignore ├── .eslintrc.js ├── Dockerfile ├── bot-config.json.example ├── README.md ├── SETUP.md ├── package.json └── src │ └── helper.js ├── large-mint-borrow-anomaly-detection ├── .dockerignore ├── .gitignore ├── Dockerfile ├── README.md ├── package.json └── src │ └── agent.config.js ├── package.json ├── transaction-volume-anomaly-detection ├── .dockerignore ├── .gitignore ├── Dockerfile ├── package.json └── README.md ├── transaction-volume-anomaly-detection-template ├── .dockerignore ├── .gitignore ├── src │ ├── bot.config.example.json │ └── agent.config.js ├── Dockerfile ├── package.json └── README.md └── LICENSE /asset-drained/.env: -------------------------------------------------------------------------------- 1 | LOCAL_NODE=1 -------------------------------------------------------------------------------- /ice-phishing/.env: -------------------------------------------------------------------------------- 1 | LOCAL_NODE=1 -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # forta-starter-kits 2 | -------------------------------------------------------------------------------- /flashloan-detector/.env: -------------------------------------------------------------------------------- 1 | LOCAL_NODE=1 -------------------------------------------------------------------------------- /large-balance-decrease/.env: -------------------------------------------------------------------------------- 1 | LOCAL_NODE=1 -------------------------------------------------------------------------------- /flashbots-transactions-detector/.env: -------------------------------------------------------------------------------- 1 | LOCAL_NODE=1 -------------------------------------------------------------------------------- /forta-tornado-cash-starter-kit/.env: -------------------------------------------------------------------------------- 1 | LOCAL_NODE=1 -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | *.log 3 | node_modules 4 | coverage 5 | -------------------------------------------------------------------------------- /asset-drained/.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | forta.config.json -------------------------------------------------------------------------------- /gov-sneak-proposal/.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | forta.config.json -------------------------------------------------------------------------------- /ice-phishing/.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | forta.config.json 4 | .env -------------------------------------------------------------------------------- /gov-voting-power-change/.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | forta.config.json -------------------------------------------------------------------------------- /malicious-gov-proposal/.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | forta.config.json -------------------------------------------------------------------------------- /bridge-balance-difference/.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | forta.config.json 4 | -------------------------------------------------------------------------------- /flashbots-transactions-detector/.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | forta.config.json -------------------------------------------------------------------------------- /flashbots-transactions-detector/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | forta.config.json -------------------------------------------------------------------------------- /flashloan-detector/.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | forta.config.json 4 | .env -------------------------------------------------------------------------------- /forta-tornado-cash-starter-kit/.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | forta.config.json -------------------------------------------------------------------------------- /forta-tornado-cash-starter-kit/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | forta.config.json -------------------------------------------------------------------------------- /large-balance-decrease/.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | forta.config.json 4 | -------------------------------------------------------------------------------- /drastic-price-change-template/.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | forta.config.json 4 | -------------------------------------------------------------------------------- /large-mint-borrow-anomaly-detection/.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | forta.config.json -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "@types/jest": "^27.4.1" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /transaction-volume-anomaly-detection/.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | forta.config.json -------------------------------------------------------------------------------- /asset-drained/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | forta.config.json 4 | coverage 5 | secrets.json -------------------------------------------------------------------------------- /ice-phishing/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | forta.config.json 4 | bot-config.json 5 | secrets.json -------------------------------------------------------------------------------- /large-mint-borrow-anomaly-detection/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | forta.config.json 4 | coverage -------------------------------------------------------------------------------- /malicious-gov-proposal/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | forta.config.json 4 | bot-config.json 5 | -------------------------------------------------------------------------------- /transaction-volume-anomaly-detection-template/.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | forta.config.json -------------------------------------------------------------------------------- /transaction-volume-anomaly-detection/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | forta.config.json 4 | coverage -------------------------------------------------------------------------------- /bridge-balance-difference/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | forta.config.json 4 | bot-config.json 5 | -------------------------------------------------------------------------------- /drastic-price-change-template/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | forta.config.json 4 | bot-config.json 5 | -------------------------------------------------------------------------------- /flashloan-detector/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | *.log 4 | forta.config.json 5 | traces*.json 6 | -------------------------------------------------------------------------------- /gov-sneak-proposal/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | forta.config.json 4 | coverage 5 | bot-config.json -------------------------------------------------------------------------------- /ice-phishing/forta.config.json: -------------------------------------------------------------------------------- 1 | {"agentId":"0x8badbf2ad65abc3df5b1d9cc388e419d9255ef999fb69aac6bf395646cf01c14"} -------------------------------------------------------------------------------- /ice-phishing/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 120, 3 | "trailingComma": "es5", 4 | "singleQuote": false 5 | } 6 | -------------------------------------------------------------------------------- /large-balance-decrease/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | forta.config.json 4 | bot-config.json 5 | traces.json 6 | -------------------------------------------------------------------------------- /asset-drained/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 120, 3 | "trailingComma": "es5", 4 | "singleQuote": false 5 | } -------------------------------------------------------------------------------- /flashloan-detector/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 120, 3 | "trailingComma": "es5", 4 | "singleQuote": false 5 | } -------------------------------------------------------------------------------- /gov-voting-power-change/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | forta.config.json 4 | coverage 5 | bot-config.json 6 | publish.log -------------------------------------------------------------------------------- /large-balance-decrease/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 120, 3 | "trailingComma": "es5", 4 | "singleQuote": false 5 | } 6 | -------------------------------------------------------------------------------- /asset-drained/src/address-type.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | Eoa: "Eoa", 3 | Contract: "Contract", 4 | Ignored: "Ignored", 5 | }; 6 | -------------------------------------------------------------------------------- /flashbots-transactions-detector/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 120, 3 | "trailingComma": "es5", 4 | "singleQuote": false 5 | } 6 | -------------------------------------------------------------------------------- /forta-tornado-cash-starter-kit/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 120, 3 | "trailingComma": "es5", 4 | "singleQuote": false 5 | } -------------------------------------------------------------------------------- /transaction-volume-anomaly-detection-template/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | forta.config.json 4 | coverage 5 | bot.config.json 6 | publish.log -------------------------------------------------------------------------------- /large-balance-decrease/bot-config.json.example: -------------------------------------------------------------------------------- 1 | { 2 | "aggregationTimePeriod": 604800, 3 | "contractAddress": "0xd7A029Db2585553978190dB5E85eC724Aa4dF23f" 4 | } 5 | -------------------------------------------------------------------------------- /gov-sneak-proposal/src/agent.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | accountThreshold: 2, 3 | voteDuration: 0, 4 | maxTracked: 100_000, 5 | preExecutionThreshold: 20, //The value here is how much of the whole we should take in this case 1/20 which is equal to 5% 6 | }; 7 | -------------------------------------------------------------------------------- /bridge-balance-difference/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | commonjs: true, 4 | es2021: true, 5 | node: true, 6 | jest: true, 7 | }, 8 | extends: 'airbnb-base', 9 | parserOptions: { 10 | ecmaVersion: 'latest', 11 | }, 12 | rules: { 13 | }, 14 | }; 15 | -------------------------------------------------------------------------------- /malicious-gov-proposal/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | commonjs: true, 4 | es2021: true, 5 | node: true, 6 | jest: true, 7 | }, 8 | extends: 'airbnb-base', 9 | parserOptions: { 10 | ecmaVersion: 'latest', 11 | }, 12 | rules: { 13 | }, 14 | }; 15 | -------------------------------------------------------------------------------- /drastic-price-change-template/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | commonjs: true, 4 | es2021: true, 5 | node: true, 6 | jest: true, 7 | }, 8 | extends: 'airbnb-base', 9 | parserOptions: { 10 | ecmaVersion: 'latest', 11 | }, 12 | rules: { 13 | }, 14 | }; 15 | -------------------------------------------------------------------------------- /gov-sneak-proposal/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM --platform=linux/amd64 node:16-alpine 2 | ENV NODE_ENV=production 3 | # Uncomment the following line to enable agent logging 4 | LABEL "network.forta.settings.agent-logs.enable"="true" 5 | WORKDIR /app 6 | COPY ./src ./src 7 | COPY package*.json ./ 8 | RUN npm ci --production 9 | CMD [ "npm", "run", "start:prod" ] -------------------------------------------------------------------------------- /flashbots-transactions-detector/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:18-alpine3.17 AS builder 2 | ENV NODE_ENV=production 3 | # Uncomment the following line to enable agent logging 4 | LABEL "network.forta.settings.agent-logs.enable"="true" 5 | WORKDIR /app 6 | COPY ./src ./src 7 | COPY package*.json ./ 8 | RUN npm ci --production 9 | CMD [ "npm", "run", "start:prod" ] -------------------------------------------------------------------------------- /asset-drained/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:18-alpine3.17 AS builder 2 | ENV NODE_ENV=production 3 | # Uncomment the following line to enable agent logging 4 | LABEL "network.forta.settings.agent-logs.enable"="true" 5 | WORKDIR /app 6 | COPY ./src ./src 7 | COPY ./LICENSE ./ 8 | COPY package*.json ./ 9 | RUN npm ci --production 10 | CMD [ "npm", "run", "start:prod" ] -------------------------------------------------------------------------------- /malicious-gov-proposal/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:16-alpine 2 | ENV NODE_ENV=production 3 | # Uncomment the following line to enable agent logging 4 | LABEL "network.forta.settings.agent-logs.enable"="true" 5 | WORKDIR /app 6 | COPY ./src ./src 7 | COPY package*.json ./ 8 | COPY bot-config.json ./ 9 | RUN npm ci --production 10 | CMD [ "npm", "run", "start:prod" ] -------------------------------------------------------------------------------- /bridge-balance-difference/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:16-alpine 2 | ENV NODE_ENV=production 3 | # Uncomment the following line to enable agent logging 4 | LABEL "network.forta.settings.agent-logs.enable"="true" 5 | WORKDIR /app 6 | COPY ./src ./src 7 | COPY package*.json ./ 8 | COPY bot-config.json ./ 9 | RUN npm ci --production 10 | CMD [ "npm", "run", "start:prod" ] 11 | -------------------------------------------------------------------------------- /drastic-price-change-template/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:16-alpine 2 | ENV NODE_ENV=production 3 | # Uncomment the following line to enable agent logging 4 | LABEL "network.forta.settings.agent-logs.enable"="true" 5 | WORKDIR /app 6 | COPY ./src ./src 7 | COPY package*.json ./ 8 | COPY bot-config.json ./ 9 | RUN npm ci --production 10 | CMD [ "npm", "run", "start:prod" ] 11 | -------------------------------------------------------------------------------- /drastic-price-change-template/bot-config.json.example: -------------------------------------------------------------------------------- 1 | { 2 | "priceDiscrepancyThreshold": 10, 3 | "asset": { 4 | "contract": "0x1f9840a85d5af5bf1d1762f925bdaddc4201f984", 5 | "coingeckoId": "uniswap", 6 | "chainlinkFeedAddress": "0x553303d460ee0afb37edff9be42922d8ff63220e" 7 | "uniswapV3Pool": "0xd0fc8ba7e267f2bc56044a7715a489d851dc6d78" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /gov-voting-power-change/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM --platform=linux/amd64 node:16-alpine 2 | ENV NODE_ENV=production 3 | # Uncomment the following line to enable agent logging 4 | LABEL "network.forta.settings.agent-logs.enable"="true" 5 | WORKDIR /app 6 | COPY ./src ./src 7 | COPY ./bot-config.json ./ 8 | COPY package*.json ./ 9 | RUN npm ci --production 10 | CMD [ "npm", "run", "start:prod" ] -------------------------------------------------------------------------------- /large-balance-decrease/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:16-alpine 2 | ENV NODE_ENV=production 3 | # Uncomment the following line to enable agent logging 4 | LABEL "network.forta.settings.agent-logs.enable"="true" 5 | WORKDIR /app 6 | COPY ./src ./src 7 | COPY ./LICENSE ./ 8 | COPY package*.json ./ 9 | COPY bot-config.json ./ 10 | RUN npm ci --production 11 | CMD [ "npm", "run", "start:prod" ] 12 | -------------------------------------------------------------------------------- /ice-phishing/src/errorCache.js: -------------------------------------------------------------------------------- 1 | const errorCache = { 2 | errors: [], 3 | add: function (error) { 4 | this.errors.push(error); 5 | }, 6 | getAll: function () { 7 | return this.errors; 8 | }, 9 | clear: function () { 10 | this.errors = []; 11 | }, 12 | len: function () { 13 | return this.errors.length; 14 | }, 15 | }; 16 | 17 | module.exports = errorCache; 18 | -------------------------------------------------------------------------------- /ice-phishing/bot-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "actionThreshold": 2, 3 | "timePeriodDays": 30, 4 | "nonceThreshold": 100, 5 | "contractTxsThreshold": 4999, 6 | "verifiedContractTxsThreshold": 1999, 7 | "approveCountThreshold": 3, 8 | "approveForAllCountThreshold": 1, 9 | "pigButcheringTransferCountThreshold": 3, 10 | "transferCountThreshold": 2, 11 | "maxAddressAlertsPerPeriod": 3 12 | } 13 | -------------------------------------------------------------------------------- /transaction-volume-anomaly-detection-template/src/bot.config.example.json: -------------------------------------------------------------------------------- 1 | { 2 | "developerAbbreviation": "", 3 | "protocolName": "", 4 | "protocolAbbreviation": "", 5 | "bots": [ 6 | { 7 | "botType": "transaction_volume", 8 | "name": "", 9 | "contracts": { 10 | "": { 11 | "address": "", 12 | "chainId": 1 13 | } 14 | } 15 | } 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /ice-phishing/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:18-alpine3.17 AS builder 2 | ENV NODE_OPTIONS=--max_old_space_size=4096 3 | ENV NODE_ENV=production 4 | # Uncomment the following line to enable agent logging 5 | LABEL "network.forta.settings.agent-logs.enable"="true" 6 | WORKDIR /app 7 | COPY ./src ./src 8 | COPY ./LICENSE ./ 9 | COPY package*.json ./ 10 | COPY bot-config.json ./ 11 | RUN npm ci --production 12 | CMD [ "npm", "run", "start:prod" ] -------------------------------------------------------------------------------- /gov-voting-power-change/bot-config.example.json: -------------------------------------------------------------------------------- 1 | { 2 | "developerAbbreviation": "", 3 | "protocolName": "lido", 4 | "protocolAbbreviation": "", 5 | "gatherMode": "any", 6 | "bots": [ 7 | { 8 | "botType": "governance", 9 | "name": "", 10 | "contracts": { 11 | "protocol_name": { 12 | "address": "0x2e59A20f205bB85a89C53f1936454680651E618e", 13 | "abiFile": "protocol.json" 14 | } 15 | } 16 | } 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /ice-phishing/src/address-type.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | EoaWithLowNonce: "EoaWithLowNonce", 3 | EoaWithHighNonce: "EoaWithHighNonce", 4 | LowNumTxsUnverifiedContract: "LowNumTxsUnverifiedContract", 5 | HighNumTxsUnverifiedContract: "HighNumTxsUnverifiedContract", 6 | LowNumTxsVerifiedContract: "LowNumTxsVerifiedContract", 7 | HighNumTxsVerifiedContract: "HighNumTxsVerifiedContract", 8 | IgnoredEoa: "IgnoredEoa", 9 | IgnoredContract: "IgnoredContract", 10 | ScamAddress: "ScamAddress", 11 | }; 12 | -------------------------------------------------------------------------------- /bridge-balance-difference/bot-config.json.example: -------------------------------------------------------------------------------- 1 | { 2 | "contract1": { 3 | "address": "0x7ea2be2df7ba6e54b1a9c70676f668455e329d29", 4 | "rpcUrl": "https://cloudflare-eth.com" 5 | }, 6 | "contract2": { 7 | "address": "0xcc9b1f919282c255eb9ad2c0757e8036165e0cad", 8 | "rpcUrl": "https://api.avax.network/ext/bc/C/rpc" 9 | }, 10 | "tokens": [ 11 | { 12 | "address1": "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", 13 | "address2": "0xa7d7079b0fead91f3e65f86e8915cb59c1a4c664" 14 | } 15 | ] 16 | } -------------------------------------------------------------------------------- /flashloan-detector/src/detectors/maker-detector.js: -------------------------------------------------------------------------------- 1 | const makerFlashloanSig = "event FlashLoan(address indexed receiver, address token, uint256 amount, uint256 fee)"; 2 | 3 | module.exports = { 4 | getMakerFlashloan: (txEvent) => { 5 | const flashloans = []; 6 | const events = txEvent.filterLog(makerFlashloanSig); 7 | 8 | events.forEach((event) => { 9 | const { token, amount, receiver } = event.args; 10 | flashloans.push({ 11 | asset: token.toLowerCase(), 12 | amount, 13 | account: receiver.toLowerCase(), 14 | }); 15 | }); 16 | return flashloans; 17 | }, 18 | }; 19 | -------------------------------------------------------------------------------- /bridge-balance-difference/README.md: -------------------------------------------------------------------------------- 1 | # Bridge Balance Difference Bot 2 | 3 | ## Description 4 | 5 | Detects if two sides of the bridge show significant balance difference 6 | 7 | ## Supported Chains 8 | 9 | - Ethereum 10 | - Optimism 11 | - Binance Smart Chain 12 | - Polygon 13 | - Fantom 14 | - Arbitrum 15 | - Avalanche 16 | 17 | ## Alerts 18 | 19 | - BRIDGE-BALANCE-DIFFERENCE 20 | - Fired when two sides of the bridge show significant balance difference 21 | - Severity is always set to "high" 22 | - Type is always set to "suspicious" 23 | 24 | ## Test Data 25 | 26 | The bot behaviour can verified with the provided unit tests 27 | -------------------------------------------------------------------------------- /flashloan-detector/src/detectors/balancer-detector.js: -------------------------------------------------------------------------------- 1 | const balancerFlashloanSig = 2 | "event FlashLoan(address indexed receiver, address indexed token, uint256 amount, uint256 feeAmount)"; 3 | 4 | module.exports = { 5 | getBalancerFlashloan: (txEvent) => { 6 | const flashloans = []; 7 | const events = txEvent.filterLog(balancerFlashloanSig); 8 | 9 | events.forEach((event) => { 10 | const { token, amount, receiver } = event.args; 11 | flashloans.push({ 12 | asset: token.toLowerCase(), 13 | amount, 14 | account: receiver.toLowerCase(), 15 | }); 16 | }); 17 | return flashloans; 18 | }, 19 | }; 20 | -------------------------------------------------------------------------------- /flashloan-detector/src/detectors/aave-v2-detector.js: -------------------------------------------------------------------------------- 1 | const aaveV2FlashloanSig = 2 | "event FlashLoan(address indexed target, address indexed initiator, address indexed asset, uint256 amount, uint256 premium, uint16 referralCode)"; 3 | 4 | module.exports = { 5 | getAaveV2Flashloan: (txEvent) => { 6 | const flashloans = []; 7 | const events = txEvent.filterLog(aaveV2FlashloanSig); 8 | 9 | events.forEach((event) => { 10 | const { asset, amount, target } = event.args; 11 | flashloans.push({ 12 | asset: asset.toLowerCase(), 13 | amount, 14 | account: target.toLowerCase(), 15 | }); 16 | }); 17 | return flashloans; 18 | }, 19 | }; 20 | -------------------------------------------------------------------------------- /flashloan-detector/src/detectors/aave-v3-detector.js: -------------------------------------------------------------------------------- 1 | const aaveV3FlashloanSig = 2 | "event FlashLoan(address indexed target, address initiator, address indexed asset, uint256 amount, uint8 interestRateMode, uint256 premium, uint16 indexed referralCode)"; 3 | 4 | module.exports = { 5 | getAaveV3Flashloan: (txEvent) => { 6 | const flashloans = []; 7 | const events = txEvent.filterLog(aaveV3FlashloanSig); 8 | 9 | events.forEach((event) => { 10 | const { asset, amount, target } = event.args; 11 | flashloans.push({ 12 | asset: asset.toLowerCase(), 13 | amount, 14 | account: target.toLowerCase(), 15 | }); 16 | }); 17 | return flashloans; 18 | }, 19 | }; 20 | -------------------------------------------------------------------------------- /flashloan-detector/Dockerfile: -------------------------------------------------------------------------------- 1 | # Build stage: obfuscate Javascript (optional) 2 | # FROM node:14.15.5-alpine as builder 3 | # WORKDIR /app 4 | # COPY . . 5 | # RUN npm install -g javascript-obfuscator 6 | # RUN javascript-obfuscator ./src --output ./dist --split-strings true --split-strings-chunk-length 3 7 | 8 | # Final stage: install production dependencies 9 | FROM node:18-alpine3.17 AS builder 10 | ENV NODE_ENV=production 11 | # Uncomment the following line to enable agent logging 12 | LABEL "network.forta.settings.agent-logs.enable"="true" 13 | WORKDIR /app 14 | # if using obfuscated code from build stage: 15 | # COPY --from=builder /app/dist ./src 16 | # else if using unobfuscated code: 17 | COPY ./src ./src 18 | COPY package*.json ./ 19 | RUN npm ci --production 20 | CMD [ "npm", "run", "start:prod" ] 21 | -------------------------------------------------------------------------------- /large-mint-borrow-anomaly-detection/Dockerfile: -------------------------------------------------------------------------------- 1 | # Build stage: obfuscate Javascript (optional) 2 | # FROM node:14.15.5-alpine as builder 3 | # WORKDIR /app 4 | # COPY . . 5 | # RUN npm install -g javascript-obfuscator 6 | # RUN javascript-obfuscator ./src --output ./dist --split-strings true --split-strings-chunk-length 3 7 | 8 | # Final stage: install production dependencies 9 | FROM node:12-alpine 10 | ENV NODE_ENV=production 11 | # Uncomment the following line to enable agent logging 12 | LABEL "network.forta.settings.agent-logs.enable"="true" 13 | WORKDIR /app 14 | # if using obfuscated code from build stage: 15 | # COPY --from=builder /app/dist ./src 16 | # else if using unobfuscated code: 17 | COPY ./src ./src 18 | COPY package*.json ./ 19 | RUN npm ci --production 20 | CMD [ "npm", "run", "start:prod" ] -------------------------------------------------------------------------------- /malicious-gov-proposal/bot-config.json.comp.example: -------------------------------------------------------------------------------- 1 | { 2 | "address": "0x408ED6354d4973f66138C91495F2f2FCbd8724C3", 3 | "type": "comp", 4 | "parameters": [ 5 | { 6 | "signature": "enableFeeAmount(uint24 fee, int24 tickSpacing)", 7 | "thresholds": [ 8 | { 9 | "name": "fee", 10 | "min": 40, 11 | "max": 50 12 | }, 13 | { 14 | "name": "tickSpacing", 15 | "min": 10, 16 | "max": 100 17 | } 18 | ] 19 | }, 20 | { 21 | "signature": "transfer(address recipient, uint256 amount)", 22 | "thresholds": [ 23 | { 24 | "name": "amount", 25 | "min": -1, 26 | "max": 100, 27 | "decimals": 18 28 | } 29 | ] 30 | } 31 | ] 32 | } -------------------------------------------------------------------------------- /transaction-volume-anomaly-detection/Dockerfile: -------------------------------------------------------------------------------- 1 | # Build stage: obfuscate Javascript (optional) 2 | # FROM node:14.15.5-alpine as builder 3 | # WORKDIR /app 4 | # COPY . . 5 | # RUN npm install -g javascript-obfuscator 6 | # RUN javascript-obfuscator ./src --output ./dist --split-strings true --split-strings-chunk-length 3 7 | 8 | # Final stage: install production dependencies 9 | FROM node:12-alpine 10 | ENV NODE_ENV=production 11 | # Uncomment the following line to enable agent logging 12 | LABEL "network.forta.settings.agent-logs.enable"="true" 13 | WORKDIR /app 14 | # if using obfuscated code from build stage: 15 | # COPY --from=builder /app/dist ./src 16 | # else if using unobfuscated code: 17 | COPY ./src ./src 18 | COPY package*.json ./ 19 | RUN npm ci --production 20 | CMD [ "npm", "run", "start:prod" ] -------------------------------------------------------------------------------- /transaction-volume-anomaly-detection-template/Dockerfile: -------------------------------------------------------------------------------- 1 | # Build stage: obfuscate Javascript (optional) 2 | # FROM node:14.15.5-alpine as builder 3 | # WORKDIR /app 4 | # COPY . . 5 | # RUN npm install -g javascript-obfuscator 6 | # RUN javascript-obfuscator ./src --output ./dist --split-strings true --split-strings-chunk-length 3 7 | 8 | # Final stage: install production dependencies 9 | FROM node:12-alpine 10 | ENV NODE_ENV=production 11 | # Uncomment the following line to enable agent logging 12 | # LABEL "network.forta.settings.agent-logs.enable"="true" 13 | WORKDIR /app 14 | # if using obfuscated code from build stage: 15 | # COPY --from=builder /app/dist ./src 16 | # else if using unobfuscated code: 17 | COPY ./src ./src 18 | COPY package*.json ./ 19 | RUN npm ci --production 20 | CMD [ "npm", "run", "start:prod" ] -------------------------------------------------------------------------------- /forta-tornado-cash-starter-kit/Dockerfile: -------------------------------------------------------------------------------- 1 | # Build stage: obfuscate Javascript (optional) 2 | # FROM node:14.15.5-alpine as builder 3 | # WORKDIR /app 4 | # COPY . . 5 | # RUN npm install -g javascript-obfuscator 6 | # RUN javascript-obfuscator ./src --output ./dist --split-strings true --split-strings-chunk-length 3 7 | 8 | # Final stage: install production dependencies 9 | FROM node:18-alpine3.17 AS builder 10 | ENV NODE_ENV=production 11 | # Uncomment the following line to enable agent logging 12 | LABEL "network.forta.settings.agent-logs.enable"="true" 13 | WORKDIR /app 14 | # if using obfuscated code from build stage: 15 | # COPY --from=builder /app/dist ./src 16 | # else if using unobfuscated code: 17 | COPY ./src ./src 18 | COPY ./LICENSE ./ 19 | COPY package*.json ./ 20 | RUN npm ci --production 21 | CMD [ "npm", "run", "start:prod" ] -------------------------------------------------------------------------------- /gov-sneak-proposal/src/bot-config.example.json: -------------------------------------------------------------------------------- 1 | { 2 | "developerAbbreviation": "", 3 | "protocolName": "lido", 4 | "protocolAbbreviation": "lido", 5 | "gatherMode": "any", 6 | "bots": [ 7 | { 8 | "botType": "governance", 9 | "name": "governance_sneak_proposal", 10 | "contracts": { 11 | "lido": { 12 | "address": "0x2e59A20f205bB85a89C53f1936454680651E618e", 13 | "minAcceptanceQuorumPCT": "50000000000000000", 14 | "PCT_BASE": "1000000000000000000", 15 | "eventABIs": [ 16 | "event StartVote(uint256 indexed voteId, address indexed creator, string metadata)", 17 | "event CastVote(uint256 indexed voteId, address indexed voter, bool supports, uint256 stake)", 18 | "event ExecuteVote(uint256 indexed voteId)" 19 | ] 20 | } 21 | } 22 | } 23 | ] 24 | } 25 | -------------------------------------------------------------------------------- /drastic-price-change-template/README.md: -------------------------------------------------------------------------------- 1 | # Drastic Price Change Anomaly Detection Bot 2 | 3 | ## Description 4 | 5 | This bot detects if the price of an asset changes drastically or there is a large discrepancy between an on-chain and an off-chain oracle 6 | 7 | ## Supported Chains 8 | 9 | - Ethereum 10 | - Optimism 11 | - Binance Smart Chain 12 | - Polygon 13 | - Fantom 14 | - Arbitrum 15 | - Avalanche 16 | 17 | ## Alerts 18 | 19 | - PRICE-DISCREPANCIES 20 | - Fired when there is a large discrepancy between an on-chain and an off-chain oracle 21 | - Severity is always set to "high" 22 | - Type is always set to "suspicious" 23 | 24 | - PRICE-FLUCTUATIONS 25 | - Fired when the price of an asset changes drastically 26 | - Severity is always set to "low" 27 | - Type is always set to "suspicious" 28 | 29 | ## [Bot Setup Walkthrough](SETUP.md) 30 | 31 | ## Test Data 32 | 33 | The bot behaviour can be verified with the provided unit tests 34 | -------------------------------------------------------------------------------- /gov-voting-power-change/src/agent.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | maxTracked: 100000, 3 | accumulationMonitoringPeriod: 7 * 60 * 60 * 24, 4 | distributionMonitoringPeriod: 2 * 60 * 60 * 24, 5 | threshholdOfAditionalVotingPowerAccumulated: 1, // 1% 6 | threshholdOfAditionalVotingPowerDistributed: 80, //80% 7 | eventSigs: [ 8 | "event Voted(uint indexed proposalID, bool position, address indexed voter)", 9 | "event Vote(uint indexed proposalId, address indexed voter, bool approve, uint weight)", 10 | "event CastVote(uint256 indexed voteId, address indexed voter, bool supports, uint256 stake)", 11 | ], 12 | tokenDefaultABI: [ 13 | "event Transfer(address indexed from, address indexed to, uint256 value)", 14 | "function balanceOf(address account) external view returns (uint256)", 15 | "function decimals() public view returns (uint8)", 16 | "function totalSupply() public view returns (uint256)", 17 | ], 18 | }; 19 | -------------------------------------------------------------------------------- /large-balance-decrease/SETUP.md: -------------------------------------------------------------------------------- 1 | # Large Balance Decrease Bot 2 | 3 | ## Description 4 | 5 | Detects if the balance of a protocol decreases significantly. 6 | 7 | ## Bot Setup Walkthrough 8 | 9 | The following steps will take you from a completely blank template to a functional bot. 10 | 11 | - Set in `bot-config.json`: 12 | - `aggregationTimePeriod` (required) - The duration of one period (in seconds). Most of the big treasuries send assets less than once a week so we recommend a period of at least one week (604800 seconds). 13 | - `contractAddress` (required) - The monitored contract 14 | - Set in `agent.js`: 15 | - `ALL_REMOVED_KEY`, `PORTION_REMOVED_KEY` and `TOTAL_TRANSFERS_KEY` should be uniquely set for each deployed instance of the bot. 16 | - Set in `package.json`: 17 | - `chainIds` array should only include the monitored chains per instance. 18 | 19 | The monitored tokens will update automatically when the contract sends or receives a transfer. 20 | -------------------------------------------------------------------------------- /malicious-gov-proposal/README.md: -------------------------------------------------------------------------------- 1 | # Malicious Governance Proposal Bot 2 | 3 | ## Description 4 | 5 | This bot detects if a proposal gets submitted with unreasonable parameters 6 | 7 | ## Supported Chains 8 | 9 | - Ethereum 10 | - Optimism 11 | - Binance Smart Chain 12 | - Polygon 13 | - Fantom 14 | - Arbitrum 15 | - Avalanche 16 | 17 | ## Alerts 18 | 19 | - POSSIBLE-MALICIOUS-GOVT-PROPOSAL-CREATED 20 | - Fired when a proposal gets submitted with unreasonable parameters 21 | - Severity is always set to "high" 22 | - Type is always set to "exploit" 23 | 24 | ## [Bot Setup Walkthrough](SETUP.md) 25 | 26 | ## Test Data 27 | 28 | The agent behaviour can be verified with the following transactions: 29 | 30 | - 0x00723e6da4a7a3d13735ee82065069bcd47d92a67460a6021310a3380c7ba339 (set fee to 100); The comp example config should be used 31 | - 0x894bd759984f5e44e040311169baa2bb18c0cc4ccdbdb4019d56c10c193be8ce (burn 3M LDO); The aragon example config should be used 32 | -------------------------------------------------------------------------------- /flashloan-detector/src/detectors/iron-bank-detector.js: -------------------------------------------------------------------------------- 1 | const { ethers, getEthersProvider } = require("forta-agent"); 2 | 3 | const ironBankFlashloanSig = 4 | "event Flashloan(address indexed receiver, uint256 amount, uint256 totalFee, uint256 reservesFee)"; 5 | const ABI = ["function underlying() public view returns (address)"]; 6 | 7 | module.exports = { 8 | getIronBankFlashloan: async (txEvent) => { 9 | const flashloans = []; 10 | const events = txEvent.filterLog(ironBankFlashloanSig); 11 | 12 | await Promise.all( 13 | events.map(async (event) => { 14 | const { amount, receiver } = event.args; 15 | 16 | const contract = new ethers.Contract(event.address, ABI, getEthersProvider()); 17 | const asset = await contract.underlying(); 18 | 19 | flashloans.push({ 20 | asset: asset.toLowerCase(), 21 | amount, 22 | account: receiver.toLowerCase(), 23 | }); 24 | }) 25 | ); 26 | 27 | return flashloans; 28 | }, 29 | }; 30 | -------------------------------------------------------------------------------- /malicious-gov-proposal/SETUP.md: -------------------------------------------------------------------------------- 1 | # Malicious Governance Proposal Bot 2 | 3 | ## Description 4 | 5 | This bot detects if a proposal gets submitted with unreasonable parameters 6 | 7 | ## Bot Setup Walkthrough 8 | 9 | The following steps will take you from a completely blank template to a functional bot. 10 | 11 | address (required) - The governance contract address 12 | 13 | type (required) - The bot only supports `comp` and `aragon` governace types 14 | 15 | parameters (required) - Array with params. It should be different based on the type: 16 | - For comp contracts each param has: 17 | - signature - The function that is monitored 18 | - thresholds 19 | - name - The parameterer's name 20 | - min - The minimum value 21 | - max - The maximum value 22 | - For aragon contracts each param has: 23 | - string - A string that is used for creating a regular expresion. The '*' is used for capturing the value we monitor. The '_' is used to ignore part of the text. Example: 'fund _ with * LDO' will detect funding to every contract. 24 | - min - The minimum value 25 | - max - The maximum value -------------------------------------------------------------------------------- /drastic-price-change-template/SETUP.md: -------------------------------------------------------------------------------- 1 | # Drastic Price Change Anomaly Detection Bot 2 | 3 | ## Description 4 | 5 | This bot detects if the price of an asset changes drastically or there is a large discrepancy between an on-chain and an off-chain oracle 6 | 7 | ## Bot Setup Walkthrough 8 | 9 | The following steps will take you from a completely blank template to a functional bot. 10 | 11 | priceDiscrepancyThreshold (required) - The maximum acceptable price difference between Chainlink price feed and another oracle 12 | 13 | asset (required) - An object containing: 14 | - contract (required) - The asset's address 15 | - coingeckoId (required) - the asset's id on coingecko. Can be obtained by calling [this endpoint](https://api.coingecko.com/api/v3/coins/list) 16 | - chainlinkFeedAddress (required) - The Chainlink price feed address. Can be obtained from [here](https://docs.chain.link/docs/ethereum-addresses/) 17 | - uniswapV3Pool - A uniswap V3 pool. The pool must be X-USD (the first token must be the asset, the second token must be pegged to USD) 18 | 19 | You also have to set the `chainIds` in the `package.json` file. The array must only contain 1 chainId -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Nethermind 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /malicious-gov-proposal/bot-config.json.aragon.example: -------------------------------------------------------------------------------- 1 | { 2 | "address": "0x2e59A20f205bB85a89C53f1936454680651E618e", 3 | "type": "aragon", 4 | "parameters": [ 5 | { 6 | "string": "burn * stETH", 7 | "min": 40, 8 | "max": 50 9 | }, 10 | { 11 | "string": "burn * LDO", 12 | "min": 4000000, 13 | "max": 5000000 14 | }, 15 | { 16 | "string": "allocate * LDO", 17 | "min": 10, 18 | "max": 100 19 | }, 20 | { 21 | "string": "send * LDO", 22 | "min": 100000, 23 | "max": 200000 24 | }, 25 | { 26 | "string": "issue * LDO", 27 | "min": 100000, 28 | "max": 200000 29 | }, 30 | { 31 | "string": "assign * LDO", 32 | "min": 100000, 33 | "max": 200000 34 | }, 35 | { 36 | "string": "set staking limit rate to * ETH", 37 | "min": 100000, 38 | "max": 130000 39 | }, 40 | { 41 | "string": "Set Execution Layer rewards withdrawal limit to *BP", 42 | "min": 0, 43 | "max": 1 44 | }, 45 | { 46 | "string": "Fund _ with * stETH", 47 | "min": 0, 48 | "max": 1 49 | } 50 | ] 51 | } -------------------------------------------------------------------------------- /ice-phishing/src/storage.js: -------------------------------------------------------------------------------- 1 | const { fetchJwt } = require("forta-agent"); 2 | const { readFileSync } = require("fs"); 3 | require("dotenv").config(); 4 | 5 | const OWNER_DB = "https://research.forta.network/database/owner/"; 6 | const hasLocalNode = process.env.hasOwnProperty("LOCAL_NODE"); 7 | 8 | const getToken = async () => { 9 | const tk = await fetchJwt({}); 10 | return { Authorization: `Bearer ${tk}` }; 11 | }; 12 | 13 | const loadJson = async (key) => { 14 | if (hasLocalNode) { 15 | const data = readFileSync("secrets.json", "utf8"); 16 | return JSON.parse(data); 17 | } else { 18 | try { 19 | const response = await fetch(`${OWNER_DB}${key}`, { 20 | headers: await getToken(), 21 | }); 22 | if (response.ok) { 23 | return response.json(); 24 | } else { 25 | throw new Error(`Error loading JSON from owner db: ${response.status}, ${response.statusText}`); 26 | } 27 | } catch (error) { 28 | throw new Error(`Error loading JSON from owner db: ${error}`); 29 | } 30 | } 31 | }; 32 | 33 | const getSecrets = async () => { 34 | return await loadJson("secrets.json"); 35 | }; 36 | 37 | module.exports = { 38 | getSecrets, 39 | }; 40 | -------------------------------------------------------------------------------- /asset-drained/src/storage.js: -------------------------------------------------------------------------------- 1 | const { fetchJwt } = require("forta-agent"); 2 | const { readFileSync } = require("fs"); 3 | require("dotenv").config(); 4 | 5 | const OWNER_DB = "https://research.forta.network/database/owner/"; 6 | const hasLocalNode = process.env.hasOwnProperty("LOCAL_NODE"); 7 | 8 | const getToken = async () => { 9 | const tk = await fetchJwt({}); 10 | return { Authorization: `Bearer ${tk}` }; 11 | }; 12 | 13 | const loadJson = async (key) => { 14 | if (hasLocalNode) { 15 | const data = readFileSync("secrets.json", "utf8"); 16 | return JSON.parse(data); 17 | } else { 18 | try { 19 | const response = await fetch(`${OWNER_DB}${key}`, { 20 | headers: await getToken(), 21 | }); 22 | if (response.ok) { 23 | return response.json(); 24 | } else { 25 | throw new Error(`Error loading JSON from owner db: ${response.status}, ${response.statusText}`); 26 | } 27 | } catch (error) { 28 | throw new Error(`Error loading JSON from owner db: ${error}`); 29 | } 30 | } 31 | }; 32 | 33 | const getSecrets = async () => { 34 | return await loadJson("secrets.json"); 35 | }; 36 | 37 | module.exports = { 38 | getSecrets, 39 | }; 40 | -------------------------------------------------------------------------------- /flashloan-detector/src/storage.js: -------------------------------------------------------------------------------- 1 | const { fetchJwt } = require("forta-agent"); 2 | const { readFileSync } = require("fs"); 3 | require("dotenv").config(); 4 | 5 | const OWNER_DB = "https://research.forta.network/database/owner/"; 6 | const hasLocalNode = process.env.hasOwnProperty("LOCAL_NODE"); 7 | 8 | const getToken = async () => { 9 | const tk = await fetchJwt({}); 10 | return { Authorization: `Bearer ${tk}` }; 11 | }; 12 | 13 | const loadJson = async (key) => { 14 | if (hasLocalNode) { 15 | const data = readFileSync("secrets.json", "utf8"); 16 | return JSON.parse(data); 17 | } else { 18 | try { 19 | const response = await fetch(`${OWNER_DB}${key}`, { 20 | headers: await getToken(), 21 | }); 22 | if (response.ok) { 23 | return response.json(); 24 | } else { 25 | throw new Error(`Error loading JSON from owner db: ${response.status}, ${response.statusText}`); 26 | } 27 | } catch (error) { 28 | throw new Error(`Error loading JSON from owner db: ${error}`); 29 | } 30 | } 31 | }; 32 | 33 | const getSecrets = async () => { 34 | return await loadJson("secrets.json"); 35 | }; 36 | 37 | module.exports = { 38 | getSecrets, 39 | }; 40 | -------------------------------------------------------------------------------- /gov-sneak-proposal/README.md: -------------------------------------------------------------------------------- 1 | # Sneak Governance Proposal Approval 2 | 3 | ## Description 4 | 5 | This bot detects wheter there is a sneak governance proposal about to be approved or is already approved 6 | 7 | ## Supported Chains 8 | 9 | - Ethereum 10 | - Binance Smart Chain 11 | - Polygon 12 | - Avalanche 13 | - Optimism 14 | - Fantom 15 | - Arbitrum 16 | 17 | ## Alerts 18 | 19 | - SNEAK-GOVT-PROPOSAL-APPROVAL-PASSED 20 | 21 | - Fired when a sneak proposal is passed 22 | - Severity is always set to "medium" 23 | - Type is always set to "suspicious" 24 | - Metadata Fields: 25 | - ACCOUNTS (accounts involved in the vote) 26 | 27 | - SNEAK-GOVT-PROPOSAL-APPROVAL-ABOUT-TO-PASS 28 | - Fired when a sneak proposal is about to pass 29 | - Severity is always set to "medium" 30 | - Type is always set to "suspicious" 31 | - Metadata Fields: 32 | - ACCOUNTS (accounts involved in the vote) 33 | 34 | ## Test Data 35 | 36 | The bot behvaiour can be verified with the supplied unit tests or with these blocks: 37 | 38 | - `yarn block 14879134,14883382,14884348,14884491,14896539,14897168,14897208,14897450,14897729` (need to use example configuration and expected behaviour is to return no findings, ethereum) 39 | -------------------------------------------------------------------------------- /flashbots-transactions-detector/src/storage.js: -------------------------------------------------------------------------------- 1 | const { fetchJwt } = require("forta-agent"); 2 | const { readFileSync } = require("fs"); 3 | require("dotenv").config(); 4 | 5 | const OWNER_DB = "https://research.forta.network/database/owner/"; 6 | const hasLocalNode = process.env.hasOwnProperty("LOCAL_NODE"); 7 | 8 | const getToken = async () => { 9 | const tk = await fetchJwt({}); 10 | return { Authorization: `Bearer ${tk}` }; 11 | }; 12 | 13 | const loadJson = async (key) => { 14 | if (hasLocalNode) { 15 | const data = readFileSync("secrets.json", "utf8"); 16 | return JSON.parse(data); 17 | } else { 18 | try { 19 | const response = await fetch(`${OWNER_DB}${key}`, { 20 | headers: await getToken(), 21 | }); 22 | if (response.ok) { 23 | return response.json(); 24 | } else { 25 | throw new Error(`Error loading JSON from owner db: ${response.status}, ${response.statusText}`); 26 | } 27 | } catch (error) { 28 | throw new Error(`Error loading JSON from owner db: ${error}`); 29 | } 30 | } 31 | }; 32 | 33 | const getSecrets = async () => { 34 | return await loadJson("secrets.json"); 35 | }; 36 | 37 | module.exports = { 38 | getSecrets, 39 | }; 40 | -------------------------------------------------------------------------------- /forta-tornado-cash-starter-kit/src/storage.js: -------------------------------------------------------------------------------- 1 | const { fetchJwt } = require("forta-agent"); 2 | const { readFileSync } = require("fs"); 3 | require("dotenv").config(); 4 | 5 | const OWNER_DB = "https://research.forta.network/database/owner/"; 6 | const hasLocalNode = process.env.hasOwnProperty("LOCAL_NODE"); 7 | 8 | const getToken = async () => { 9 | const tk = await fetchJwt({}); 10 | return { Authorization: `Bearer ${tk}` }; 11 | }; 12 | 13 | const loadJson = async (key) => { 14 | if (hasLocalNode) { 15 | const data = readFileSync("secrets.json", "utf8"); 16 | return JSON.parse(data); 17 | } else { 18 | try { 19 | const response = await fetch(`${OWNER_DB}${key}`, { 20 | headers: await getToken(), 21 | }); 22 | if (response.ok) { 23 | return response.json(); 24 | } else { 25 | throw new Error(`Error loading JSON from owner db: ${response.status}, ${response.statusText}`); 26 | } 27 | } catch (error) { 28 | throw new Error(`Error loading JSON from owner db: ${error}`); 29 | } 30 | } 31 | }; 32 | 33 | const getSecrets = async () => { 34 | return await loadJson("secrets.json"); 35 | }; 36 | 37 | module.exports = { 38 | getSecrets, 39 | }; 40 | -------------------------------------------------------------------------------- /ice-phishing/bot-config.json.example: -------------------------------------------------------------------------------- 1 | { 2 | "approveCountThreshold": 2, 3 | "transferCountThreshold": 2, 4 | "timePeriodDays": 30, 5 | "nonceThreshold": 100, 6 | "maxAddressAlertsPerPeriod": 5, 7 | "etherscanApis": { 8 | "1": { 9 | "key": "YourApiKeyToken", 10 | "url": "https://api.etherscan.io/api?module=contract&action=getabi" 11 | }, 12 | "10": { 13 | "key": "YourApiKeyToken", 14 | "url": "https://api-optimistic.etherscan.io/api?module=contract&action=getabi" 15 | }, 16 | "56": { 17 | "key": "YourApiKeyToken", 18 | "url": "https://api.bscscan.com/api?module=contract&action=getabi" 19 | }, 20 | "137": { 21 | "key": "YourApiKeyToken", 22 | "url": "https://api.polygonscan.com/api?module=contract&action=getabi" 23 | }, 24 | "250": { 25 | "key": "YourApiKeyToken", 26 | "url": "https://api.ftmscan.com/api?module=contract&action=getabi" 27 | }, 28 | "42161": { 29 | "key": "YourApiKeyToken", 30 | "url": "https://api.arbiscan.io/api?module=contract&action=getabi" 31 | }, 32 | "43114": { 33 | "key": "YourApiKeyToken", 34 | "url": "https://api.snowtrace.io/api?module=contract&action=getabi" 35 | } 36 | } 37 | } -------------------------------------------------------------------------------- /large-mint-borrow-anomaly-detection/README.md: -------------------------------------------------------------------------------- 1 | # Large Mint/Borrow Value Anomaly Detection 2 | 3 | ## Description 4 | 5 | This bot detects Transactions with Anomalies in Volume for Mints/Borrows 6 | 7 | ## Supported Chains 8 | 9 | - Ethereum 10 | - Binance Smart Chain 11 | - Polygon 12 | - Optimism 13 | - Arbitrum 14 | - Avalanche 15 | - Fantom 16 | 17 | ## Alerts 18 | 19 | - HIGH-MINT-VALUE 20 | 21 | - Fired when there is unusually high number of mints from an address 22 | - Severity is always set to "medium" 23 | - Type is always set to "exploit" 24 | - Metadata fields: 25 | - FIRST_TRANSACTION_HASH (first hash when it occured) 26 | - LAST_TRANSACTION_HASH (last hash when it occured) 27 | - ASSET_IMPACTED (address of the impaced asset) 28 | - BASELINE_VOLUME (the normal volume) 29 | 30 | - HIGH-BORROW-VALUE 31 | 32 | - Fired when there is unusually high number of borrows from an address 33 | - Severity is always set to "medium" 34 | - Type is always set to "exploit" 35 | - Metadata fields: 36 | - FIRST_TRANSACTION_HASH (first hash when it occured) 37 | - LAST_TRANSACTION_HASH (last hash when it occured) 38 | - ASSET_IMPACTED (address of the impaced asset) 39 | - BASELINE_VOLUME (the normal volume) 40 | 41 | ## Test Data 42 | 43 | The bot behaviour can be verified with supplied unit tests 44 | -------------------------------------------------------------------------------- /transaction-volume-anomaly-detection/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "transaction-volume-anomaly-detection", 3 | "displayName": "Transaction Volume Anomaly Detection", 4 | "version": "0.0.1", 5 | "description": "This bot detects Transactions with Anomalies in Volume", 6 | "longDescription": "This bot identifies transactions that exhibit irregularities in their volume. Its core purpose revolves around systematically monitoring transactional activities and pinpointing instances where there are notable deviations from expected volume patterns.", 7 | "chainIds": [ 8 | 1, 9 | 56, 10 | 10, 11 | 137, 12 | 42161, 13 | 250, 14 | 43114 15 | ], 16 | "scripts": { 17 | "start": "npm run start:dev", 18 | "start:dev": "nodemon --watch src --watch forta.config.json -e js,json --exec \"forta-agent run\"", 19 | "start:prod": "forta-agent run --prod", 20 | "tx": "forta-agent run --tx", 21 | "block": "forta-agent run --block", 22 | "range": "forta-agent run --range", 23 | "file": "forta-agent run --file", 24 | "publish": "forta-agent publish", 25 | "push": "forta-agent push", 26 | "disable": "forta-agent disable", 27 | "enable": "forta-agent enable", 28 | "keyfile": "forta-agent keyfile", 29 | "test": "jest" 30 | }, 31 | "dependencies": { 32 | "@types/jest": "^27.4.1", 33 | "arima": "^0.2.5", 34 | "forta-agent": "^0.1.36" 35 | }, 36 | "devDependencies": { 37 | "jest": "^27.0.6", 38 | "nodemon": "^2.0.8" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /flashloan-detector/src/detectors/dodo-detector.js: -------------------------------------------------------------------------------- 1 | const { ethers, getEthersProvider } = require("forta-agent"); 2 | 3 | const dodoFlashloanAbi = 4 | "event DODOFlashLoan (address borrower, address assetTo, uint256 baseAmount, uint256 quoteAmount)"; 5 | 6 | const dodoPoolAbi = [ 7 | "function _BASE_TOKEN_() public view returns (address)", 8 | "function _QUOTE_TOKEN_() public view returns (address)", 9 | ]; 10 | 11 | module.exports = { 12 | getDodoFlashloan: async (txEvent) => { 13 | const events = txEvent.filterLog(dodoFlashloanAbi); 14 | 15 | const flashloans = await Promise.all( 16 | events.map(async (event) => { 17 | const { address } = event; 18 | const { assetTo, baseAmount, quoteAmount } = event.args; 19 | 20 | const contract = new ethers.Contract(address, dodoPoolAbi, getEthersProvider()); 21 | 22 | if (quoteAmount.gt(ethers.constants.Zero)) { 23 | const quoteToken = await contract._QUOTE_TOKEN_(); 24 | 25 | return { 26 | asset: quoteToken.toLowerCase(), 27 | amount: quoteAmount, 28 | account: assetTo.toLowerCase(), 29 | }; 30 | } else if (baseAmount.gt(ethers.constants.Zero)) { 31 | const baseToken = await contract._BASE_TOKEN_(); 32 | 33 | return { 34 | asset: baseToken.toLowerCase(), 35 | amount: baseAmount, 36 | account: assetTo.toLowerCase(), 37 | }; 38 | } 39 | }) 40 | ); 41 | return flashloans; 42 | }, 43 | }; 44 | -------------------------------------------------------------------------------- /gov-sneak-proposal/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "gov-sneak-proposal", 3 | "displayName": "Gov Sneak Proposal", 4 | "version": "0.0.1", 5 | "description": "This bot detects if there is a sneak governance proposal about to be approved or is already approved", 6 | "longDescription": "The bot focuses on detecting instances of sneak governance proposals that are poised for approval or have already been approved. Its core purpose is to identify for situations where such proposals, which may be characterized by hidden or unexpected content, are on the verge of obtaining approval or have already achieved it within the governance system", 7 | "chainIds": [ 8 | 1, 9 | 137, 10 | 56, 11 | 43114, 12 | 42116, 13 | 250, 14 | 10 15 | ], 16 | "scripts": { 17 | "start": "npm run start:dev", 18 | "start:dev": "nodemon --watch src --watch forta.config.json -e js,json --exec \"forta-agent run\"", 19 | "start:prod": "forta-agent run --prod", 20 | "tx": "forta-agent run --tx", 21 | "block": "forta-agent run --block", 22 | "range": "forta-agent run --range", 23 | "file": "forta-agent run --file", 24 | "publish": "forta-agent publish", 25 | "push": "forta-agent push", 26 | "disable": "forta-agent disable", 27 | "enable": "forta-agent enable", 28 | "keyfile": "forta-agent keyfile", 29 | "test": "jest" 30 | }, 31 | "dependencies": { 32 | "forta-agent": "^0.1.36" 33 | }, 34 | "devDependencies": { 35 | "jest": "^27.0.6", 36 | "nodemon": "^2.0.8" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /large-mint-borrow-anomaly-detection/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "large-mint-borrow-volume-anomlay-detection", 3 | "displayName": "Large Mint/Borrow Volume Anomaly Detection", 4 | "version": "0.0.1", 5 | "description": "This bot detects large mint/borrow anomalies", 6 | "longDescription": "This bot focuses on transactional anomalies related to minting and borrowing activities. Its primary purpose is to identify instances where there are notable irregularities in transaction volume for these actions.", 7 | "chainIds": [ 8 | 1, 9 | 56, 10 | 10, 11 | 137, 12 | 42161, 13 | 250, 14 | 43114 15 | ], 16 | "scripts": { 17 | "start": "npm run start:dev", 18 | "start:dev": "nodemon --watch src --watch forta.config.json -e js,json --exec \"forta-agent run\"", 19 | "start:prod": "forta-agent run --prod", 20 | "tx": "forta-agent run --tx", 21 | "block": "forta-agent run --block", 22 | "range": "forta-agent run --range", 23 | "file": "forta-agent run --file", 24 | "publish": "forta-agent publish", 25 | "push": "forta-agent push", 26 | "disable": "forta-agent disable", 27 | "enable": "forta-agent enable", 28 | "keyfile": "forta-agent keyfile", 29 | "test": "jest" 30 | }, 31 | "dependencies": { 32 | "@types/jest": "^27.4.1", 33 | "arima": "^0.2.5", 34 | "bignumber.js": "^9.0.2", 35 | "forta-agent": "^0.1.36", 36 | "rolling-math": "^0.0.3" 37 | }, 38 | "devDependencies": { 39 | "jest": "^27.0.6", 40 | "nodemon": "^2.0.8" 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /large-mint-borrow-anomaly-detection/src/agent.config.js: -------------------------------------------------------------------------------- 1 | const commonEventSigs = [ 2 | "event Borrow(address indexed _reserve,address indexed _user,uint256 _amount,uint256 _borrowRateMode,uint256 _borrowRate,uint256 _originationFee,uint256 _borrowBalanceIncrease,uint16 indexed _referral,uint256 _timestamp)", 3 | "event Borrow(address borrower, uint256 borrowAmount, uint256 accountBorrows, uint256 totalBorrows)", 4 | "event Transfer(address indexed from, address indexed to, uint256 value)", 5 | "event Mint(address minter, uint256 mintAmount, uint256 mintTokens)", 6 | ]; 7 | 8 | module.exports = { 9 | bucketBlockSize: 5, 10 | commonEventSigs, 11 | limitTracked: 10000, 12 | aggregationTimePeriod: 1, 13 | getMinBucketBlockSizeByChainId: (chainId) => { 14 | switch (chainId) { 15 | case 1: 16 | return 1000; 17 | case 56: 18 | return 5000; 19 | case 137: 20 | return 6000; 21 | case 43114: 22 | return 5000; 23 | case 10: 24 | return 1200; 25 | case 42161: 26 | return 1000; 27 | case 250: 28 | return 12000; 29 | } 30 | }, 31 | getBlocktimeByChainId: (chainId) => { 32 | switch (chainId) { 33 | case 1: 34 | return 14; 35 | case 137: 36 | return 2.7; 37 | case 43114: 38 | return 3; 39 | case 56: 40 | return 3; 41 | case 10: 42 | return 13; 43 | case 42161: 44 | return 15; 45 | case 250: 46 | return 1.2; 47 | } 48 | }, 49 | }; 50 | -------------------------------------------------------------------------------- /gov-voting-power-change/README.md: -------------------------------------------------------------------------------- 1 | # Governance Voting Power Change 2 | 3 | ## Description 4 | 5 | This bot detects changes in voting power for a specific address for a specific Governance protocol 6 | 7 | ## Supported Chains 8 | 9 | - Ethereum 10 | - Polygon 11 | - Fantom 12 | - Optimism 13 | - Avalanche 14 | - Binance Smart Chain 15 | - Arbitrum 16 | 17 | ## Alerts 18 | 19 | - SIGNIFICANT-VOTING-POWER-ACCUMULATION 20 | 21 | - Fired when an address recieves a significant increase in voting power for a protocol 22 | - Severity is always set to "low" 23 | - Type is always set to "suspicious" 24 | 25 | - SIGNIFICANT-VOTING-POWER-ACCUMULATION-VOTED 26 | 27 | - Fired when an address recieves a significant increase in voting power for a protocol and has voted on the protocol 28 | - Severity is always set to "medium" 29 | - Type is always set to "suspicious" 30 | 31 | - SIGNIFICANT-VOTING-POWER-ACCUMULATION-DISTRIBUTION 32 | 33 | - Fired when an address recieves a significant increase in voting power for a protocol and then distributing it to other people 34 | - Severity is always set to "medium" 35 | - Type is always set to "suspicious" 36 | 37 | - SIGNIFICANT-VOTING-POWER-ACCUMULATION-DISTRIBUTION-VOTED 38 | - Fired when an address recieves a significant increase in voting power for a protocol and then distributing it to other people and then has voted 39 | - Severity is always set to "medium" 40 | - Type is always set to "suspicious" 41 | 42 | ## Test Data 43 | 44 | The bot behaviour can be verified with the specified unit tests, 45 | -------------------------------------------------------------------------------- /transaction-volume-anomaly-detection-template/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "transaction-volume-anomaly-detection-template", 3 | "displayName": "Transaction Volume Anomaly Detection Template", 4 | "version": "0.0.1", 5 | "description": "This bot detects Transactions with Anomalies in Volume - template", 6 | "longDescription": "This bot identifies transactions that exhibit irregularities in their volume. Its core purpose revolves around systematically monitoring transactional activities and pinpointing instances where there are notable deviations from expected volume patterns. - template", 7 | "chainIds": [ 8 | 1, 9 | 56, 10 | 10, 11 | 137, 12 | 42161, 13 | 250, 14 | 43114 15 | ], 16 | "scripts": { 17 | "start": "npm run start:dev", 18 | "start:dev": "nodemon --watch src --watch forta.config.json -e js,json --exec \"forta-agent run\"", 19 | "start:prod": "forta-agent run --prod", 20 | "tx": "forta-agent run --tx", 21 | "block": "forta-agent run --block", 22 | "range": "forta-agent run --range", 23 | "file": "forta-agent run --file", 24 | "publish": "forta-agent publish", 25 | "push": "forta-agent push", 26 | "disable": "forta-agent disable", 27 | "enable": "forta-agent enable", 28 | "keyfile": "forta-agent keyfile", 29 | "test": "jest" 30 | }, 31 | "dependencies": { 32 | "@types/jest": "^27.4.1", 33 | "arima": "^0.2.5", 34 | "forta-agent": "^0.1.36" 35 | }, 36 | "devDependencies": { 37 | "jest": "^27.0.6", 38 | "nodemon": "^2.0.8" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /gov-voting-power-change/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "gov-voting-power-change", 3 | "displayName": "Governance Voting Power Change", 4 | "version": "0.0.1", 5 | "description": "This bot detects changes in voting power for a specific address for a specific Governance protocol", 6 | "longDescription": "This bot's primary function revolves around detecting shifts in voting power attributed to a specified address within a designated governance framework. By closely monitoring and identifying changes in the influence a particular address holds over governance decisions, the bot offers insights into alterations that might impact the decision-making dynamics of the specified protocol.", 7 | "chainIds": [ 8 | 1, 9 | 137, 10 | 10, 11 | 250, 12 | 56, 13 | 42116 14 | ], 15 | "scripts": { 16 | "start": "npm run start:dev", 17 | "start:dev": "nodemon --watch src --watch forta.config.json -e js,json --exec \"forta-agent run\"", 18 | "start:prod": "forta-agent run --prod", 19 | "tx": "forta-agent run --tx", 20 | "block": "forta-agent run --block", 21 | "range": "forta-agent run --range", 22 | "file": "forta-agent run --file", 23 | "publish": "forta-agent publish", 24 | "push": "forta-agent push", 25 | "disable": "forta-agent disable", 26 | "enable": "forta-agent enable", 27 | "keyfile": "forta-agent keyfile", 28 | "test": "jest" 29 | }, 30 | "dependencies": { 31 | "@types/jest": "^27.5.1", 32 | "ethers-multicall": "^0.2.3", 33 | "forta-agent": "^0.1.36" 34 | }, 35 | "devDependencies": { 36 | "jest": "^27.0.6", 37 | "nodemon": "^2.0.8" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /malicious-gov-proposal/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "malicious-governance-proposal", 3 | "displayName": "Malicious Governance Proposal", 4 | "version": "0.0.1", 5 | "description": "This bot detects if a proposal gets submitted with unreasonable parameters", 6 | "longDescription": "This bot identifies instances where proposals are submitted with parameters that appear unreasonable. Its primary function involves systematically scrutinizing proposed parameters within the context of governance proposals, aiming to recognize and flag situations where these parameters deviate significantly from established norms or rational considerations.", 7 | "chainIds": [ 8 | 1, 9 | 10, 10 | 56, 11 | 137, 12 | 250, 13 | 42161, 14 | 43114 15 | ], 16 | "scripts": { 17 | "start": "npm run start:dev", 18 | "start:dev": "nodemon --watch src --watch forta.config.json -e js,json --exec \"forta-agent run\"", 19 | "start:prod": "forta-agent run --prod", 20 | "tx": "forta-agent run --tx", 21 | "block": "forta-agent run --block", 22 | "range": "forta-agent run --range", 23 | "file": "forta-agent run --file", 24 | "publish": "forta-agent publish", 25 | "push": "forta-agent push", 26 | "disable": "forta-agent disable", 27 | "enable": "forta-agent enable", 28 | "keyfile": "forta-agent keyfile", 29 | "test": "jest" 30 | }, 31 | "dependencies": { 32 | "forta-agent": "^0.1.36" 33 | }, 34 | "devDependencies": { 35 | "eslint": "^8.20.0", 36 | "eslint-config-airbnb-base": "^15.0.0", 37 | "eslint-plugin-import": "^2.26.0", 38 | "jest": "^28.1.3", 39 | "nodemon": "^2.0.19" 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /flashbots-transactions-detector/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "flashbots-transactions-detector", 3 | "displayName": "Flashbots Transactions Detector", 4 | "version": "0.0.9", 5 | "description": "This bot detects flashbots transactions", 6 | "longDescription": "The bot identifies transactions executed Flashbots. Its core purpose is to autonomously recognize and flag transactions that are conducted using the Flashbots mechanism.", 7 | "repository": "https://github.com/NethermindEth/forta-starter-kits/tree/main/flashbots-transactions-detector", 8 | "chainIds": [ 9 | 1 10 | ], 11 | "chainSettings": { 12 | "1": { 13 | "shards": 1, 14 | "target": 6 15 | } 16 | }, 17 | "scripts": { 18 | "start": "npm run start:dev", 19 | "start:dev": "nodemon --watch src --watch forta.config.json -e js,json --exec \"forta-agent run\"", 20 | "start:prod": "forta-agent run --prod", 21 | "tx": "forta-agent run --tx", 22 | "block": "forta-agent run --block", 23 | "range": "forta-agent run --range", 24 | "file": "forta-agent run --file", 25 | "publish": "forta-agent publish", 26 | "push": "forta-agent push", 27 | "disable": "forta-agent disable", 28 | "enable": "forta-agent enable", 29 | "keyfile": "forta-agent keyfile", 30 | "test": "jest --detectOpenHandles", 31 | "format": "prettier --write \"src/**/*.js\"" 32 | }, 33 | "dependencies": { 34 | "axios": "^0.27.2", 35 | "bot-alert-rate": "^0.0.4", 36 | "dotenv": "^16.0.3", 37 | "forta-agent": "^0.1.48", 38 | "node-fetch": "^2.6.7" 39 | }, 40 | "devDependencies": { 41 | "forta-agent-tools": "^3.1.2", 42 | "jest": "^29.7.0", 43 | "nodemon": "^3.0.1", 44 | "prettier": "^2.7.1" 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /drastic-price-change-template/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "drastic-price-change-anomaly-bot", 3 | "displayName": "Drastic Price Change Anomaly Bot", 4 | "version": "0.0.1", 5 | "description": "Detects if the price of an asset changes drastically or there is a large discrepancy between an on-chain and an off-chain oracle", 6 | "longDescription": "The bot operates as an observer with a primary objective of identifying significant variations in asset prices or notable disparities between on-chain and off-chain oracles. By closely monitoring price fluctuations, the bot notifies when abrupt price changes occur or when there is a substantial inconsistency between the data provided by on-chain sources and external oracles.", 7 | "chainIds": [ 8 | 1, 9 | 10, 10 | 56, 11 | 137, 12 | 250, 13 | 42161, 14 | 43114 15 | ], 16 | "scripts": { 17 | "start": "npm run start:dev", 18 | "start:dev": "nodemon --watch src --watch forta.config.json -e js,json --exec \"forta-agent run\"", 19 | "start:prod": "forta-agent run --prod", 20 | "tx": "forta-agent run --tx", 21 | "block": "forta-agent run --block", 22 | "range": "forta-agent run --range", 23 | "file": "forta-agent run --file", 24 | "publish": "forta-agent publish", 25 | "push": "forta-agent push", 26 | "disable": "forta-agent disable", 27 | "enable": "forta-agent enable", 28 | "keyfile": "forta-agent keyfile", 29 | "test": "jest" 30 | }, 31 | "dependencies": { 32 | "arima": "^0.2.5", 33 | "axios": "^0.27.2", 34 | "forta-agent": "^0.1.36" 35 | }, 36 | "devDependencies": { 37 | "eslint": "^8.20.0", 38 | "eslint-config-airbnb-base": "^15.0.0", 39 | "eslint-plugin-import": "^2.26.0", 40 | "jest": "^28.1.3", 41 | "nodemon": "^2.0.19" 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /bridge-balance-difference/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "bridge-balance-difference-bot", 3 | "displayName": "Bridge Balance Difference Bot", 4 | "version": "0.0.1", 5 | "description": "Detects if two sides of the bridge show significant balance difference", 6 | "longDescription": "The bot operates as a monitoring tool that focuses on the balances of two interconnected sides of a bridge. Its primary function lies in identifying instances where a substantial disparity in balances exists between these two sides of the bridge. This discrepancy detection serves as a mechanism to ensure the integrity and equilibrium of the blockchain's interlinked components, allowing stakeholders to be informed of potential imbalances that may require attention or investigation.", 7 | "chainIds": [ 8 | 1, 9 | 10, 10 | 56, 11 | 137, 12 | 250, 13 | 42161, 14 | 43114 15 | ], 16 | "scripts": { 17 | "start": "npm run start:dev", 18 | "start:dev": "nodemon --watch src --watch forta.config.json -e js,json --exec \"forta-agent run\"", 19 | "start:prod": "forta-agent run --prod", 20 | "tx": "forta-agent run --tx", 21 | "block": "forta-agent run --block", 22 | "range": "forta-agent run --range", 23 | "file": "forta-agent run --file", 24 | "publish": "forta-agent publish", 25 | "push": "forta-agent push", 26 | "disable": "forta-agent disable", 27 | "enable": "forta-agent enable", 28 | "keyfile": "forta-agent keyfile", 29 | "test": "jest" 30 | }, 31 | "dependencies": { 32 | "arima": "^0.2.5", 33 | "forta-agent": "^0.1.36" 34 | }, 35 | "devDependencies": { 36 | "eslint": "^8.20.0", 37 | "eslint-config-airbnb-base": "^15.0.0", 38 | "eslint-plugin-import": "^2.26.0", 39 | "jest": "^28.1.3", 40 | "nodemon": "^2.0.19" 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /transaction-volume-anomaly-detection-template/README.md: -------------------------------------------------------------------------------- 1 | # Transaction Volume Anomaly Detection 2 | 3 | ## Description 4 | 5 | This bot detects Transactions with Anomalies in Volume - template 6 | 7 | ## Supported Chains 8 | 9 | - Ethereum 10 | - Binance Smart Chain 11 | - Polygon 12 | - Optimism 13 | - Arbitrum 14 | - Fantom 15 | - Avalanche 16 | 17 | ## Alerts 18 | 19 | - SUCCESSFUL-INTERNAL-TRANSACTION-VOL-INCREASE 20 | 21 | - Fired when there is unusually high number of successful internal transactions 22 | - Severity is always set to "low" 23 | - Type is always set to "suspicious" 24 | - Metadata fields: 25 | - COUNT (Current count of successful transaction) 26 | - EXPECTED_BASELINE (Expected baseline count of successful transaction) 27 | 28 | - SUCCESSFUL-TRANSACTION-VOL-INCREASE 29 | 30 | - Fired when there is unusually high number of successful transactions 31 | - Severity is always set to "low" 32 | - Type is always set to "suspicious" 33 | - Metadata fields: 34 | - COUNT (Current count of successful transaction) 35 | - EXPECTED_BASELINE (Expected baseline count of successful transaction) 36 | 37 | - FAILED-TRANSACTION-VOL-INCREASE 38 | 39 | - Fired when there is unusually high number of failed transactions 40 | - Severity is always set to "high" 41 | - Type is always set to "exploit" 42 | - Metadata fields: 43 | - COUNT (Current count of successful transaction) 44 | - EXPECTED_BASELINE (Expected baseline count of successful transaction) 45 | 46 | - FAILED-INTERNAL-TRANSACTION-VOL-INCREASE 47 | 48 | - Fired when there is unusually high number of failed internal transactions 49 | - Severity is always set to "medium" 50 | - Type is always set to "suspicious" 51 | - Metadata fields: 52 | - COUNT (Current count of successful transaction) 53 | - EXPECTED_BASELINE (Expected baseline count of successful transaction) 54 | 55 | ## Test Data 56 | 57 | The agent behaviour can be verified with supplied unit tests 58 | -------------------------------------------------------------------------------- /flashloan-detector/src/flashloan-detector.js: -------------------------------------------------------------------------------- 1 | const { getAaveV2Flashloan } = require("./detectors/aave-v2-detector"); 2 | const { getAaveV3Flashloan } = require("./detectors/aave-v3-detector"); 3 | const { getDydxFlashloan } = require("./detectors/dydx-detector"); 4 | const { getEulerFlashloan } = require("./detectors/euler-detector"); 5 | const { getIronBankFlashloan } = require("./detectors/iron-bank-detector"); 6 | const { getMakerFlashloan } = require("./detectors/maker-detector"); 7 | const { getBalancerFlashloan } = require("./detectors/balancer-detector"); 8 | const { getUniswapV2Flashloan } = require("./detectors/uniswap-v2-detector"); 9 | const { getUniswapV3Flashloan } = require("./detectors/uniswap-v3-detector"); 10 | const { getDodoFlashloan } = require("./detectors/dodo-detector"); 11 | 12 | module.exports = { 13 | // Returns an array of protocols from which a flashloan was taken 14 | async getFlashloans(txEvent) { 15 | const flashloanProtocols = []; 16 | const aaveV2Flashloans = getAaveV2Flashloan(txEvent); 17 | const aaveV3Flashloans = getAaveV3Flashloan(txEvent); 18 | const dydxFlashloans = await getDydxFlashloan(txEvent); 19 | const eulerFlashloans = getEulerFlashloan(txEvent); 20 | const ironBankFlashloans = await getIronBankFlashloan(txEvent); 21 | const makerFlashloans = getMakerFlashloan(txEvent); 22 | const uniswapV2Flashloans = await getUniswapV2Flashloan(txEvent); 23 | const uniswapV3Flashloans = await getUniswapV3Flashloan(txEvent); 24 | const balancerFlashloans = getBalancerFlashloan(txEvent); 25 | const dodoFlashloans = await getDodoFlashloan(txEvent); 26 | 27 | flashloanProtocols.push( 28 | ...aaveV2Flashloans, 29 | ...aaveV3Flashloans, 30 | ...dydxFlashloans, 31 | ...eulerFlashloans, 32 | ...ironBankFlashloans, 33 | ...makerFlashloans, 34 | ...uniswapV2Flashloans, 35 | ...uniswapV3Flashloans, 36 | ...balancerFlashloans, 37 | ...dodoFlashloans 38 | ); 39 | 40 | return flashloanProtocols; 41 | }, 42 | }; 43 | -------------------------------------------------------------------------------- /large-balance-decrease/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "large-balance-decrease-bot", 3 | "displayName": "Large Balance Decrease Bot", 4 | "version": "0.0.4", 5 | "description": "Detects if the balance of a protocol decreases significantly", 6 | "longDescription": "The bot is centered around monitoring the financial stability of protocols. Its primary role involves identifying instances where the balance of a specific bridge experiences a substantial decrease. By actively tracking these significant declines in balance, the bot alerts to potential deviations that may warrant further investigation or action.", 7 | "chainIds": [ 8 | 1, 9 | 10, 10 | 56, 11 | 137, 12 | 250, 13 | 42161, 14 | 43114 15 | ], 16 | "chainSettings": { 17 | "1": { 18 | "shards": 2, 19 | "target": 1 20 | }, 21 | "56": { 22 | "shards": 2, 23 | "target": 1 24 | }, 25 | "default": { 26 | "shards": 1, 27 | "target": 1 28 | } 29 | }, 30 | "repository": "https://github.com/NethermindEth/forta-starter-kits/tree/main/large-balance-decrease", 31 | "scripts": { 32 | "start": "npm run start:dev", 33 | "start:dev": "nodemon --watch src --watch forta.config.json -e js,json --exec \"forta-agent run\"", 34 | "start:prod": "forta-agent run --prod", 35 | "tx": "forta-agent run --tx", 36 | "block": "forta-agent run --block", 37 | "range": "forta-agent run --range", 38 | "file": "forta-agent run --file", 39 | "publish": "forta-agent publish", 40 | "push": "forta-agent push", 41 | "disable": "forta-agent disable", 42 | "enable": "forta-agent enable", 43 | "keyfile": "forta-agent keyfile", 44 | "test": "jest", 45 | "format": "prettier --write \"src/**/*.js\"" 46 | }, 47 | "dependencies": { 48 | "arima": "^0.2.5", 49 | "forta-agent": "^0.1.36", 50 | "node-fetch": "^2.6.7", 51 | "dotenv": "^16.0.3" 52 | }, 53 | "devDependencies": { 54 | "jest": "^28.1.3", 55 | "nodemon": "^3.0.1", 56 | "prettier": "^2.7.1" 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /asset-drained/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "asset-drained", 3 | "displayName": "Asset Drained", 4 | "version": "0.2.9", 5 | "description": "Detects if an asset is fully drained from a contract", 6 | "longDescription": "The bot identifies instances where a contract's assets have been significantly depleted, reaching a threshold of 99% or more within a single block. By closely monitoring the transfers of both ERC20 tokens and native tokens from contracts, the bot raises an alert when it detects a rapid reduction in a contract's balance", 7 | "repository": "https://github.com/NethermindEth/forta-starter-kits/tree/main/asset-drained", 8 | "chainIds": [ 9 | 1, 10 | 10, 11 | 56, 12 | 137, 13 | 250, 14 | 42161, 15 | 43114 16 | ], 17 | "chainSettings": { 18 | "1": { 19 | "shards": 2, 20 | "target": 3 21 | }, 22 | "56": { 23 | "shards": 4, 24 | "target": 3 25 | }, 26 | "137": { 27 | "shards": 4, 28 | "target": 3 29 | }, 30 | "250": { 31 | "shards": 2, 32 | "target": 3 33 | }, 34 | "default": { 35 | "shards": 1, 36 | "target": 3 37 | } 38 | }, 39 | "scripts": { 40 | "start": "npm run start:dev", 41 | "start:dev": "nodemon --watch src --watch forta.config.json -e js,json --exec \"forta-agent run\"", 42 | "start:prod": "forta-agent run --prod", 43 | "tx": "forta-agent run --tx", 44 | "block": "forta-agent run --block", 45 | "range": "forta-agent run --range", 46 | "file": "forta-agent run --file", 47 | "publish": "forta-agent publish", 48 | "info": "forta-agent info", 49 | "logs": "forta-agent logs", 50 | "push": "forta-agent push", 51 | "disable": "forta-agent disable", 52 | "enable": "forta-agent enable", 53 | "keyfile": "forta-agent keyfile", 54 | "test": "jest", 55 | "format": "prettier --write \"src/**/*.js\"" 56 | }, 57 | "dependencies": { 58 | "bot-alert-rate": "^0.0.4", 59 | "dotenv": "^16.0.3", 60 | "forta-agent": "^0.1.40", 61 | "forta-agent-tools": "^3.1.2", 62 | "lru-cache": "^7.14.0" 63 | }, 64 | "devDependencies": { 65 | "jest": "^28.1.3", 66 | "nodemon": "^3.0.1", 67 | "prettier": "^2.7.1" 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /forta-tornado-cash-starter-kit/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tornado-cash-funded-account-interaction", 3 | "displayName": "Tornado Cash Funded Account Interaction", 4 | "version": "0.0.9", 5 | "description": "This bot detects when an account that was funded by Tornado Cash interacts with any contract", 6 | "longDescription": "The bot operates as a tracker with a core function of identifying interactions between accounts funded by Tornado Cash and external contracts. Its primary role is to detect instances where accounts that have received funding from Tornado Cash with contracts beyond the Tornado Cash ecosystem.", 7 | "repository": "https://github.com/NethermindEth/forta-starter-kits/tree/main/forta-tornado-cash-starter-kit", 8 | "chainIds": [ 9 | 1, 10 | 56, 11 | 137, 12 | 10, 13 | 42161 14 | ], 15 | "chainSettings": { 16 | "1": { 17 | "shards": 9, 18 | "target": 3 19 | }, 20 | "10": { 21 | "shards": 4, 22 | "target": 3 23 | }, 24 | "56": { 25 | "shards": 15, 26 | "target": 3 27 | }, 28 | "137": { 29 | "shards": 14, 30 | "target": 3 31 | }, 32 | "42161": { 33 | "shards": 3, 34 | "target": 3 35 | } 36 | }, 37 | "scripts": { 38 | "start": "npm run start:dev", 39 | "start:dev": "nodemon --watch src --watch forta.config.json -e js,json --exec \"forta-agent run\"", 40 | "start:prod": "forta-agent run --prod", 41 | "tx": "forta-agent run --tx", 42 | "block": "forta-agent run --block", 43 | "range": "forta-agent run --range", 44 | "file": "forta-agent run --file", 45 | "publish": "forta-agent publish", 46 | "push": "forta-agent push", 47 | "disable": "forta-agent disable", 48 | "enable": "forta-agent enable", 49 | "keyfile": "forta-agent keyfile", 50 | "test": "jest", 51 | "format": "prettier --write \"src/**/*.js\"" 52 | }, 53 | "dependencies": { 54 | "@types/jest": "^27.4.1", 55 | "bot-alert-rate": "^0.0.4", 56 | "dotenv": "^16.0.3", 57 | "forta-agent": "^0.1.48", 58 | "lru-cache": "^10.0.1", 59 | "node-fetch": "^2.6.7" 60 | }, 61 | "devDependencies": { 62 | "jest": "^29.7.0", 63 | "nodemon": "^2.0.8", 64 | "prettier": "^2.7.1" 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /flashloan-detector/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "flashloan-detection-bot", 3 | "displayName": "Flashloan Detection Bot", 4 | "version": "0.1.8", 5 | "description": "Forta bot that detects if a transaction contains a flashloan where the borrower makes large profit", 6 | "longDescription": "This bot focuses on detecting instances of flashloan utilization, the bot diligently examines each transaction to determine if it aligns with the characteristics of a flashloan. It further analyzes the transaction to assess whether the borrower has generated a substantial profit, using a preset percentage threshold. The bot's function is activated solely in cases where both a flashloan is involved and the borrower's profit meets the specified criteria, providing an objective mechanism for identifying these specific transaction types.", 7 | "repository": "https://github.com/NethermindEth/forta-starter-kits/tree/main/flashloan-detector", 8 | "chainIds": [ 9 | 1, 10 | 10, 11 | 56, 12 | 137, 13 | 250, 14 | 42161, 15 | 43114 16 | ], 17 | "chainSettings": { 18 | "1": { 19 | "shards": 4, 20 | "target": 3 21 | }, 22 | "56": { 23 | "shards": 6, 24 | "target": 3 25 | }, 26 | "default": { 27 | "shards": 3, 28 | "target": 1 29 | } 30 | }, 31 | "scripts": { 32 | "start": "npm run start:dev", 33 | "start:dev": "nodemon --watch src --watch forta.config.json -e js,json --exec \"forta-agent run\"", 34 | "start:prod": "forta-agent run --prod", 35 | "tx": "forta-agent run --tx", 36 | "block": "forta-agent run --block", 37 | "range": "forta-agent run --range", 38 | "file": "forta-agent run --file", 39 | "publish": "forta-agent publish", 40 | "push": "forta-agent push", 41 | "disable": "forta-agent disable", 42 | "enable": "forta-agent enable", 43 | "keyfile": "forta-agent keyfile", 44 | "test": "jest --detectOpenHandles", 45 | "format": "prettier --write \"src/**/*.js\"" 46 | }, 47 | "dependencies": { 48 | "axios": "^0.27.2", 49 | "dotenv": "^16.0.3", 50 | "forta-agent": "^0.1.48", 51 | "forta-agent-tools": "^3.2.6", 52 | "lru-cache": "^10.0.1", 53 | "node-fetch": "^2.6.7" 54 | }, 55 | "devDependencies": { 56 | "jest": "^28.1.1", 57 | "nodemon": "^2.0.16", 58 | "prettier": "^2.7.1" 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /flashloan-detector/src/detectors/uniswap-v3-detector.js: -------------------------------------------------------------------------------- 1 | const { ethers, getEthersProvider } = require("forta-agent"); 2 | 3 | const FLASH_EVENT_ABI = [ 4 | "event Flash(address indexed sender, address indexed recipient, uint256 amount0, uint256 amount1, uint256 paid0, uint256 paid1)", 5 | ]; 6 | const SWAP_ABI = 7 | "function swap(address recipient, bool zeroForOne, int256 amountSpecified, uint160 sqrtPriceLimitX96, bytes data)"; 8 | 9 | const ABI = [ 10 | "function token0() public view returns (address token)", 11 | "function token1() public view returns (address token)", 12 | ]; 13 | 14 | module.exports = { 15 | getUniswapV3Flashloan: async (txEvent) => { 16 | const flashEvents = txEvent.filterLog(FLASH_EVENT_ABI); 17 | const assetSwaps = txEvent.filterFunction(SWAP_ABI); 18 | 19 | const flashloans = await Promise.all( 20 | flashEvents.map(async (flash) => { 21 | const { recipient: flashloanRecipient, amount0, amount1 } = flash.args; 22 | const { address: flashloanAddress } = flash; 23 | let swapRecipient; 24 | 25 | // Get the correct amount and asset address 26 | const tokenIndex = amount0.gt(ethers.constants.Zero) ? 0 : 1; 27 | const amount = tokenIndex === 0 ? amount0 : amount1; 28 | const tokenFnCall = tokenIndex === 0 ? "token0" : "token1"; 29 | 30 | const contract = new ethers.Contract(flashloanAddress, ABI, getEthersProvider()); 31 | const asset = await contract[tokenFnCall](); 32 | 33 | if (assetSwaps.length > 0) { 34 | await Promise.all( 35 | assetSwaps.map(async (swap) => { 36 | const { recipient: swapReceiver, zeroForOne } = swap.args; 37 | const { address: swapAddress } = swap; 38 | 39 | // Check if there was a `swap` in the same 40 | // UniswapV3 Pool in the same txn 41 | if (swapAddress != flashloanAddress) return; 42 | 43 | // Check if the `swap` was swapping OUT of the 44 | // flashloaned token into the pool's other token 45 | if ((tokenIndex === 0 && zeroForOne === true) || (tokenIndex === 1 && zeroForOne === false)) { 46 | swapRecipient = swapReceiver; 47 | } 48 | }) 49 | ); 50 | } 51 | 52 | return { 53 | asset: asset.toLowerCase(), 54 | amount, 55 | account: !swapRecipient ? flashloanRecipient.toLowerCase() : swapRecipient.toLowerCase(), 56 | }; 57 | }) 58 | ); 59 | 60 | return flashloans.filter((f) => !!f); 61 | }, 62 | }; 63 | -------------------------------------------------------------------------------- /transaction-volume-anomaly-detection/README.md: -------------------------------------------------------------------------------- 1 | # Transaction Volume Anomaly Detection 2 | 3 | ## Description 4 | 5 | This bot detects Transactions with Anomalies in Volume 6 | 7 | ## Arima Configuration Settings Description: 8 | 9 | - p - the number of lag observations (probably > 1 because there is a lot of variance) 10 | - d - the number of times that the raw observations are differenced (probably 0) 11 | - q - the size of the moving average window (probably > 1 because there is a lot of variance) 12 | - P - the number of seasonal lag observations (2 because we want to check the last 2 weeks) 13 | - D - the number of seasonal differences (probably 0) 14 | - Q - the number of seasonal moving average window (2 because we want to check the last 2 weeks) 15 | - s - The number of time steps for a single seasonal period (should be selected such that 1 season is 1 week) 16 | 17 | ## Supported Chains 18 | 19 | - Ethereum 20 | - Binance Smart Chain 21 | - Polygon 22 | - Optimism 23 | - Arbitrum 24 | - Avalanche 25 | - Fantom 26 | 27 | ## Alerts 28 | 29 | - SUCCESSFUL-INTERNAL-TRANSACTION-VOL-INCREASE 30 | 31 | - Fired when there is unusually high number of successful internal transactions 32 | - Severity is always set to "low" 33 | - Type is always set to "suspicious" 34 | - Metadata fields: 35 | - COUNT (Current count of successful transaction) 36 | - EXPECTED_BASELINE (Expected baseline count of successful transaction) 37 | 38 | - SUCCESSFUL-TRANSACTION-VOL-INCREASE 39 | 40 | - Fired when there is unusually high number of successful transactions 41 | - Severity is always set to "low" 42 | - Type is always set to "suspicious" 43 | - Metadata fields: 44 | - COUNT (Current count of successful transaction) 45 | - EXPECTED_BASELINE (Expected baseline count of successful transaction) 46 | 47 | - FAILED-TRANSACTION-VOL-INCREASE 48 | 49 | - Fired when there is unusually high number of failed transactions 50 | - Severity is always set to "high" 51 | - Type is always set to "exploit" 52 | - Metadata fields: 53 | - COUNT (Current count of successful transaction) 54 | - EXPECTED_BASELINE (Expected baseline count of successful transaction) 55 | 56 | - FAILED-INTERNAL-TRANSACTION-VOL-INCREASE 57 | 58 | - Fired when there is unusually high number of failed internal transactions 59 | - Severity is always set to "medium" 60 | - Type is always set to "suspicious" 61 | - Metadata fields: 62 | - COUNT (Current count of successful transaction) 63 | - EXPECTED_BASELINE (Expected baseline count of successful transaction) 64 | 65 | ## Test Data 66 | 67 | The agent behaviour can be verified with supplied unit tests 68 | -------------------------------------------------------------------------------- /flashloan-detector/src/persistence.helper.js: -------------------------------------------------------------------------------- 1 | const { fetchJwt } = require("forta-agent"); 2 | const fetch = require("node-fetch"); 3 | const { existsSync, readFileSync, writeFileSync } = require("fs"); 4 | require("dotenv").config(); 5 | 6 | class PersistenceHelper { 7 | databaseUrl; 8 | 9 | constructor(dbUrl) { 10 | this.databaseUrl = dbUrl; 11 | } 12 | 13 | async persist(value, key) { 14 | const hasLocalNode = process.env.hasOwnProperty("LOCAL_NODE"); 15 | if (!hasLocalNode) { 16 | const token = await fetchJwt({}); 17 | const headers = { Authorization: `Bearer ${token}` }; 18 | try { 19 | const response = await fetch(`${this.databaseUrl}${key}`, { 20 | method: "POST", 21 | headers: headers, 22 | body: JSON.stringify(value), 23 | }); 24 | 25 | if (response.ok) { 26 | console.log(`successfully persisted ${value} to database`); 27 | return; 28 | } 29 | } catch (e) { 30 | console.log(`failed to persist ${value} to database. Error: ${e}`); 31 | } 32 | } else { 33 | // Persist locally 34 | writeFileSync(key, value.toString()); 35 | return; 36 | } 37 | } 38 | 39 | async load(key) { 40 | const hasLocalNode = process.env.hasOwnProperty("LOCAL_NODE"); 41 | if (!hasLocalNode) { 42 | const token = await fetchJwt({}); 43 | const headers = { Authorization: `Bearer ${token}` }; 44 | try { 45 | const response = await fetch(`${this.databaseUrl}${key}`, { headers }); 46 | 47 | if (response.ok) { 48 | const data = await response.json(); 49 | const value = parseInt(data); 50 | console.log(`successfully fetched ${value} from database`); 51 | return value; 52 | } else { 53 | console.log(`${key} has no database entry`); 54 | // If this is the first bot instance that is deployed, 55 | // the database will not have data to return, 56 | // thus return zero to assign value to the variables 57 | // necessary 58 | return 0; 59 | } 60 | } catch (e) { 61 | console.log(`Error in fetching data.`); 62 | throw e; 63 | } 64 | } else { 65 | // Checking if it exists locally 66 | if (existsSync(key)) { 67 | const data = readFileSync(key); 68 | return parseInt(data.toString()); 69 | } else { 70 | console.log(`file ${key} does not exist`); 71 | // If this is the first bot instance that is deployed, 72 | // the database will not have data to return, 73 | // thus return zero to assign value to the variables 74 | // necessary 75 | return 0; 76 | } 77 | } 78 | } 79 | } 80 | 81 | module.exports = { 82 | PersistenceHelper, 83 | }; 84 | -------------------------------------------------------------------------------- /large-balance-decrease/src/persistence.helper.js: -------------------------------------------------------------------------------- 1 | const { fetchJwt } = require("forta-agent"); 2 | const fetch = require("node-fetch"); 3 | const { existsSync, readFileSync, writeFileSync } = require("fs"); 4 | require("dotenv").config(); 5 | 6 | class PersistenceHelper { 7 | databaseUrl; 8 | 9 | constructor(dbUrl) { 10 | this.databaseUrl = dbUrl; 11 | } 12 | 13 | async persist(value, key) { 14 | const hasLocalNode = process.env.hasOwnProperty("LOCAL_NODE"); 15 | if (!hasLocalNode) { 16 | const token = await fetchJwt({}); 17 | const headers = { Authorization: `Bearer ${token}` }; 18 | try { 19 | const response = await fetch(`${this.databaseUrl}${key}`, { 20 | method: "POST", 21 | headers: headers, 22 | body: JSON.stringify(value), 23 | }); 24 | 25 | if (response.ok) { 26 | console.log(`successfully persisted ${value} to database`); 27 | return; 28 | } 29 | } catch (e) { 30 | console.log(`failed to persist ${value} to database. Error: ${e}`); 31 | } 32 | } else { 33 | // Persist locally 34 | writeFileSync(key, value.toString()); 35 | return; 36 | } 37 | } 38 | 39 | async load(key) { 40 | const hasLocalNode = process.env.hasOwnProperty("LOCAL_NODE"); 41 | if (!hasLocalNode) { 42 | const token = await fetchJwt({}); 43 | const headers = { Authorization: `Bearer ${token}` }; 44 | try { 45 | const response = await fetch(`${this.databaseUrl}${key}`, { headers }); 46 | 47 | if (response.ok) { 48 | const data = await response.json(); 49 | const value = parseInt(data); 50 | console.log(`successfully fetched ${value} from database`); 51 | return value; 52 | } else { 53 | console.log(`${key} has no database entry`); 54 | // If this is the first bot instance that is deployed, 55 | // the database will not have data to return, 56 | // thus return zero to assign value to the variables 57 | // necessary 58 | return 0; 59 | } 60 | } catch (e) { 61 | console.log(`Error in fetching data.`); 62 | throw e; 63 | } 64 | } else { 65 | // Checking if it exists locally 66 | if (existsSync(key)) { 67 | const data = readFileSync(key); 68 | return parseInt(data.toString()); 69 | } else { 70 | console.log(`file ${key} does not exist`); 71 | // If this is the first bot instance that is deployed, 72 | // the database will not have data to return, 73 | // thus return zero to assign value to the variables 74 | // necessary 75 | return 0; 76 | } 77 | } 78 | } 79 | } 80 | 81 | module.exports = { 82 | PersistenceHelper, 83 | }; 84 | -------------------------------------------------------------------------------- /flashbots-transactions-detector/src/persistence.helper.js: -------------------------------------------------------------------------------- 1 | const { fetchJwt } = require("forta-agent"); 2 | const fetch = require("node-fetch"); 3 | const { existsSync, readFileSync, writeFileSync } = require("fs"); 4 | require("dotenv").config(); 5 | 6 | class PersistenceHelper { 7 | databaseUrl; 8 | 9 | constructor(dbUrl) { 10 | this.databaseUrl = dbUrl; 11 | } 12 | 13 | async persist(value, key) { 14 | const hasLocalNode = process.env.hasOwnProperty("LOCAL_NODE"); 15 | if (!hasLocalNode) { 16 | const token = await fetchJwt({}); 17 | const headers = { Authorization: `Bearer ${token}` }; 18 | try { 19 | const response = await fetch(`${this.databaseUrl}${key}`, { 20 | method: "POST", 21 | headers: headers, 22 | body: JSON.stringify(value), 23 | }); 24 | 25 | if (response.ok) { 26 | console.log(`successfully persisted ${value} to database`); 27 | return; 28 | } 29 | } catch (e) { 30 | console.log(`failed to persist ${value} to database. Error: ${e}`); 31 | } 32 | } else { 33 | // Persist locally 34 | writeFileSync(key, value.toString()); 35 | return; 36 | } 37 | } 38 | 39 | async load(key) { 40 | const hasLocalNode = process.env.hasOwnProperty("LOCAL_NODE"); 41 | if (!hasLocalNode) { 42 | const token = await fetchJwt({}); 43 | const headers = { Authorization: `Bearer ${token}` }; 44 | try { 45 | const response = await fetch(`${this.databaseUrl}${key}`, { headers }); 46 | 47 | if (response.ok) { 48 | const data = await response.json(); 49 | const value = parseInt(data); 50 | console.log(`successfully fetched ${value} from database`); 51 | return value; 52 | } else { 53 | console.log(`${key} has no database entry`); 54 | // If this is the first bot instance that is deployed, 55 | // the database will not have data to return, 56 | // thus return zero to assign value to the variables 57 | // necessary 58 | return 0; 59 | } 60 | } catch (e) { 61 | console.log(`Error in fetching data.`); 62 | throw e; 63 | } 64 | } else { 65 | // Checking if it exists locally 66 | if (existsSync(key)) { 67 | const data = readFileSync(key); 68 | return parseInt(data.toString()); 69 | } else { 70 | console.log(`file ${key} does not exist`); 71 | // If this is the first bot instance that is deployed, 72 | // the database will not have data to return, 73 | // thus return zero to assign value to the variables 74 | // necessary 75 | return 0; 76 | } 77 | } 78 | } 79 | } 80 | 81 | module.exports = { 82 | PersistenceHelper, 83 | }; 84 | -------------------------------------------------------------------------------- /forta-tornado-cash-starter-kit/README.md: -------------------------------------------------------------------------------- 1 | # Tornado Cash funded account interacted with contract 2 | 3 | ## Description 4 | 5 | This bot detects when an account that was funded by Tornado Cash interacts with any (non-Tornado Cash) contract 6 | 7 | ## Supported Chains 8 | 9 | - Ethereum 10 | - BNB Smart Chain 11 | - Optimism 12 | - Polygon 13 | - Arbitrum 14 | 15 | ## Alerts 16 | 17 | - TORNADO-CASH-FUNDED-ACCOUNT-INTERACTION 18 | - Fired when a transaction contains contract interactions from a Tornado Cash funded account 19 | - Severity is always set to "low" 20 | - Type is always set to "suspicious" 21 | - Metadata: 22 | - `anomalyScore` - score of how anomalous the alert is (0-1) 23 | - Score calculated by finding amount of TORNADO-CASH-FUNDED-ACCOUNT-INTERACTION transactions out of the total number of contract interactions processed by this bot. 24 | - Note: score differs based on chain. 25 | - Labels: 26 | - Label 1: 27 | - `entity`: The Tornado Cash funded account's address 28 | - `entityType`: The type of the entity, always set to "Address" 29 | - `label`: The type of the label, always set to "Attacker" 30 | - `confidence`: The confidence level of the address being an attacker (0-1). Always set to 0.7. 31 | - Label 2: 32 | - `entity`: The transaction hash of the Tornado Cash funded account contract interaction 33 | - `entityType`: The type of the entity, always set to "Transaction" 34 | - `label`: The type of the label, always set to "Suspicious" 35 | - `confidence`: The confidence level of the transaction being suspicious (0-1). Always set to 0.7. 36 | 37 | ## Test Data 38 | 39 | The bot behaviour can be verified with the following transactions: 40 | 41 | - [0x1ec1d8e073fa2476799e48a6a33b272aa28431261598b4fc93f63c58aae571f2](https://etherscan.io/tx/0x1ec1d8e073fa2476799e48a6a33b272aa28431261598b4fc93f63c58aae571f2) (returns contract interactions from tx to now, Ethereum) 42 | - [0x8c5e39abbbecefcd80684a8a241f5e11e2506bf73166ed473d666fb367c10f0d](https://bscscan.com/tx/0x8c5e39abbbecefcd80684a8a241f5e11e2506bf73166ed473d666fb367c10f0d) (returns contract interactions from tx to now, BNB Smart Chain) 43 | - [0x458ccb3fedf31e3423c20647a089f20402a43310667d1896e6b4eff42f46f38c](https://optimistic.etherscan.io/tx/0x458ccb3fedf31e3423c20647a089f20402a43310667d1896e6b4eff42f46f38c) (returns contract interactions from tx to now, Optimism) 44 | - [0x269ab0c4b30eede3c3d64e4b4df641657b89c18db49c3fbd5ee1ead7fa21f146](https://polygonscan.com/tx/0x269ab0c4b30eede3c3d64e4b4df641657b89c18db49c3fbd5ee1ead7fa21f146) (returns contract interactions from tx to now, Polygon) 45 | - [0xc82b4890610b487cffb27bf93ae2a904fb391cb8dc2dd5bad1e300e81cab443e](https://arbiscan.io/tx/0xc82b4890610b487cffb27bf93ae2a904fb391cb8dc2dd5bad1e300e81cab443e) (returns contract interactions from tx to now, Arbitrum) 46 | 47 | This bot behaviour can be verified with the following block range: 48 | 49 | - 14747739..14747745 (Ethereum) 50 | -------------------------------------------------------------------------------- /drastic-price-change-template/src/helper.js: -------------------------------------------------------------------------------- 1 | const { getEthersProvider, ethers } = require('forta-agent'); 2 | const { default: axios } = require('axios'); 3 | 4 | const CHAINLINK_ABI = [ 5 | 'function decimals() external view returns (uint8)', 6 | 'function latestAnswer() public view returns (int256 answer)', 7 | ]; 8 | 9 | const UNISWAP_ABI = [ 10 | 'function observe(uint32[] calldata secondsAgos) external view returns (int56[] memory tickCumulatives, uint160[] memory secondsPerLiquidityCumulativeX128s)', 11 | 'function token0() public view returns (address token)', 12 | 'function token1() public view returns (address token)', 13 | ]; 14 | 15 | const TOKEN_ABI = ['function decimals() external view returns (uint8)']; 16 | 17 | async function getChainlinkPrice(contract, decimals) { 18 | const answer = await contract.latestAnswer(); 19 | const price = ethers.utils.formatUnits(answer, decimals); 20 | return parseFloat(price); 21 | } 22 | 23 | async function getUniswapPrice(contract, exponent, interval) { 24 | // return null if uniswap contract is not provided 25 | if (!contract) return null; 26 | 27 | // Sometimes the observe function fails with "OLD". In this case return null 28 | try { 29 | // Get the time-weighted average price for a interval 30 | const [tickCumulatives] = await contract.observe([interval, 0]); 31 | 32 | const tick = (tickCumulatives[1] - tickCumulatives[0]) / interval; 33 | const price = (1.0001 ** tick) * (10 ** exponent); 34 | return price; 35 | } catch { 36 | return null; 37 | } 38 | } 39 | 40 | async function getCoingeckoPrice(id) { 41 | const url = `https://api.coingecko.com/api/v3/simple/price?ids=${id}&vs_currencies=usd`; 42 | const response = await axios.get(url); 43 | return response.data[id].usd; 44 | } 45 | 46 | function getChainlinkContract(address) { 47 | return new ethers.Contract(address, CHAINLINK_ABI, getEthersProvider()); 48 | } 49 | 50 | // Returns the contract and the decimals diff between the 2 tokens 51 | async function getUniswapParams(address) { 52 | const contract = new ethers.Contract(address, UNISWAP_ABI, getEthersProvider()); 53 | 54 | const token0 = await contract.token0(); 55 | const token1 = await contract.token1(); 56 | 57 | const token0Contract = new ethers.Contract(token0, TOKEN_ABI, getEthersProvider()); 58 | const token1Contract = new ethers.Contract(token1, TOKEN_ABI, getEthersProvider()); 59 | 60 | const token0Decimals = await token0Contract.decimals(); 61 | const token1Decimals = await token1Contract.decimals(); 62 | 63 | const exponent = token0Decimals - token1Decimals; 64 | 65 | return [contract, exponent]; 66 | } 67 | 68 | function calculatePercentage(price1, price2) { 69 | const percentage = (1 - (price1 / price2)) * 100; 70 | return +Math.abs(percentage).toFixed(2); 71 | } 72 | 73 | module.exports = { 74 | getChainlinkPrice, 75 | getUniswapPrice, 76 | getCoingeckoPrice, 77 | getChainlinkContract, 78 | getUniswapParams, 79 | calculatePercentage, 80 | }; 81 | -------------------------------------------------------------------------------- /ice-phishing/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "forta-ice-phishing-starter-kit", 3 | "version": "0.1.11", 4 | "displayName": "Ice Phishing Detection Bot", 5 | "description": "This bot detects if an account gains high number of approvals and if it transfers the approved funds", 6 | "longDescription": "The bot's functionality revolves around identifying specific transaction patterns involving accounts or contracts. It detects instances where an account, whether an externally owned account (EOA) with a low nonce or an unverified contract with a low transaction count, garners a unusually high number of approvals or ERC20 permissions, subsequently transferring the approved funds. The bot extends its analysis to EOAs with high nonces or verified contracts with limited transactions, emitting alerts of lower severity. Moreover, it scrutinizes the involvement of accounts or contracts flagged in the ScamSniffer database or identified by the Malicious Smart Contract ML Bot v2 alert, particularly in Approval, Transfer, or permit transactions. The bot's multifaceted approach contributes to a comprehensive monitoring system within the blockchain ecosystem, aimed at identifying potential anomalies or risks associated with transactional activities.", 7 | "repository": "https://github.com/NethermindEth/forta-starter-kits/tree/main/ice-phishing", 8 | "chainIds": [ 9 | 1, 10 | 10, 11 | 56, 12 | 137, 13 | 42161, 14 | 43114 15 | ], 16 | "chainSettings": { 17 | "1": { 18 | "shards": 14, 19 | "target": 3 20 | }, 21 | "56": { 22 | "shards": 19, 23 | "target": 3 24 | }, 25 | "137": { 26 | "shards": 20, 27 | "target": 3 28 | }, 29 | "10": { 30 | "shards": 8, 31 | "target": 3 32 | }, 33 | "43114": { 34 | "shards": 5, 35 | "target": 3 36 | }, 37 | "default": { 38 | "shards": 7, 39 | "target": 3 40 | } 41 | }, 42 | "scripts": { 43 | "start": "npm run start:dev", 44 | "start:dev": "nodemon --watch src --watch forta.config.json -e js,json --exec \"forta-agent run\"", 45 | "start:prod": "forta-agent run --prod", 46 | "tx": "forta-agent run --tx", 47 | "block": "forta-agent run --block", 48 | "range": "forta-agent run --range", 49 | "file": "forta-agent run --file", 50 | "publish": "forta-agent publish", 51 | "info": "forta-agent info", 52 | "logs": "forta-agent logs", 53 | "push": "forta-agent push", 54 | "disable": "forta-agent disable", 55 | "enable": "forta-agent enable", 56 | "keyfile": "forta-agent keyfile", 57 | "test": "jest --detectOpenHandles", 58 | "format": "prettier --write \"src/**/*.js\"" 59 | }, 60 | "dependencies": { 61 | "axios": "^0.27.2", 62 | "forta-agent": "^0.1.48", 63 | "bot-alert-rate": "^0.0.4", 64 | "lru-cache": "^7.13.1", 65 | "node-fetch": "^2.6.7", 66 | "dotenv": "^16.0.3" 67 | }, 68 | "devDependencies": { 69 | "jest": "^28.1.3", 70 | "nodemon": "^2.0.22", 71 | "prettier": "^2.7.1", 72 | "forta-agent-tools": "^3.1.1" 73 | }, 74 | "overrides": { 75 | "semver": "7.5.2" 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /forta-tornado-cash-starter-kit/src/helper.js: -------------------------------------------------------------------------------- 1 | const tornadoCashAddressesETHER = [ 2 | "0x12D66f87A04A9E220743712cE6d9bB1B5616B8Fc", // 0.1 ETH 3 | "0x47CE0C6eD5B0Ce3d3A51fdb1C52DC66a7c3c2936", // 1 ETH 4 | "0xD4B88Df4D29F5CedD6857912842cff3b20C8Cfa3", //100 DAI 5 | "0xFD8610d20aA15b7B2E3Be39B396a1bC3516c7144", //1000 DAI 6 | "0x07687e702b410Fa43f4cB4Af7FA097918ffD2730", //10_000 DAI 7 | "0x23773E65ed146A459791799d01336DB287f25334", // 100_000 DAI 8 | "0x22aaA7720ddd5388A3c0A3333430953C68f1849b", // 5000 cDAI 9 | "0x03893a7c7463AE47D46bc7f091665f1893656003", // 50_000 cDAI 10 | "0x2717c5e28cf931547B621a5dddb772Ab6A35B701", // 500_000 cDAI 11 | "0xD21be7248e0197Ee08E0c20D4a96DEBdaC3D20Af", // 5_000_000 cDAI 12 | "0x4736dCf1b7A3d580672CcE6E7c65cd5cc9cFBa9D", // 100 USDC 13 | "0xd96f2B1c14Db8458374d9Aca76E26c3D18364307", // 1000 USDC 14 | "0x169AD27A470D064DEDE56a2D3ff727986b15D52B", // 100 USDT 15 | "0x0836222F2B2B24A3F36f98668Ed8F0B38D1a872f", // 1000 USDT 16 | "0x178169B423a011fff22B9e3F3abeA13414dDD0F1", // 0.1 WBTC 17 | "0xbB93e510BbCD0B7beb5A853875f9eC60275CF498", // 10 WBTC 18 | ]; 19 | 20 | const tornadoCashAddressesBSC = [ 21 | "0x84443cfd09a48af6ef360c6976c5392ac5023a1f", // 0.1 BNB 22 | "0xd47438c816c9e7f2e2888e060936a499af9582b3", // 1 BNB 23 | "0x330bdFADE01eE9bF63C209Ee33102DD334618e0a", // 10 BNB 24 | "0x1e34a77868e19a6647b1f2f47b51ed72dede95dd", // 100 BNB 25 | ]; 26 | 27 | const tornadoCashAddressesOPTIMISM = [ 28 | "0x84443CFd09A48AF6eF360C6976C5392aC5023a1F", // 0.1 ETH 29 | "0xd47438C816c9E7f2E2888E060936a499Af9582b3", // 1 ETH 30 | ]; 31 | 32 | const tornadoCashAddressesPolygon = [ 33 | "0x1E34A77868E19A6647b1f2F47B51ed72dEDE95DD", // 100 MATIC 34 | "0xdf231d99Ff8b6c6CBF4E9B9a945CBAcEF9339178", // 1000 MATIC 35 | "0xaf4c0B70B2Ea9FB7487C7CbB37aDa259579fe040", // 10_000 MATIC 36 | "0xa5C2254e4253490C54cef0a4347fddb8f75A4998", // 100_000 MATIC 37 | ]; 38 | 39 | const tornadoCashAddressesARBITRUM = [ 40 | "0x84443CFd09A48AF6eF360C6976C5392aC5023a1F", // 0.1 ETH 41 | "0xd47438C816c9E7f2E2888E060936a499Af9582b3", // 1 ETH 42 | ]; 43 | 44 | module.exports = { 45 | eventABI: "event Withdrawal(address to, bytes32 nullifierHash, address indexed relayer, uint256 fee)", 46 | addressLimit: 100000, 47 | getContractsByChainId: (chainId) => { 48 | switch (chainId) { 49 | case 1: 50 | return tornadoCashAddressesETHER; 51 | case 56: 52 | return tornadoCashAddressesBSC; 53 | case 10: 54 | return tornadoCashAddressesOPTIMISM; 55 | case 137: 56 | return tornadoCashAddressesPolygon; 57 | case 42161: 58 | return tornadoCashAddressesARBITRUM; 59 | } 60 | }, 61 | getInitialFundedByTornadoCash: (chainId) => { 62 | switch (chainId) { 63 | case 1: 64 | return new Set(["0x58f970044273705ab3b0e87828e71123a7f95c9d"]); 65 | case 56: 66 | return new Set(["0x0f3470ed99f835c353be12ce0f82f68c1cf8e411"]); 67 | case 10: 68 | return new Set(["0x933ea7bd9de556dcaa85b775f67afe4ebd3591d4"]); 69 | case 137: 70 | return new Set(["0x08a83ca9fd882e1ed1477927dee00c2e50320a0a"]); 71 | case 42161: 72 | return new Set(["0x543c25dc5e3154fabede4d4a669312f187d56383"]); 73 | } 74 | }, 75 | }; 76 | -------------------------------------------------------------------------------- /ice-phishing/src/config.js: -------------------------------------------------------------------------------- 1 | const etherscanApis = { 2 | 1: { 3 | urlContract: "https://api.etherscan.io/api?module=contract&action=getabi", 4 | urlAccount: "https://api.etherscan.io/api?module=account&action=txlist", 5 | urlAccountToken: "https://api.etherscan.io/api?module=account&action=tokentx", 6 | urlContractCreation: "https://api.etherscan.io/api?module=contract&action=getcontractcreation", 7 | urlLogs: "https://api.etherscan.io/api?module=logs&action=getLogs", 8 | }, 9 | 10: { 10 | urlContract: "https://api-optimistic.etherscan.io/api?module=contract&action=getabi", 11 | urlAccount: "https://api-optimistic.etherscan.io/api?module=account&action=txlist", 12 | urlAccountToken: "https://api-optimistic.etherscan.io/api?module=account&action=tokentx", 13 | urlContractCreation: "https://api-optimistic.etherscan.io/api?module=contract&action=getcontractcreation", 14 | urlLogs: "https://api-optimistic.etherscan.io/api?module=logs&action=getLogs", 15 | }, 16 | 56: { 17 | urlContract: "https://api.bscscan.com/api?module=contract&action=getabi", 18 | urlAccount: "https://api.bscscan.com/api?module=account&action=txlist", 19 | urlAccountToken: "https://api.bscscan.com/api?module=account&action=tokentx", 20 | urlContractCreation: "https://api.bscscan.com/api?module=contract&action=getcontractcreation", 21 | urlLogs: "https://api.bscscan.com/api?module=logs&action=getLogs", 22 | }, 23 | 137: { 24 | urlContract: "https://api.polygonscan.com/api?module=contract&action=getabi", 25 | urlAccount: "https://api.polygonscan.com/api?module=account&action=txlist", 26 | urlAccountToken: "https://api.polygonscan.com/api?module=account&action=tokentx", 27 | urlContractCreation: "https://api.polygonscan.com/api?module=contract&action=getcontractcreation", 28 | urlLogs: "https://api.polygonscan.com/api?module=logs&action=getLogs", 29 | }, 30 | 250: { 31 | urlContract: "https://api.ftmscan.com/api?module=contract&action=getabi", 32 | urlAccount: "https://api.ftmscan.com/api?module=account&action=txlist", 33 | urlAccountToken: "https://api.ftmscan.com/api?module=account&action=tokentx", 34 | urlContractCreation: "https://api.ftmscan.com/api?module=contract&action=getcontractcreation", 35 | urlLogs: "https://api.ftmscan.com/api?module=logs&action=getLogs", 36 | }, 37 | 42161: { 38 | urlContract: "https://api.arbiscan.io/api?module=contract&action=getabi", 39 | urlAccount: "https://api.arbiscan.io/api?module=account&action=txlist", 40 | urlAccountToken: "https://api.arbiscan.io/api?module=account&action=tokentx", 41 | urlContractCreation: "https://api.arbiscan.io/api?module=contract&action=getcontractcreation", 42 | urlLogs: "https://api.arbiscan.io/api?module=logs&action=getLogs", 43 | }, 44 | 43114: { 45 | urlContract: "https://api.snowtrace.io/api?module=contract&action=getabi", 46 | urlAccount: "https://api.snowtrace.io/api?module=account&action=txlist", 47 | urlAccountToken: "https://api.snowtrace.io/api?module=account&action=tokentx", 48 | urlContractCreation: "https://api.snowtrace.io/api?module=contract&action=getcontractcreation", 49 | urlLogs: "https://api.snowtrace.io/api?module=logs&action=getLogs", 50 | }, 51 | }; 52 | 53 | module.exports = { 54 | etherscanApis, 55 | }; 56 | -------------------------------------------------------------------------------- /flashloan-detector/src/detectors/euler-detector.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-plusplus */ 2 | /* eslint-disable no-bitwise */ 3 | const { ethers } = require("forta-agent"); 4 | 5 | const eulerEventSigs = [ 6 | "event Borrow(address indexed underlying, address indexed account, uint amount)", 7 | "event Repay(address indexed underlying, address indexed account, uint amount)", 8 | "event RequestBorrow(address indexed account, uint amount)", 9 | ]; 10 | 11 | const zero = ethers.constants.Zero; 12 | 13 | // Generate unique id from the protocol, asset and account 14 | function hashCode(protocol, asset, account) { 15 | const str = protocol + asset + account; 16 | let hash = 0; 17 | if (str.length === 0) return hash; 18 | for (let i = 0; i < str.length; i++) { 19 | const char = str.charCodeAt(i); 20 | hash = (hash << 5) - hash + char; 21 | hash &= hash; // Convert to 32bit integer 22 | } 23 | return hash; 24 | } 25 | 26 | module.exports = { 27 | getEulerFlashloan: (txEvent) => { 28 | const flashloans = []; 29 | // Euler doesn't support flashloans natively so we have to sum 30 | // the borrows and the repays in a tx. If they are equal then 31 | // the transaction probably contains flashloan 32 | const events = txEvent.filterLog(eulerEventSigs); 33 | if (events.length === 0) return flashloans; 34 | 35 | // Store the borrowed and repayed amount for each protocol 36 | // and each underlying asset seperately to prevent false positives. 37 | // e.g. user flashloans 100 USDC from Euler and repays a loan on a Euler fork. 38 | const markets = {}; 39 | 40 | events.forEach((event) => { 41 | const { address, name } = event; 42 | const { underlying, amount, account } = event.args; 43 | 44 | // We process the RequestBorrow events later 45 | if (name === "RequestBorrow") return; 46 | 47 | const id = hashCode(address, underlying, account); 48 | 49 | if (!markets[id]) { 50 | markets[id] = { 51 | address, 52 | underlying, 53 | account, 54 | deposited: zero, 55 | withdrawn: zero, 56 | }; 57 | } 58 | 59 | if (name === "Borrow") { 60 | markets[id].withdrawn = markets[id].withdrawn.add(amount); 61 | } else { 62 | markets[id].deposited = markets[id].deposited.add(amount); 63 | } 64 | }); 65 | 66 | Object.values(markets).forEach((market) => { 67 | const { address, underlying, account, deposited, withdrawn } = market; 68 | 69 | // The Borrow event always return the amount with 18 decimals which leads 70 | // to wrong usd profits so we need to get the corresponding RequestBorrow 71 | // borrow event and calculate the decimal difference 72 | const amount = events 73 | .filter((event) => event.name === "RequestBorrow") 74 | .filter((event) => event.args.account === account && event.address === address) 75 | .map((event) => event.args.amount) 76 | .filter((a) => withdrawn.toString().startsWith(a.toString()))[0]; 77 | 78 | if (!amount) return; 79 | 80 | const decimalsDiff = withdrawn.div(amount); 81 | if (deposited.eq(withdrawn)) { 82 | flashloans.push({ 83 | asset: underlying.toLowerCase(), 84 | amount: withdrawn.div(decimalsDiff), 85 | account: account.toLowerCase(), 86 | }); 87 | } 88 | }); 89 | // Check if the balance difference for a market is equal to 0 90 | return flashloans; 91 | }, 92 | }; 93 | -------------------------------------------------------------------------------- /flashloan-detector/README.md: -------------------------------------------------------------------------------- 1 | # Flashloan Detection Bot 2 | 3 | ## Description 4 | 5 | This bot detects if a transaction contains a flashloan and the borrower made significant profit. The percentage threshold is set to 2%. 6 | 7 | ## Supported Chains 8 | 9 | - Ethereum 10 | - Optimism 11 | - Binance Smart Chain 12 | - Polygon 13 | - Fantom 14 | - Arbitrum 15 | - Avalanche 16 | 17 | ## Alerts 18 | 19 | Describe each of the type of alerts fired by this agent 20 | 21 | - FLASHLOAN-ATTACK 22 | 23 | - Fired when a transaction contains a flashoan and the borrower made significant profit 24 | - Severity is always set to "low" 25 | - Type is always set to "exploit" 26 | - Metadata: 27 | - `profit` - profit made from the flashloan 28 | - `tokens` - array of all tokens involved in the transaction 29 | - `anomalyScore` - score of how anomalous the alert is (0-1) 30 | - Score calculated by finding amount of `FLASHLOAN-ATTACK` transactions out of the total number of flashloans processed by this bot. 31 | - Note: score differs based on chain. 32 | - Labels 33 | - Label 1: 34 | - `entityType`: The type of the entity, always set to "Address" 35 | - `entity`: The attacker's address 36 | - `label`: The type of the label, always set to "Attacker" 37 | - `confidence`: The confidence level of the address being an attacker (0-1). Always set to `0.6`. 38 | - Label 2: 39 | - `entityType`: The type of the entity, always set to "Transaction" 40 | - `entity`: The transaction hash 41 | - `label`: The type of the label, always set to "Exploit" 42 | - `confidence`: The confidence level of the transaction being an exploit (0-1). Always set to `0.6`. 43 | 44 | - FLASHLOAN-ATTACK-WITH-HIGH-PROFIT 45 | - Fired when a transaction contains a flashoan and the borrower made significant profit 46 | - Severity is always set to "high" 47 | - Type is always set to "exploit" 48 | - Metadata: 49 | - `profit` - profit made from the flashloan 50 | - `tokens` - array of all tokens involved in the transaction 51 | - `anomalyScore` - score of how anomalous the alert is (0-1) 52 | - Score calculated by finding amount of `FLASHLOAN-ATTACK-WITH-HIGH-PROFIT` transactions out of the total number of flashloans processed by this bot. 53 | - Note: score differs based on chain. 54 | - Labels 55 | - Label 1: 56 | - `entityType`: The type of the entity, always set to "Address" 57 | - `entity`: The attacker's address 58 | - `label`: The type of the label, always set to "Attacker" 59 | - `confidence`: The confidence level of the address being an attacker (0-1). Always set to `0.9`. 60 | - Label 2: 61 | - `entityType`: The type of the entity, always set to "Transaction" 62 | - `entity`: The transaction hash 63 | - `label`: The type of the label, always set to "Exploit" 64 | - `confidence`: The confidence level of the transaction being an exploit (0-1). Always set to `0.9`. 65 | 66 | ## Test Data 67 | 68 | The bot behaviour can be verified with the following transactions: 69 | 70 | - [0xe7e0474793aad11875c131ebd7582c8b73499dd3c5a473b59e6762d4e373d7b8](https://etherscan.io/tx/0xe7e0474793aad11875c131ebd7582c8b73499dd3c5a473b59e6762d4e373d7b8) (SaddleFinance exploit) 71 | - [0x47c7ab4a9e829415322c8933cf17261cd666dbeb875f0d559ca2785d21cae661](https://etherscan.io/tx/0x47c7ab4a9e829415322c8933cf17261cd666dbeb875f0d559ca2785d21cae661) (Curve Finance exploit) 72 | - To test this exploit transaction, lower `PERCENTAGE_THRESHOLD` in `agent.js` (L12) to `1.75`, as currently the transaction does not clear the percentage threshold of `2`. 73 | -------------------------------------------------------------------------------- /flashloan-detector/src/detectors/dydx-detector.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-await-in-loop */ 2 | /* eslint-disable no-restricted-syntax */ 3 | /* eslint-disable no-plusplus */ 4 | /* eslint-disable no-bitwise */ 5 | const { ethers, getEthersProvider } = require("forta-agent"); 6 | 7 | const dydxSoloMarginAddress = "0x1e0447b19bb6ecfdae1e4ae1694b0c3659614e4e"; 8 | const dydxEventSigs = [ 9 | "event LogDeposit(address indexed accountOwner, uint256 accountNumber, uint256 market, ((bool sign, uint256 value) deltaWei, tuple(bool sign, uint128 value) newPar) update, address from)", 10 | "event LogWithdraw(address indexed accountOwner, uint256 accountNumber, uint256 market, ((bool sign, uint256 value) deltaWei, tuple(bool sign, uint128 value) newPar) update, address from)", 11 | ]; 12 | 13 | const ABI = [ 14 | "function getMarket(uint256 marketId) public view returns (tuple(address token, tuple(uint128 borrow, uint128 supply) totalPar, tuple(uint96 borrow, uint96 supply, uint32 lastUpdate) index, address priceOracle, address interestSetter, tuple(uint256 value) marginPremium, tuple(uint256 value) spreadPremium, bool isClosing) memory)", 15 | ]; 16 | 17 | const zero = ethers.constants.Zero; 18 | const two = ethers.BigNumber.from(2); 19 | 20 | function hashCode(protocol, asset, account) { 21 | const str = protocol + asset + account; 22 | let hash = 0; 23 | if (str.length === 0) return hash; 24 | for (let i = 0; i < str.length; i++) { 25 | const char = str.charCodeAt(i); 26 | hash = (hash << 5) - hash + char; 27 | hash &= hash; // Convert to 32bit integer 28 | } 29 | return hash; 30 | } 31 | 32 | // DyDx doesn't natively support "flashloan" feature. 33 | // However you can achieve a similar behavior by executing a series of operations on 34 | // the SoloMargin contract. In order to mimic an Aave flashloan on DyDx, you would need to: 35 | // 1. Borrow x amount of tokens. (Withdraw) 36 | // 2. Call a function (i.e. Logic to handle flashloaned funds). (Call) 37 | // 3. Deposit back x (+2 wei) amount of tokens. (Deposit) 38 | module.exports = { 39 | getDydxFlashloan: async (txEvent) => { 40 | // Store the balance difference for each market seperately 41 | const markets = {}; 42 | 43 | const events = txEvent.filterLog(dydxEventSigs, dydxSoloMarginAddress); 44 | 45 | // Increase the balanceDiff for the specific market on every deposit 46 | // and decrease it on every withdraw 47 | for (const event of events) { 48 | const market = event.args.market.toNumber(); 49 | const { address } = event; 50 | const { accountOwner, from } = event.args; 51 | const { value, sign } = event.args.update.deltaWei; 52 | 53 | const id = hashCode(address, market, accountOwner); 54 | 55 | if (!markets[id]) { 56 | const contract = new ethers.Contract(address, ABI, getEthersProvider()); 57 | const [asset] = await contract.getMarket(market); 58 | 59 | markets[id] = { 60 | asset, 61 | account: from, 62 | deposited: zero, 63 | withdrawn: zero, 64 | }; 65 | } 66 | 67 | if (sign) { 68 | markets[id].deposited = markets[id].deposited.add(value); 69 | } else { 70 | markets[id].withdrawn = markets[id].withdrawn.add(value); 71 | } 72 | } 73 | const flashloans = []; 74 | 75 | // For each market check if deposited - withdrawn is equal to 2 76 | Object.values(markets).forEach((market) => { 77 | const { asset, account, deposited, withdrawn } = market; 78 | 79 | if (deposited.sub(withdrawn).eq(two)) { 80 | flashloans.push({ 81 | asset: asset.toLowerCase(), 82 | amount: withdrawn, 83 | account: account.toLowerCase(), 84 | }); 85 | } 86 | }); 87 | return flashloans; 88 | }, 89 | }; 90 | -------------------------------------------------------------------------------- /asset-drained/src/agent-performance.spec.js: -------------------------------------------------------------------------------- 1 | const { createBlockEvent, createTransactionEvent, getEthersProvider } = require("forta-agent"); 2 | const { default: calculateAlertRate } = require("bot-alert-rate"); 3 | 4 | const { provideInitialize, provideHandleTransaction, provideHandleBlock } = require("./agent"); 5 | const { getValueInUsd, getTotalSupply } = require("./helper"); 6 | jest.setTimeout(350000); 7 | 8 | describe("Asset drained bot performance test", () => { 9 | it("tests performance", async () => { 10 | initialize = provideInitialize(getEthersProvider()); 11 | handleBlock = provideHandleBlock(calculateAlertRate, getValueInUsd, getTotalSupply); 12 | handleTransaction = provideHandleTransaction(); 13 | await initialize(); 14 | 15 | const blocksToRun = 10; 16 | let totalProcessingTime = 0; 17 | const startingBlock = 17278834; 18 | // Chain: Blocktime 19 | // Ethereum: 12s, 20 | // BSC: 3s, 21 | // Polygon: 2s, 22 | // Arbitrum: 1s, 23 | // Optimism: 2s, 24 | // Fantom: 1s 25 | // Avalanche: 24s 26 | 27 | // Chain: Avg block processing time [choosing the slowest avg time observed] 28 | // Ethereum: 2803.19ms (starting block 17278834) 29 | // BSC: 3584.50ms (starting block 28287306) 30 | // Polygon: 3694.08 (starting block 42811000) 31 | // Arbitrum: 676.20ms (starting block 91594904) 32 | // Optimism: 504.36ms (starting block 794634) 33 | // Fantom: 978.47ms (starting block 62507523) 34 | // Avalanche: 1429.75ms (starting block 30134274) 35 | 36 | // which results in the following sharding config: 37 | // Ethereum - 1 38 | // BSC - 2 39 | // Polygon - 2 40 | // Arbitrum - 1 41 | // Optimism - 1 42 | // Fantom - 2 (Avg Time close to Block Time so 2 shards) 43 | 44 | for (let i = 0; i < blocksToRun; i++) { 45 | const block = await getEthersProvider().getBlock(startingBlock + i); 46 | const maxRetries = 2; 47 | 48 | const txReceipts = await Promise.all( 49 | block.transactions.map(async (hash) => { 50 | let retries = 0; 51 | let receipt; 52 | 53 | while (retries < maxRetries) { 54 | try { 55 | receipt = await getEthersProvider().getTransactionReceipt(hash); 56 | break; // If successful, exit the retry loop 57 | } catch (error) { 58 | console.log(`Error fetching receipt for transaction ${hash}. Retrying...`); 59 | retries++; 60 | } 61 | } 62 | 63 | return receipt; // Return the receipt (or undefined if retries exhausted) 64 | }) 65 | ); 66 | const nextBlock = await getEthersProvider().getBlock(startingBlock + i + 1); 67 | const nextBlockEvent = createBlockEvent({ 68 | block: nextBlock, 69 | }); 70 | await Promise.all( 71 | txReceipts.map(async (txReceipt) => { 72 | const txEvent = createTransactionEvent({ 73 | transaction: { 74 | hash: txReceipt.transactionHash, 75 | from: txReceipt.from, 76 | to: txReceipt.to, 77 | }, 78 | block: { 79 | number: txReceipt.blockNumber, 80 | }, 81 | logs: txReceipt.logs, 82 | }); 83 | await handleTransaction(txEvent); 84 | }) 85 | ); 86 | const startTime = performance.now(); 87 | await handleBlock(nextBlockEvent); 88 | const endTime = performance.now(); 89 | totalProcessingTime += endTime - startTime; 90 | } 91 | const processingTimeAvgMs = totalProcessingTime / blocksToRun; 92 | console.log(`Avg processing time: ${processingTimeAvgMs}ms`); 93 | }); 94 | }); 95 | -------------------------------------------------------------------------------- /transaction-volume-anomaly-detection-template/src/agent.config.js: -------------------------------------------------------------------------------- 1 | const config = require("./bot.config.json"); 2 | 3 | module.exports = { 4 | bucketBlockSize: 1000, 5 | getMinBucketBlockSizeByChainId: (chainId) => { 6 | switch (chainId) { 7 | case 1: 8 | return 1000; 9 | case 56: 10 | return 5000; 11 | case 137: 12 | return 6000; 13 | case 43114: 14 | return 5000; 15 | case 10: 16 | return 1200; 17 | case 42161: 18 | return 1000; 19 | case 250: 20 | return 12000; 21 | } 22 | }, 23 | globalSensitivity: 1, // Default is 1, Greater than 1 will increase detections, lower than 1 will decrease detections 24 | getContractsByChainId: (chainId) => { 25 | switch (chainId) { 26 | case 1: 27 | let addressesForEth = []; 28 | config.bots.map((b) => { 29 | const objectValues = Object.values(b.contracts); 30 | const objectsFiltered = objectValues.filter((o) => o.chainId == 1); 31 | addressesForEth = objectsFiltered; 32 | }); 33 | return addressesForEth; 34 | case 56: 35 | let addressesForBSC = []; 36 | config.bots.map((b) => { 37 | const objectValues = Object.values(b.contracts); 38 | const objectsFiltered = objectValues.filter((o) => o.chainId == 56); 39 | addressesForBSC = objectsFiltered; 40 | }); 41 | return addressesForBSC; 42 | case 10: 43 | let addressesForOptimism = []; 44 | config.bots.map((b) => { 45 | const objectValues = Object.values(b.contracts); 46 | const objectsFiltered = objectValues.filter((o) => o.chainId == 10); 47 | addressesForOptimism = objectsFiltered; 48 | }); 49 | return addressesForOptimism; 50 | case 137: 51 | let addressesForPolygon = []; 52 | config.bots.map((b) => { 53 | const objectValues = Object.values(b.contracts); 54 | const objectsFiltered = objectValues.filter((o) => o.chainId == 137); 55 | addressesForPolygon = objectsFiltered; 56 | }); 57 | return addressesForPolygon; 58 | case 42161: 59 | let addressesForChain = []; 60 | config.bots.map((b) => { 61 | const objectValues = Object.values(b.contracts); 62 | const objectsFiltered = objectValues.filter( 63 | (o) => o.chainId == 42161 64 | ); 65 | addressesForChain = objectsFiltered; 66 | }); 67 | return addressesForChain; 68 | case 250: 69 | let addressesForFTM = []; 70 | config.bots.map((b) => { 71 | const objectValues = Object.values(b.contracts); 72 | const objectsFiltered = objectValues.filter((o) => o.chainId == 250); 73 | addressesForFTM = objectsFiltered; 74 | }); 75 | return addressesForFTM; 76 | case 43114: 77 | let addressesForArbitrum = []; 78 | config.bots.map((b) => { 79 | const objectValues = Object.values(b.contracts); 80 | const objectsFiltered = objectValues.filter( 81 | (o) => o.chainId == 43114 82 | ); 83 | addressesForArbitrum = objectsFiltered; 84 | }); 85 | return addressesForArbitrum; 86 | } 87 | }, 88 | getBlocktimeByChainId: (chainId) => { 89 | switch (chainId) { 90 | case 1: 91 | return 14; 92 | case 137: 93 | return 2.7; 94 | case 43114: 95 | return 3; 96 | case 56: 97 | return 3; 98 | case 10: 99 | return 13; 100 | case 42161: 101 | return 15; 102 | case 250: 103 | return 1.2; 104 | } 105 | }, 106 | }; 107 | -------------------------------------------------------------------------------- /flashloan-detector/src/helper.spec.js: -------------------------------------------------------------------------------- 1 | const { ethers, getEthersProvider } = require("forta-agent"); 2 | const helper = require("./helper"); 3 | 4 | // Mock the getEthersProvider function of the forta-agent module 5 | jest.mock("forta-agent", () => { 6 | const original = jest.requireActual("forta-agent"); 7 | return { 8 | ...original, 9 | getEthersProvider: jest.fn(), 10 | }; 11 | }); 12 | 13 | // Mock the getEthersProvider impl to return 14 | // getNetworkMock and _isSigner (needed for the Contract creation) 15 | const mockGetNetwork = jest.fn(); 16 | getEthersProvider.mockImplementation(() => ({ getNetwork: mockGetNetwork, _isSigner: true })); 17 | 18 | // Mock the init() and all() functions of the ethers-multicall module 19 | jest.mock("ethers-multicall", () => ({ 20 | Provider: jest.fn().mockImplementation(() => ({ init: () => {}, all: jest.fn() })), 21 | })); 22 | 23 | const asset = "0xasset"; 24 | const amount = ethers.utils.parseUnits("100", 18); 25 | const from = "0xfrom"; 26 | const to = "0xto"; 27 | 28 | const tokenProfitsEvents = [ 29 | { 30 | address: asset, 31 | args: { src: from, dst: to, wad: amount }, 32 | }, 33 | ]; 34 | 35 | const traces = [ 36 | { 37 | action: { 38 | from, 39 | to, 40 | value: "0xa", 41 | callType: "call", 42 | }, 43 | }, 44 | { 45 | action: { 46 | from, 47 | to, 48 | value: "0x0", 49 | callType: "call", 50 | }, 51 | }, 52 | { 53 | action: { 54 | balance: "0xa", 55 | refundAddress: to, 56 | }, 57 | }, 58 | ]; 59 | 60 | // TODO 61 | // Add tests for calculateTokensUsdProfit, calculateNativeUsdProfit and calculateBorrowedAmount 62 | describe("helper module", () => { 63 | const mockTxEvent = { filterLog: jest.fn() }; 64 | 65 | beforeEach(() => { 66 | mockTxEvent.filterLog.mockReset(); 67 | }); 68 | 69 | describe("init", () => { 70 | it("should call getNetwork", async () => { 71 | mockGetNetwork.mockResolvedValueOnce({ chainId: "1" }); 72 | await helper.init(); 73 | 74 | expect(mockGetNetwork).toHaveBeenCalledTimes(1); 75 | }); 76 | }); 77 | describe("calculateTokenProfits", () => { 78 | it("should calculate 0 profits if there are no transactions from/to the address", async () => { 79 | const profits = helper.calculateTokenProfits(tokenProfitsEvents, "0xotherAddress"); 80 | 81 | expect(profits).toStrictEqual({ [asset]: helper.zero }); 82 | }); 83 | it("should calculate positive profits", async () => { 84 | const profits = helper.calculateTokenProfits(tokenProfitsEvents, to); 85 | 86 | expect(profits).toStrictEqual({ [asset]: amount }); 87 | }); 88 | it("should calculate negative profits", async () => { 89 | const profits = helper.calculateTokenProfits(tokenProfitsEvents, from); 90 | 91 | expect(profits).toStrictEqual({ [asset]: amount.mul(-1) }); 92 | }); 93 | }); 94 | describe("calculateNativeProfits", () => { 95 | it("should calculate 0 profits if there are no transactions from/to the address", async () => { 96 | const profits = helper.calculateNativeProfit(traces, "0xotherAddress"); 97 | 98 | expect(profits).toStrictEqual(helper.zero); 99 | }); 100 | it("should calculate positive profits", async () => { 101 | const profits = helper.calculateNativeProfit(traces, to); 102 | 103 | // 0xa = 10; 1 transfer of 10 + 1 refund of 10 = 20 104 | const expectedProfit = ethers.BigNumber.from(20); 105 | 106 | expect(profits).toStrictEqual(expectedProfit); 107 | }); 108 | it("should calculate negative profits", async () => { 109 | const profits = helper.calculateNativeProfit(traces, from); 110 | 111 | // 0xa = 10; 1 transfer of 10 112 | const expectedProfit = ethers.BigNumber.from(-10); 113 | 114 | expect(profits).toStrictEqual(expectedProfit); 115 | }); 116 | }); 117 | }); 118 | -------------------------------------------------------------------------------- /asset-drained/README.md: -------------------------------------------------------------------------------- 1 | # Asset Drained Bot 2 | 3 | ## Description 4 | 5 | This bot detects if a contract has had 99% or more of one of its assets drained within a block. It monitors ERC20 and native tokens transfers from contracts and raises an alert when a contract has its balance decreased by 99% or more from one block to the next. An alert is triggered if the value lost is either above the USD threshold (i.e., $10,000) or exceeds 5% of the token's total supply." 6 | 7 | ## Supported Chains 8 | 9 | - Ethereum 10 | - Optimism 11 | - BNB Smart Chain 12 | - Polygon 13 | - Fantom 14 | - Arbitrum 15 | - Avalanche 16 | 17 | ## Alerts 18 | 19 | - ASSET-DRAINED 20 | 21 | - Fired when a contract has had 99% or more of one of its assets drained 22 | - Severity is always set to "high" 23 | - Type is always set to "suspicious" 24 | - Metadata: 25 | - `contract` - the contract's address 26 | - `asset` - the asset's address 27 | - `initiators` - the EOA(s) that initiated the transaction(s) 28 | - `preDrainBalance` - the pre-drain balance 29 | - `postDrainBalance` - the post-drain balance 30 | - `txHashes` - the hash(es) of the transaction(s) in which the contract was drained 31 | - `blockNumber` - the block number at the time of the contract drain 32 | - `anomalyScore` - score of how anomalous the alert is (0-1) 33 | - Score calculated by finding amount of `ASSET-DRAINED` transactions out of the total number of ERC20 transfers processed by this bot. 34 | - Note: score differs based on chain. 35 | - Labels: 36 | - Label 1: 37 | - `entityType`: The type of the entity, always set to "Address" 38 | - `entity`: The victim's address 39 | - `label`: The type of the label, always set to "Victim" 40 | - `confidence`: The confidence level of the address being a victim (0-1). Always set to `1`. 41 | - Label 2: 42 | - `entityType`: The type of the entity, always set to "Address" 43 | - `entity`: The initiator EOA's address 44 | - `label`: The type of the label, always set to "Attacker" 45 | - `confidence`: The confidence level of the address being a victim (0-1). Always set to `0.5`. 46 | - Addresses contain the list of addresses that received the assets from the drained contract 47 | 48 | - ASSET-DRAINED-LIQUIDITY-REMOVAL 49 | - Fired when a contract has had 99% or more of one of its assets drained by _liquidity removal_ 50 | - Severity is always set to "high" 51 | - Type is always set to "suspicious" 52 | - Metadata: 53 | - `contract` - the contract's address 54 | - `asset` - the asset's address 55 | - `initiators` - the EOA(s) that initiated the transaction(s) 56 | - `preDrainBalance` - the pre-drain balance 57 | - `postDrainBalance` - the post-drain balance 58 | - `txHashes` - the hash(es) of the transaction(s) in which the contract was drained 59 | - `blockNumber` - the block number at the time of the contract drain 60 | - `anomalyScore` - score of how anomalous the alert is (0-1) 61 | - Score calculated by finding amount of `ASSET-DRAINED` transactions out of the total number of ERC20 transfers processed by this bot. 62 | - Note: score differs based on chain. 63 | - Labels: 64 | - Label 1: 65 | - `entityType`: The type of the entity, always set to "Address" 66 | - `entity`: The victim's address 67 | - `label`: The type of the label, always set to "Victim" 68 | - `confidence`: The confidence level of the address being a victim (0-1). Always set to `1`. 69 | - Label 2: 70 | - `entityType`: The type of the entity, always set to "Address" 71 | - `entity`: The initiator EOA's address 72 | - `label`: The type of the label, always set to "Attacker" 73 | - `confidence`: The confidence level of the address being a victim (0-1). Always set to `0.5`. 74 | - Addresses contain the list of addresses that received the assets from the drained contract 75 | 76 | ## Test Data 77 | 78 | ### Ethereum Mainnet 79 | 80 | The bot behaviour can be verified by running: 81 | 82 | - `npm run block 13499798,13499799` (CREAM exploit). 83 | - `npm run block 15572488,15572489` (WinterMute exploit). 84 | - `npm run block 15794364,15794365` (OlympusDAO exploit). 85 | 86 | ### BNB Smart Chain 87 | 88 | - `npm run block 30235565,30235566` (Liquidity Removal Alert) 89 | 90 | Every block we process the transactions from the previous one so when testing you should provide the exploit block and the next one. 91 | -------------------------------------------------------------------------------- /bridge-balance-difference/src/agent.js: -------------------------------------------------------------------------------- 1 | const { 2 | Finding, 3 | FindingSeverity, 4 | FindingType, 5 | ethers, 6 | } = require("forta-agent"); 7 | const ARIMA = require("arima"); 8 | const { contract1, contract2, tokens: t } = require("../bot-config.json"); 9 | 10 | const INTERVAL = 3600; // 1 hour 11 | 12 | const secondsPerYear = 60 * 60 * 24 * 365; 13 | const periodsPerYear = Math.floor(secondsPerYear / INTERVAL); 14 | 15 | const arima = new ARIMA({ 16 | p: 1, 17 | d: 0, 18 | q: 1, 19 | verbose: false, 20 | }); 21 | 22 | const ABI = [ 23 | "function balanceOf(address) public view returns (uint256)", 24 | "function decimals() external view returns (uint8)", 25 | ]; 26 | 27 | const provider1 = new ethers.providers.JsonRpcProvider(contract1.rpcUrl); 28 | const provider2 = new ethers.providers.JsonRpcProvider(contract2.rpcUrl); 29 | 30 | // Each token has 2 contracts 31 | const tokens = t.map((token) => ({ 32 | timeSeries: [], 33 | decimals: 0, 34 | tokenContract1: new ethers.Contract(token.address1, ABI, provider1), 35 | tokenContract2: new ethers.Contract(token.address2, ABI, provider2), 36 | })); 37 | 38 | let chainId1; 39 | let chainId2; 40 | let lastTimestamp = 0; 41 | 42 | function provideInitialize(prov1, prov2, tokensObj) { 43 | return async function initialize() { 44 | chainId1 = (await prov1.getNetwork()).chainId; 45 | chainId2 = (await prov2.getNetwork()).chainId; 46 | 47 | const decimals = await Promise.all( 48 | tokensObj.map(async (token) => token.tokenContract1.decimals()) 49 | ); 50 | 51 | decimals.forEach((dec, i) => { 52 | tokens[i].decimals = dec; 53 | }); 54 | }; 55 | } 56 | 57 | function provideHandleBlock(tokensData) { 58 | return async function handleBlock(blockEvent) { 59 | let findings = []; 60 | const { timestamp } = blockEvent.block; 61 | 62 | // Check the price once every INTERVAL 63 | if (timestamp < lastTimestamp + INTERVAL) return findings; 64 | 65 | findings = await Promise.all( 66 | tokensData.map(async (token) => { 67 | const { tokenContract1, tokenContract2, decimals, timeSeries } = token; 68 | let tempFinding = null; 69 | 70 | // Get the token balance for both bridge contracts 71 | const balance1 = await tokenContract1.balanceOf(contract1.address); 72 | const balance2 = await tokenContract2.balanceOf(contract2.address); 73 | 74 | // Keep only 2 decimals because ARIMA doesn't work well with a lot of decimals 75 | let difference = ethers.utils.formatUnits( 76 | balance1.sub(balance2), 77 | decimals 78 | ); 79 | difference = parseFloat((+difference).toFixed(2)); 80 | 81 | if (timeSeries.length > 10) { 82 | arima.train(timeSeries); 83 | const [pred, err] = arima.predict(1).flat(); 84 | 85 | // Calculate the 95% confidence interval 86 | const high = pred + 1.96 * Math.sqrt(err); 87 | console.log( 88 | "Difference: ", 89 | difference, 90 | "High: ", 91 | high, 92 | "Should Alert: ", 93 | difference > high 94 | ); 95 | if (difference > high) { 96 | tempFinding = Finding.fromObject({ 97 | name: "Bridge Balance Difference Bot", 98 | description: `${contract1.address} (Chain: ${chainId1}) shows statistically significant difference to the other side of the bridge ${contract2.address} (Chain: ${chainId2})`, 99 | alertId: "BRIDGE-BALANCE-DIFFERENCE", 100 | severity: FindingSeverity.High, 101 | type: FindingType.Suspicious, 102 | }); 103 | } 104 | } 105 | 106 | // Add the current diff to the time series 107 | timeSeries.push(difference); 108 | 109 | // Only keep data for the last 1 year 110 | if (timeSeries.length > periodsPerYear) timeSeries.shift(); 111 | return tempFinding; 112 | }) 113 | ); 114 | 115 | lastTimestamp = timestamp; 116 | 117 | // Filter out null elements 118 | findings = findings.filter((f) => !!f); 119 | return findings; 120 | }; 121 | } 122 | 123 | module.exports = { 124 | provideInitialize, 125 | initialize: provideInitialize(provider1, provider2, tokens), 126 | provideHandleBlock, 127 | handleBlock: provideHandleBlock(tokens), 128 | resetLastTimestamp: () => { 129 | lastTimestamp = 0; 130 | }, // Used in unit tests 131 | }; 132 | -------------------------------------------------------------------------------- /forta-tornado-cash-starter-kit/src/agent.js: -------------------------------------------------------------------------------- 1 | const { Finding, FindingSeverity, FindingType, Label, EntityType, getEthersProvider } = require("forta-agent"); 2 | const { getContractsByChainId, getInitialFundedByTornadoCash, eventABI, addressLimit } = require("./helper"); 3 | const { default: calculateAlertRate } = require("bot-alert-rate"); 4 | const { ScanCountType } = require("bot-alert-rate"); 5 | const { getSecrets } = require("./storage"); 6 | const { LRUCache } = require("lru-cache"); 7 | 8 | let chainId; 9 | let apiKeys; 10 | let isRelevantChain; 11 | const ethersProvider = getEthersProvider(); 12 | const BOT_ID = "0x617c356a4ad4b755035ef8024a87d36d895ee3cb0864e7ce9b3cf694dd80c82a"; 13 | const cache = new LRUCache({ max: 10_000 }); 14 | 15 | let totalContractInteractions = 0; 16 | 17 | let tornadoCashAddresses; 18 | 19 | //Adding one placeholder address for testing purposes 20 | let fundedByTornadoCash = new Set(["0x58f970044273705ab3b0e87828e71123a7f95c9d"]); 21 | 22 | //Load all properties by chainId 23 | const provideInitialize = (ethersProvider) => async () => { 24 | chainId = (await ethersProvider.getNetwork()).chainId; 25 | apiKeys = await getSecrets(); 26 | process.env["ZETTABLOCK_API_KEY"] = apiKeys.generalApiKeys.ZETTABLOCK[0]; 27 | 28 | // Optimism is not yet supported by bot-alert-rate package 29 | isRelevantChain = Number(chainId) === 10; 30 | tornadoCashAddresses = getContractsByChainId(chainId); 31 | fundedByTornadoCash = getInitialFundedByTornadoCash(chainId); 32 | }; 33 | 34 | function provideHandleTranscation(ethersProvider, calculateAlertRate) { 35 | return async function handleTransaction(txEvent) { 36 | const findings = []; 37 | const filteredForFunded = txEvent.filterLog(eventABI, tornadoCashAddresses); 38 | filteredForFunded.forEach((tx) => { 39 | const { to } = tx.args; 40 | if (fundedByTornadoCash.size >= addressLimit) { 41 | const tempFundedByTornadoCashArray = [...fundedByTornadoCash]; 42 | tempFundedByTornadoCashArray.shift(); 43 | fundedByTornadoCash = new Set(tempFundedByTornadoCashArray); 44 | } 45 | fundedByTornadoCash.add(to.toLowerCase()); 46 | }); 47 | if (!txEvent.to) { 48 | return findings; 49 | } 50 | 51 | const cacheKey = `contractCode-${chainId}-${txEvent.to}`; 52 | 53 | let contractCode; 54 | if (cache.has(cacheKey)) { 55 | contractCode = cache.get(cacheKey); 56 | } else { 57 | contractCode = await ethersProvider.getCode(txEvent.to); 58 | cache.set(cacheKey, contractCode); 59 | } 60 | 61 | if (contractCode !== "0x") { 62 | if (isRelevantChain) { 63 | totalContractInteractions += 1; 64 | } 65 | } else { 66 | return findings; 67 | } 68 | 69 | if (tornadoCashAddresses.includes(txEvent.to)) { 70 | return findings; 71 | } 72 | const hasInteractedWith = fundedByTornadoCash.has(txEvent.from); 73 | if (hasInteractedWith) { 74 | const anomalyScore = await calculateAlertRate( 75 | Number(chainId), 76 | BOT_ID, 77 | "TORNADO-CASH-FUNDED-ACCOUNT-INTERACTION", 78 | isRelevantChain ? ScanCountType.CustomScanCount : ScanCountType.ContractInteractionCount, 79 | totalContractInteractions 80 | ); 81 | findings.push( 82 | Finding.fromObject({ 83 | name: "Tornado Cash funded account interacted with contract", 84 | description: `${txEvent.from} interacted with contract ${txEvent.to}`, 85 | alertId: "TORNADO-CASH-FUNDED-ACCOUNT-INTERACTION", 86 | severity: FindingSeverity.Low, 87 | type: FindingType.Suspicious, 88 | metadata: { 89 | anomalyScore: anomalyScore.toString(), 90 | }, 91 | labels: [ 92 | Label.fromObject({ 93 | entity: txEvent.from, 94 | entityType: EntityType.Address, 95 | label: "Attacker", 96 | confidence: 0.7, 97 | }), 98 | Label.fromObject({ 99 | entity: txEvent.hash, 100 | entityType: EntityType.Transaction, 101 | label: "Suspicious", 102 | confidence: 0.7, 103 | }), 104 | ], 105 | }) 106 | ); 107 | } 108 | return findings; 109 | }; 110 | } 111 | 112 | module.exports = { 113 | initialize: provideInitialize(ethersProvider), 114 | provideInitialize, 115 | handleTransaction: provideHandleTranscation(ethersProvider, calculateAlertRate), 116 | provideHandleTranscation, 117 | }; 118 | -------------------------------------------------------------------------------- /ice-phishing/src/persistence.helper.js: -------------------------------------------------------------------------------- 1 | const { fetchJwt } = require("forta-agent"); 2 | const fetch = require("node-fetch"); 3 | const { existsSync, readFileSync, writeFileSync } = require("fs"); 4 | require("dotenv").config(); 5 | 6 | class PersistenceHelper { 7 | databaseUrl; 8 | 9 | constructor(dbUrl) { 10 | this.databaseUrl = dbUrl; 11 | } 12 | 13 | async persist(value, key) { 14 | const hasLocalNode = process.env.hasOwnProperty("LOCAL_NODE"); 15 | if (!hasLocalNode) { 16 | const token = await fetchJwt({}); 17 | 18 | const headers = { Authorization: `Bearer ${token}` }; 19 | try { 20 | const response = await fetch(`${this.databaseUrl}${key}`, { 21 | method: "POST", 22 | headers: headers, 23 | body: JSON.stringify(value), 24 | }); 25 | 26 | if (response.ok) { 27 | console.log(`successfully persisted value to database`); 28 | return; 29 | } else { 30 | console.log(response.status, response.statusText); 31 | } 32 | } catch (e) { 33 | console.log(`failed to persist value to database. Error: ${e}`); 34 | } 35 | } else { 36 | // Persist locally 37 | writeFileSync(key, JSON.stringify(value)); 38 | return; 39 | } 40 | } 41 | 42 | async load(key) { 43 | const hasLocalNode = process.env.hasOwnProperty("LOCAL_NODE"); 44 | if (!hasLocalNode) { 45 | const token = await fetchJwt({}); 46 | const headers = { Authorization: `Bearer ${token}` }; 47 | try { 48 | const response = await fetch(`${this.databaseUrl}${key}`, { headers }); 49 | 50 | if (response.ok) { 51 | const data = await response.json(); 52 | //const value = parseInt(data); 53 | console.log("successfully fetched data from database"); 54 | if (key.includes("shard")) { 55 | return data; 56 | } else { 57 | console.log("fetched:", Number(data)); 58 | return Number(data); 59 | } 60 | } else { 61 | console.log(`${key} has no database entry`); 62 | // If this is the first bot instance that is deployed, 63 | // the database will not have data to return, 64 | // thus return zero to assign value to the variables 65 | // necessary 66 | if (key.includes("shard")) { 67 | return { 68 | approvals: {}, 69 | approvalsERC20: {}, 70 | approvalsERC721: {}, 71 | approvalsForAll721: {}, 72 | approvalsForAll1155: {}, 73 | approvalsInfoSeverity: {}, 74 | approvalsERC20InfoSeverity: {}, 75 | approvalsERC721InfoSeverity: {}, 76 | approvalsForAll721InfoSeverity: {}, 77 | approvalsForAll1155InfoSeverity: {}, 78 | permissions: {}, 79 | permissionsInfoSeverity: {}, 80 | transfers: {}, 81 | transfersLowSeverity: {}, 82 | pigButcheringTransfers: {}, 83 | }; 84 | } else { 85 | return 0; 86 | } 87 | } 88 | } catch (e) { 89 | console.log(`Error in fetching data.`); 90 | throw e; 91 | } 92 | } else { 93 | // Checking if it exists locally 94 | if (existsSync(key)) { 95 | let data; 96 | data = JSON.parse(readFileSync(key).toString()); 97 | return data; 98 | } else { 99 | console.log(`file ${key} does not exist`); 100 | 101 | // If this is the first bot instance that is deployed, 102 | // the database will not have data to return, 103 | // thus return zero to assign value to the variables 104 | // necessary 105 | if (key.includes("shard")) { 106 | return { 107 | approvals: {}, 108 | approvalsERC20: {}, 109 | approvalsERC721: {}, 110 | approvalsForAll721: {}, 111 | approvalsForAll1155: {}, 112 | approvalsInfoSeverity: {}, 113 | approvalsERC20InfoSeverity: {}, 114 | approvalsERC721InfoSeverity: {}, 115 | approvalsForAll721InfoSeverity: {}, 116 | approvalsForAll1155InfoSeverity: {}, 117 | permissions: {}, 118 | permissionsInfoSeverity: {}, 119 | transfers: {}, 120 | transfersLowSeverity: {}, 121 | pigButcheringTransfers: {}, 122 | }; 123 | } else { 124 | return 0; 125 | } 126 | } 127 | } 128 | } 129 | } 130 | 131 | module.exports = { 132 | PersistenceHelper, 133 | }; 134 | -------------------------------------------------------------------------------- /malicious-gov-proposal/src/agent.js: -------------------------------------------------------------------------------- 1 | const { 2 | Finding, 3 | FindingSeverity, 4 | FindingType, 5 | ethers, 6 | } = require("forta-agent"); 7 | const { address, type, parameters } = require("../bot-config.json"); 8 | 9 | const COMP_EVENT_SIG = 10 | "event ProposalCreated(uint id, address proposer, address[] targets, uint[] values, string[] signatures, bytes[] calldatas, uint startBlock, uint endBlock, string description)"; 11 | const AAVE_EVENT_SIG = 12 | "event ProposalCreated(uint256 id, address indexed creator, address indexed executor, address[] targets, uint256[] values, string[] signatures, bytes[] calldatas, bool[] withDelegatecalls, uint256 startBlock, uint256 endBlock, address strategy, bytes32 ipfsHash)"; 13 | const ARAGON_EVENT_SIG = 14 | "event StartVote(uint256 indexed voteId, address indexed creator, string metadata)"; 15 | 16 | let eventSig; 17 | let processFunc; 18 | let params; 19 | 20 | function processSignatures(event) { 21 | const { signatures, calldatas } = event.args; 22 | let finding = false; 23 | 24 | signatures.forEach((sig, i) => { 25 | // Match the function name with a provided signature 26 | const funcName = sig.split("(")[0]; 27 | const param = params.find((p) => p.signature.split("(")[0] === funcName); 28 | if (!param) return; 29 | 30 | // Get the function arguments and decode the data 31 | const functionArgs = param.signature 32 | .split("(")[1] 33 | .split(")")[0] 34 | .split(", "); 35 | const decoded = ethers.utils.defaultAbiCoder.decode( 36 | functionArgs, 37 | calldatas[i] 38 | ); 39 | 40 | // Check if there is a parameter that is outside the range 41 | param.thresholds.forEach((threshold) => { 42 | const { name, min, max, decimals } = threshold; 43 | 44 | const decodedValue = decimals 45 | ? +ethers.utils.formatUnits(decoded[name], decimals) 46 | : decoded[name]; 47 | 48 | if (!decodedValue) return; 49 | console.log("Decoded value:", decodedValue); 50 | console.log("Min:", min); 51 | console.log("Max:", max); 52 | if (decodedValue < min || decodedValue > max) { 53 | finding = true; 54 | } 55 | }); 56 | }); 57 | 58 | return finding; 59 | } 60 | 61 | function processAragonVote(event) { 62 | const { metadata } = event.args; 63 | let finding = false; 64 | 65 | // Process the metadata: remove the prefix, convert it to lower case and split it to changes 66 | const processedMetadata = metadata.split("Omnibus vote: ")[1].toLowerCase(); 67 | const changes = processedMetadata.split(";"); 68 | 69 | // Compare each change with every provided param 70 | changes.forEach((message) => { 71 | params.forEach((param) => { 72 | const { string, min, max } = param; 73 | 74 | // Get the param string and process it 75 | // replace '*' with wildcard capture group and '_' with wildcard 76 | const processedString = string 77 | .toLowerCase() 78 | .replace("*", "(.+)") 79 | .replace("_", ".+"); 80 | 81 | // Test the metadata 82 | const regex = new RegExp(processedString); 83 | const match = message.match(regex); 84 | if (!match) return; 85 | 86 | // Convert the match to a number 87 | // Remove the commas: (1,000 => 1000) 88 | const number = match[1].replace(/,/g, ""); 89 | 90 | if (number < min || number > max) { 91 | finding = true; 92 | } 93 | }); 94 | }); 95 | return finding; 96 | } 97 | 98 | function provideInitialize(configType, configParams) { 99 | params = configParams; 100 | return () => { 101 | switch (configType) { 102 | case "comp": 103 | eventSig = COMP_EVENT_SIG; 104 | processFunc = processSignatures; 105 | break; 106 | case "aave": 107 | eventSig = AAVE_EVENT_SIG; 108 | processFunc = processSignatures; 109 | break; 110 | case "aragon": 111 | eventSig = ARAGON_EVENT_SIG; 112 | processFunc = processAragonVote; 113 | break; 114 | default: 115 | throw new Error("Invalid type"); 116 | } 117 | }; 118 | } 119 | 120 | const handleTransaction = async (txEvent) => { 121 | const findings = []; 122 | 123 | const events = txEvent.filterLog(eventSig, address); 124 | 125 | events.forEach((event) => { 126 | const isMalicious = processFunc(event); 127 | 128 | if (isMalicious) { 129 | findings.push( 130 | Finding.fromObject({ 131 | name: "Possible malicious governance proposal created", 132 | description: `${txEvent.from} created a possible malicious governance proposal`, 133 | alertId: "POSSIBLE-MALICIOUS-GOVT-PROPOSAL-CREATED", 134 | severity: FindingSeverity.High, 135 | type: FindingType.Exploit, 136 | }) 137 | ); 138 | } 139 | }); 140 | 141 | return findings; 142 | }; 143 | 144 | module.exports = { 145 | provideInitialize, 146 | initialize: provideInitialize(type, parameters), 147 | handleTransaction, 148 | }; 149 | -------------------------------------------------------------------------------- /bridge-balance-difference/src/agent.spec.js: -------------------------------------------------------------------------------- 1 | const { 2 | FindingType, 3 | FindingSeverity, 4 | Finding, 5 | ethers, 6 | } = require('forta-agent'); 7 | const { 8 | provideInitialize, 9 | provideHandleBlock, 10 | resetLastTimestamp, 11 | } = require('./agent'); 12 | 13 | const contract1 = { 14 | address: '0x1', 15 | rpcUrl: 'url', 16 | }; 17 | 18 | const contract2 = { 19 | address: '0x2', 20 | rpcUrl: 'url', 21 | }; 22 | 23 | const tokens = [ 24 | { 25 | address1: '0xtoken1', 26 | address2: '0xtoken2', 27 | }, 28 | ]; 29 | 30 | const tokenData = { 31 | timeSeries: [], 32 | decimals: 18, 33 | tokenContract1: { 34 | balanceOf: jest.fn(), 35 | decimals: () => 18, 36 | }, 37 | tokenContract2: { 38 | balanceOf: jest.fn(), 39 | decimals: () => 18, 40 | }, 41 | }; 42 | 43 | const tokensData = [tokenData]; 44 | 45 | const oneHundred = ethers.utils.parseEther('100'); 46 | const twoHundred = ethers.utils.parseEther('200'); 47 | 48 | const chainId1 = 1; 49 | const chainId2 = 2; 50 | 51 | const provider1 = { getNetwork: () => ({ chainId: chainId1 }) }; 52 | const provider2 = { getNetwork: () => ({ chainId: chainId2 }) }; 53 | 54 | // Mock the config file 55 | jest.mock('../bot-config.json', () => ({ 56 | contract1, 57 | contract2, 58 | tokens, 59 | }), { virtual: true }); 60 | 61 | describe('bridge balance difference bot', () => { 62 | let initialize; 63 | let handleBlock; 64 | 65 | describe('handleBlock', () => { 66 | beforeAll(async () => { 67 | initialize = provideInitialize(provider1, provider2, tokensData); 68 | handleBlock = provideHandleBlock(tokensData); 69 | 70 | await initialize(); 71 | }); 72 | 73 | beforeEach(() => { 74 | tokenData.tokenContract1.balanceOf.mockReset(); 75 | tokenData.tokenContract2.balanceOf.mockReset(); 76 | tokenData.timeSeries = []; 77 | resetLastTimestamp(); 78 | }); 79 | 80 | it('should return empty findings if not enough time has passed', async () => { 81 | const mockBlockEvent = { block: { timestamp: 1000 } }; 82 | const findings = await handleBlock(mockBlockEvent); 83 | expect(findings).toStrictEqual([]); 84 | }); 85 | 86 | it('should return empty findings if there is not enough data', async () => { 87 | const mockBlockEvent = { block: { timestamp: 4000 } }; 88 | 89 | tokenData.tokenContract1.balanceOf.mockResolvedValueOnce(oneHundred); 90 | tokenData.tokenContract2.balanceOf.mockResolvedValueOnce(oneHundred); 91 | 92 | const findings = await handleBlock(mockBlockEvent); 93 | 94 | expect(findings).toStrictEqual([]); 95 | expect(tokenData.tokenContract1.balanceOf).toHaveBeenCalledTimes(1); 96 | expect(tokenData.tokenContract2.balanceOf).toHaveBeenCalledTimes(1); 97 | expect(tokenData.tokenContract1.balanceOf).toHaveBeenCalledWith(contract1.address); 98 | expect(tokenData.tokenContract2.balanceOf).toHaveBeenCalledWith(contract2.address); 99 | }); 100 | 101 | it('should return empty findings if the difference is not significant', async () => { 102 | const mockBlockEvent = { block: { timestamp: 4000 } }; 103 | 104 | // Add 11 entries with balance diff = 10 105 | tokenData.timeSeries = Array(11).fill().map(() => 10); 106 | 107 | // The current diff is 100 - 100 = 0 so it is not significant 108 | tokenData.tokenContract1.balanceOf.mockResolvedValueOnce(oneHundred); 109 | tokenData.tokenContract2.balanceOf.mockResolvedValueOnce(oneHundred); 110 | 111 | const findings = await handleBlock(mockBlockEvent); 112 | 113 | expect(findings).toStrictEqual([]); 114 | expect(tokenData.tokenContract1.balanceOf).toHaveBeenCalledTimes(1); 115 | expect(tokenData.tokenContract2.balanceOf).toHaveBeenCalledTimes(1); 116 | expect(tokenData.tokenContract1.balanceOf).toHaveBeenCalledWith(contract1.address); 117 | expect(tokenData.tokenContract2.balanceOf).toHaveBeenCalledWith(contract2.address); 118 | }); 119 | 120 | it('should return findings if the difference is significant', async () => { 121 | const mockBlockEvent = { block: { timestamp: 4000 } }; 122 | 123 | // Add 11 entries with balance diff = 10 124 | tokenData.timeSeries = Array(11).fill().map(() => 10); 125 | 126 | // The current diff is 200 - 100 = 100 so it is significant 127 | tokenData.tokenContract1.balanceOf.mockResolvedValueOnce(twoHundred); 128 | tokenData.tokenContract2.balanceOf.mockResolvedValueOnce(oneHundred); 129 | 130 | const findings = await handleBlock(mockBlockEvent); 131 | 132 | expect(findings).toStrictEqual([Finding.fromObject({ 133 | name: 'Bridge Balance Difference Bot', 134 | description: `${contract1.address} (Chain: 1) shows statistically significant difference to the other side of the bridge ${contract2.address} (Chain: 2)`, 135 | alertId: 'BRIDGE-BALANCE-DIFFERENCE', 136 | severity: FindingSeverity.High, 137 | type: FindingType.Suspicious, 138 | })]); 139 | expect(tokenData.tokenContract1.balanceOf).toHaveBeenCalledTimes(1); 140 | expect(tokenData.tokenContract2.balanceOf).toHaveBeenCalledTimes(1); 141 | expect(tokenData.tokenContract1.balanceOf).toHaveBeenCalledWith(contract1.address); 142 | expect(tokenData.tokenContract2.balanceOf).toHaveBeenCalledWith(contract2.address); 143 | }); 144 | }); 145 | }); 146 | -------------------------------------------------------------------------------- /large-balance-decrease/README.md: -------------------------------------------------------------------------------- 1 | # Large Balance Decrease Bot 2 | 3 | ## Description 4 | 5 | Detects if the balance of a protocol decreases significantly. 6 | 7 | ## Supported Chains 8 | 9 | - Ethereum 10 | - Optimism 11 | - Binance Smart Chain 12 | - Polygon 13 | - Fantom 14 | - Arbitrum 15 | - Avalanche 16 | 17 | ## Alerts 18 | 19 | - BALANCE-DECREASE-ASSETS-ALL-REMOVED 20 | 21 | - Fired when the token balance of a protocol is completely drained 22 | - Severity is always set to "critical" 23 | - Type is always set to "exploit" 24 | - Metadata: 25 | - `firstTxHash` - the hash of the first transaction for the period 26 | - `lastTxHash` - the hash of the last transaction for the period 27 | - `assetImpacted` - the drained asset 28 | - `anomalyScore` - score of how anomalous the alert is (0-1) 29 | - Score calculated by finding amount of `BALANCE-DECREASE-ASSETS-ALL-REMOVED` transactions out of the total number of token transfers in which the monitored address was involved. 30 | - Note: score differs based on chain. 31 | - Labels: 32 | - Label 1: 33 | - `entityType`: The type of the entity, always set to "Transaction" 34 | - `entity`: The `firstTxhash` 35 | - `label`: The type of the label, always set to "Suspicious" 36 | - `confidence`: The confidence level of the transaction being suspicious (0-1). Always set to `0.9`. 37 | - Label 2: 38 | - `entityType`: The type of the entity, always set to "Transaction" 39 | - `entity`: The `lastTxHash` 40 | - `label`: The type of the label, always set to "Suspicious" 41 | - `confidence`: The confidence level of the transaction being suspicious (0-1). Always set to `0.9`. 42 | - Label 3: 43 | - `entityType`: The type of the entity, always set to "Address" 44 | - `entity`: The monitored contract address 45 | - `label`: The type of the label, always set to "Victim" 46 | - `confidence`: The confidence level of the address being a victim (0-1). Always set to `0.9`. 47 | - Label 4: 48 | - `entityType`: The type of the entity, always set to "Address" 49 | - `entity`: The first transaction's initiator address 50 | - `label`: The type of the label, always set to "Attacker" 51 | - `confidence`: The confidence level of the address being an attacker (0-1). Always set to `0.9`. 52 | - Label 5: 53 | - `entityType`: The type of the entity, always set to "Address" 54 | - `entity`: The last transaction's initiator address 55 | - `label`: The type of the label, always set to "Attacker" 56 | - `confidence`: The confidence level of the address being an attacker (0-1). Always set to `0.9`. 57 | 58 | - BALANCE-DECREASE-ASSETS-PORTION-REMOVED 59 | - Fired when the token balance of a protocol decreases significantly 60 | - Severity is always set to "medium" 61 | - Type is always set to "exploit" 62 | - Metadata: 63 | - `firstTxHash` - the hash of the first transaction for the period 64 | - `lastTxHash` - the hash of the last transaction for the period 65 | - `assetImpacted` - the impacted asset 66 | - `assetVolumeDecreasePercentage` - the decrease percentage 67 | - `anomalyScore` - score of how anomalous the alert is (0-1) 68 | - Score calculated by finding amount of `BALANCE-DECREASE-ASSETS-PORTION-REMOVED` transactions out of the total number of token transfers in which the monitored address was involved. 69 | - Note: score differs based on chain. 70 | - Labels: 71 | - Label 1: 72 | - `entityType`: The type of the entity, always set to "Transaction" 73 | - `entity`: The `firstTxhash` 74 | - `label`: The type of the label, always set to "Suspicious" 75 | - `confidence`: The confidence level of the transaction being suspicious (0-1). Always set to `0.7`. 76 | - Label 2: 77 | - `entityType`: The type of the entity, always set to "Transaction" 78 | - `entity`: The `lastTxHash` 79 | - `label`: The type of the label, always set to "Suspicious" 80 | - `confidence`: The confidence level of the transaction being suspicious (0-1). Always set to `0.7`. 81 | - Label 3: 82 | - `entityType`: The type of the entity, always set to "Address" 83 | - `entity`: The monitored contract address 84 | - `label`: The type of the label, always set to "Victim" 85 | - `confidence`: The confidence level of the address being a victim (0-1). Always set to `0.7`. 86 | - Label 4: 87 | - `entityType`: The type of the entity, always set to "Address" 88 | - `entity`: The first transaction's initiator address 89 | - `label`: The type of the label, always set to "Attacker" 90 | - `confidence`: The confidence level of the address being an attacker (0-1). Always set to `0.7`. 91 | - Label 5: 92 | - `entityType`: The type of the entity, always set to "Address" 93 | - `entity`: The last transaction's initiator address 94 | - `label`: The type of the label, always set to "Attacker" 95 | - `confidence`: The confidence level of the address being an attacker (0-1). Always set to `0.7`. 96 | 97 | ## [Bot Setup Walkthrough](SETUP.md) 98 | 99 | ## Test Data 100 | 101 | The bot behaviour can be verified with the following commands: 102 | 103 | - `npm run block 13158432,13176177,13182676,13202391,13204222,13209657,13210732,13227537,13249184,13261896,13266552,13278108,13278248,13299220,13318971,13333652,13342229,13365887,13388139,13406596,13428536,13448391,13453200,13466158,13484500,13484904,13499798,13510000`. Note: you have to change the contractAddress to `0xe89a6d0509faf730bd707bf868d9a2a744a363c7` and the aggregationTimePeriod to `86400` 104 | -------------------------------------------------------------------------------- /flashloan-detector/src/flashloan-detector.spec.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-plusplus */ 2 | const { ethers } = require("forta-agent"); 3 | const { getFlashloans } = require("./flashloan-detector"); 4 | 5 | const asset = "0xasset"; 6 | const amount = ethers.utils.parseUnits("100", 18); 7 | const account = "0xaccount"; 8 | const market = "0xmarket"; 9 | 10 | jest.mock("forta-agent", () => { 11 | const original = jest.requireActual("forta-agent"); 12 | return { 13 | ...original, 14 | getEthersProvider: jest.fn(), 15 | ethers: { 16 | ...original.ethers, 17 | Contract: jest.fn().mockImplementation(() => ({ 18 | getMarket: () => [asset], 19 | underlying: () => asset, 20 | token0: () => asset, 21 | _QUOTE_TOKEN_: () => asset, 22 | })), 23 | }, 24 | }; 25 | }); 26 | 27 | const mockAaveV2Event = { 28 | args: { asset, amount, target: account }, 29 | }; 30 | 31 | const mockAaveV3Event = { 32 | args: { asset, amount, target: account }, 33 | }; 34 | 35 | const mockDydxWithdrawEvent = { 36 | address: market, 37 | args: { 38 | market: ethers.constants.Zero, 39 | accountOwner: account, 40 | from: account, 41 | update: { 42 | deltaWei: { 43 | sign: false, 44 | value: amount, 45 | }, 46 | }, 47 | }, 48 | }; 49 | 50 | const mockDydxDepositEvent = { 51 | address: market, 52 | args: { 53 | market: ethers.constants.Zero, 54 | accountOwner: account, 55 | from: account, 56 | update: { 57 | deltaWei: { 58 | sign: true, 59 | value: amount.add(2), 60 | }, 61 | }, 62 | }, 63 | }; 64 | 65 | const mockEulerBorrowEvent = { 66 | name: "Borrow", 67 | address: market, 68 | args: { 69 | amount, 70 | underlying: asset, 71 | account, 72 | }, 73 | }; 74 | 75 | const mockEulerRepayEvent = { 76 | name: "Repay", 77 | address: market, 78 | args: { 79 | amount, 80 | underlying: asset, 81 | account, 82 | }, 83 | }; 84 | 85 | const mockEulerRequestBorrowEvent = { 86 | name: "RequestBorrow", 87 | address: market, 88 | args: { 89 | amount, 90 | account, 91 | }, 92 | }; 93 | 94 | const mockIronBankEvent = { 95 | args: { amount, receiver: account }, 96 | }; 97 | 98 | const mockMakerEvent = { 99 | args: { token: asset, amount, receiver: account }, 100 | }; 101 | 102 | const mockUniswapV2FunctionCall = { 103 | address: market, 104 | args: { 105 | to: account, 106 | data: "0x0" + "0".repeat(64), // Data length should be more 32 bytes (64 hex characters) 107 | amount0Out: amount, 108 | amount1Out: ethers.constants.Zero, 109 | }, 110 | }; 111 | 112 | const mockUniswapV2Event = { 113 | address: market, 114 | args: { 115 | to: account, 116 | amount0Out: amount, 117 | amount1Out: ethers.constants.Zero, 118 | }, 119 | }; 120 | 121 | const mockUniswapV3Event = { 122 | address: market, 123 | args: { 124 | recipient: account, 125 | amount0: amount, 126 | amount1: ethers.constants.Zero, 127 | }, 128 | }; 129 | 130 | const mockBalancerEvent = { 131 | args: { token: asset, amount, receiver: account }, 132 | }; 133 | 134 | const mockDodoFlashLoanEvent = { 135 | address: "0xdefi", 136 | args: { baseAmount: ethers.constants.Zero, quoteAmount: amount, assetTo: account }, 137 | }; 138 | 139 | describe("FlashloanDetector library", () => { 140 | const mockTxEvent = { 141 | filterLog: jest.fn(), 142 | filterFunction: jest.fn(), 143 | transaction: { 144 | data: "0x0", 145 | }, 146 | }; 147 | 148 | beforeEach(() => { 149 | mockTxEvent.filterLog.mockReset(); 150 | mockTxEvent.filterFunction.mockReset(); 151 | }); 152 | 153 | describe("getFlashloans", () => { 154 | it("should return empty array if there are no flashloans", async () => { 155 | // Don't mock 156 | mockTxEvent.filterLog.mockReturnValue([]); 157 | mockTxEvent.filterFunction.mockReturnValue([]); 158 | const flashloans = await getFlashloans(mockTxEvent); 159 | 160 | expect(flashloans).toStrictEqual([]); 161 | }); 162 | 163 | it("should return all the protocols if there is a flashloan from all", async () => { 164 | mockTxEvent.filterLog.mockReturnValueOnce([mockAaveV2Event]); 165 | mockTxEvent.filterLog.mockReturnValueOnce([mockAaveV3Event]); 166 | mockTxEvent.filterLog.mockReturnValueOnce([mockDydxDepositEvent, mockDydxWithdrawEvent]); 167 | mockTxEvent.filterLog.mockReturnValueOnce([ 168 | mockEulerRequestBorrowEvent, 169 | mockEulerBorrowEvent, 170 | mockEulerRepayEvent, 171 | ]); 172 | mockTxEvent.filterLog.mockReturnValueOnce([mockIronBankEvent]); 173 | mockTxEvent.filterLog.mockReturnValueOnce([mockMakerEvent]); 174 | mockTxEvent.filterFunction.mockReturnValueOnce([mockUniswapV2FunctionCall]); 175 | mockTxEvent.filterLog.mockReturnValueOnce([mockUniswapV2Event]); 176 | mockTxEvent.filterLog.mockReturnValueOnce([mockUniswapV3Event]); 177 | // Checking for a `swap` in UniswapV3 Pool 178 | mockTxEvent.filterFunction.mockReturnValueOnce([]); 179 | mockTxEvent.filterLog.mockReturnValueOnce([mockBalancerEvent]); 180 | mockTxEvent.filterLog.mockReturnValueOnce([mockDodoFlashLoanEvent]); 181 | const flashloans = await getFlashloans(mockTxEvent); 182 | 183 | const expectedFlashloanData = { account, amount, asset }; 184 | const expectedArray = []; 185 | 186 | // 10 flashloans: 187 | // 1. aaveV2, 2. aaveV3, 3. dydx, 4. euler, 5. iron bank 188 | // 6. maker, 7. uniswap V2, 8. uniswap V3, 9. balancer, 10. DODO 189 | for (let i = 0; i < 10; i++) { 190 | expectedArray.push(expectedFlashloanData); 191 | } 192 | 193 | expect(flashloans).toStrictEqual(expectedArray); 194 | }); 195 | }); 196 | }); 197 | -------------------------------------------------------------------------------- /flashloan-detector/src/detectors/uniswap-v2-detector.js: -------------------------------------------------------------------------------- 1 | const { getEthersProvider, ethers } = require("forta-agent"); 2 | 3 | const functionSignature = "function swap(uint amount0Out, uint amount1Out, address to, bytes calldata data)"; 4 | const swapFunctionSelector = "022c0d9f"; 5 | const _32bytes = 64; 6 | 7 | const POOL_TOKENS_ABI = [ 8 | "function token0() public view returns (address token)", 9 | "function token1() public view returns (address token)", 10 | ]; 11 | 12 | const SWAP_ABI = [ 13 | "event Swap(address indexed sender, uint256 amount0In, uint256 amount1In, uint256 amount0Out, uint256 amount1Out, address indexed to)", 14 | ]; 15 | 16 | // Also covers PancakeSwap on BSC, as it is a UniswapV2 fork. 17 | module.exports = { 18 | getUniswapV2Flashloan: async (txEvent) => { 19 | const swaps = txEvent.filterFunction(functionSignature); 20 | 21 | if (!swaps.length) { 22 | const calldata = txEvent.transaction.data; 23 | let index = 0; 24 | let flashloans = []; 25 | 26 | // Continuously search for the selector within the calldata 27 | while ((index = calldata.indexOf(swapFunctionSelector, index)) !== -1) { 28 | // Adjust to start searching for the next occurrence in subsequent iterations 29 | index += swapFunctionSelector.length; 30 | 31 | // Extract the arguments part of the calldata from the current selector position 32 | const argsCalldata = "0x" + calldata.substring(index); 33 | 34 | try { 35 | // NOTE: Will only work when the swap's arguments come right after the function selector in the calldata 36 | const [amount0Out, amount1Out, to, data] = ethers.utils.defaultAbiCoder.decode( 37 | ["uint256", "uint256", "address", "bytes"], 38 | argsCalldata 39 | ); 40 | 41 | if (ethers.utils.hexlify(data) !== "0x") { 42 | const swapEvents = txEvent.filterLog(SWAP_ABI); 43 | 44 | // Loop through swapEvents to find the corresponding swap event 45 | for (let i = 0; i < swapEvents.length; i++) { 46 | const { amount0Out: eventAmount0Out, amount1Out: eventAmount1Out, to: eventTo } = swapEvents[i].args; 47 | 48 | if ( 49 | eventAmount0Out.toString() === amount0Out.toString() && 50 | eventAmount1Out.toString() === amount1Out.toString() && 51 | to === eventTo 52 | ) { 53 | const address = swapEvents[i].address; 54 | const tokenIndex = amount0Out.gt(ethers.constants.Zero) ? 0 : 1; 55 | const amount = tokenIndex === 0 ? amount0Out : amount1Out; 56 | const tokenFnCall = tokenIndex === 0 ? "token0" : "token1"; 57 | 58 | const contract = new ethers.Contract(address, POOL_TOKENS_ABI, getEthersProvider()); 59 | const asset = await contract[tokenFnCall](); 60 | 61 | flashloans.push({ 62 | asset: asset.toLowerCase(), 63 | amount, 64 | account: to.toLowerCase(), 65 | }); 66 | break; 67 | } 68 | } 69 | } 70 | } catch { 71 | break; 72 | } 73 | } 74 | return flashloans; 75 | } else { 76 | const flashloans = await Promise.all( 77 | swaps.map(async (swap) => { 78 | const { data, amount0Out, amount1Out } = swap.args; 79 | let to; 80 | 81 | // Decoding the `to` field may fail (e.g. tx 0x0f6c2326b49724c586f133857b2586be93ebc3fd5d7559c475180f0800620741 on Mainnet) 82 | try { 83 | to = await swap.args.to(); 84 | } catch { 85 | const swapEvents = txEvent.filterLog(SWAP_ABI); 86 | if (!swapEvents.length) return null; 87 | 88 | for (let i = 0; i < swapEvents.length; i++) { 89 | const { amount0Out: eventAmount0Out, amount1Out: eventAmount1Out, to: eventTo } = swapEvents[i].args; 90 | if ( 91 | eventAmount0Out.toString() === amount0Out.toString() && 92 | eventAmount1Out.toString() === amount1Out.toString() 93 | ) { 94 | to = eventTo; 95 | break; 96 | } 97 | } 98 | } 99 | 100 | const { address } = swap; 101 | // In the context of Uniswap V2's protocol, a non-empty `data` field during a swap operation indicates a flash swap. 102 | // This is documented in Uniswap's documentation (https://docs.uniswap.org/protocol/V2/guides/smart-contract-integration/using-flash-swaps). 103 | // However, other protocols with identical swap function signatures may use the `data` field differently. 104 | // For instance, a transaction on Mainnet (tx: 0xe099c7bb3f1ce6bc79a5df4e66a58d60ce131c1293583a9181a808618933495a) uses `data` to represent a price value. 105 | // Therefore, to accurately identify flash swaps while accounting for these differences, we check if the `data` field is not only non-empty but also exceeds a certain length threshold. 106 | // We consider `data` lengths of 64 characters or less (after removing the '0x' prefix) as potentially not indicative of a flash swap. 107 | if (data === "0x" || data.slice(2).length <= _32bytes) { 108 | return null; 109 | } 110 | 111 | // Get the correct amount and asset address 112 | const tokenIndex = amount0Out.gt(ethers.constants.Zero) ? 0 : 1; 113 | const amount = tokenIndex === 0 ? amount0Out : amount1Out; 114 | const tokenFnCall = tokenIndex === 0 ? "token0" : "token1"; 115 | 116 | const contract = new ethers.Contract(address, POOL_TOKENS_ABI, getEthersProvider()); 117 | const asset = await contract[tokenFnCall](); 118 | 119 | return { 120 | asset: asset.toLowerCase(), 121 | amount, 122 | account: to.toLowerCase(), 123 | }; 124 | }) 125 | ); 126 | 127 | return flashloans.filter((f) => !!f); 128 | } 129 | }, 130 | }; 131 | -------------------------------------------------------------------------------- /flashloan-detector/src/persistence.helper.spec.js: -------------------------------------------------------------------------------- 1 | const { PersistenceHelper } = require("./persistence.helper"); 2 | const { existsSync, writeFileSync, unlinkSync } = require("fs"); 3 | const fetch = require("node-fetch"); 4 | 5 | const { Response } = jest.requireActual("node-fetch"); 6 | jest.mock("node-fetch"); 7 | 8 | const mockDbUrl = "databaseurl.com/"; 9 | const mockJwt = "MOCK_JWT"; 10 | const mockKey = "mock-test-key"; 11 | 12 | // Mock environment variables 13 | const mockHasOwnProperty = jest.fn(); 14 | process.env = { 15 | hasOwnProperty: mockHasOwnProperty, 16 | }; 17 | 18 | // Mock the fetchJwt function of the forta-agent module 19 | const mockFetchJwt = jest.fn(); 20 | jest.mock("forta-agent", () => { 21 | const original = jest.requireActual("forta-agent"); 22 | return { 23 | ...original, 24 | fetchJwt: () => mockFetchJwt(), 25 | }; 26 | }); 27 | 28 | const removePersistentState = () => { 29 | if (existsSync(mockKey)) { 30 | unlinkSync(mockKey); 31 | } 32 | }; 33 | 34 | describe("Persistence Helper test suite", () => { 35 | let persistenceHelper; 36 | let mockFetch = jest.mocked(fetch, true); 37 | 38 | beforeAll(() => { 39 | persistenceHelper = new PersistenceHelper(mockDbUrl); 40 | }); 41 | 42 | beforeEach(() => { 43 | removePersistentState(); 44 | }); 45 | 46 | afterEach(() => { 47 | jest.clearAllMocks(); 48 | }); 49 | 50 | it("should correctly POST a value to the database", async () => { 51 | const mockValue = 101; 52 | 53 | const mockResponseInit = { status: 202 }; 54 | const mockPostMethodResponse = { data: "4234" }; 55 | const mockFetchResponse = new Response(JSON.stringify(mockPostMethodResponse), mockResponseInit); 56 | 57 | mockHasOwnProperty.mockReturnValueOnce(false); 58 | mockFetchJwt.mockResolvedValueOnce(mockJwt); 59 | mockFetch.mockResolvedValueOnce(Promise.resolve(mockFetchResponse)); 60 | 61 | const spy = jest.spyOn(console, "log").mockImplementation(() => {}); 62 | await persistenceHelper.persist(mockValue, mockKey); 63 | 64 | expect(spy).toHaveBeenCalledWith("successfully persisted 101 to database"); 65 | expect(mockHasOwnProperty).toHaveBeenCalledTimes(1); 66 | expect(mockFetchJwt).toHaveBeenCalledTimes(1); 67 | expect(mockFetch).toHaveBeenCalledTimes(1); 68 | expect(mockFetch.mock.calls[0][0]).toEqual(`${mockDbUrl}${mockKey}`); 69 | expect(mockFetch.mock.calls[0][1].method).toEqual("POST"); 70 | expect(mockFetch.mock.calls[0][1].headers).toEqual({ Authorization: `Bearer ${mockJwt}` }); 71 | expect(mockFetch.mock.calls[0][1].body).toEqual(JSON.stringify(mockValue)); 72 | }); 73 | 74 | it("should correctly store a value to a local file", async () => { 75 | const mockValue = 101; 76 | 77 | mockHasOwnProperty.mockReturnValueOnce(true); 78 | await persistenceHelper.persist(mockValue, mockKey); 79 | 80 | expect(mockHasOwnProperty).toHaveBeenCalledTimes(1); 81 | expect(mockFetchJwt).not.toHaveBeenCalled(); 82 | expect(mockFetch).not.toHaveBeenCalled(); 83 | 84 | expect(existsSync("mock-test-key")).toBeDefined(); 85 | }); 86 | 87 | it("should fail to POST a value to the database", async () => { 88 | const mockValue = 202; 89 | 90 | const mockResponseInit = { status: 305 }; 91 | const mockPostMethodResponse = { data: "4234" }; 92 | const mockFetchResponse = new Response(JSON.stringify(mockPostMethodResponse), mockResponseInit); 93 | 94 | mockHasOwnProperty.mockReturnValueOnce(false); 95 | mockFetchJwt.mockResolvedValueOnce(mockJwt); 96 | mockFetch.mockResolvedValueOnce(mockFetchResponse); 97 | const spy = jest.spyOn(console, "log").mockImplementation(() => {}); 98 | 99 | await persistenceHelper.persist(mockValue, mockKey); 100 | expect(spy).not.toHaveBeenCalledWith("successfully persisted 202 to database"); 101 | 102 | expect(mockHasOwnProperty).toHaveBeenCalledTimes(1); 103 | expect(mockFetchJwt).toHaveBeenCalledTimes(1); 104 | expect(mockFetch).toHaveBeenCalledTimes(1); 105 | }); 106 | 107 | it("should correctly load variable values from the database", async () => { 108 | const mockData = 4234; 109 | 110 | const mockResponseInit = { status: 207 }; 111 | const mockPostMethodResponse = mockData.toString(); 112 | const mockFetchResponse = new Response(JSON.stringify(mockPostMethodResponse), mockResponseInit); 113 | 114 | mockHasOwnProperty.mockReturnValueOnce(false); 115 | mockFetchJwt.mockResolvedValueOnce(mockJwt); 116 | mockFetch.mockResolvedValueOnce(mockFetchResponse); 117 | 118 | const fetchedValue = await persistenceHelper.load(mockKey); 119 | expect(fetchedValue).toStrictEqual(4234); 120 | }); 121 | 122 | it("should fail to load values from the database, but return zero", async () => { 123 | const mockData = 4234; 124 | 125 | const mockResponseInit = { status: 308 }; 126 | const mockPostMethodResponse = mockData.toString(); 127 | const mockFetchResponse = new Response(JSON.stringify(mockPostMethodResponse), mockResponseInit); 128 | 129 | mockHasOwnProperty.mockReturnValueOnce(false); 130 | mockFetchJwt.mockResolvedValueOnce(mockJwt); 131 | mockFetch.mockResolvedValueOnce(mockFetchResponse); 132 | 133 | const fetchedValue = await persistenceHelper.load(mockKey); 134 | expect(fetchedValue).toStrictEqual(0); 135 | }); 136 | 137 | it("should correctly load values from a local file if it exists", async () => { 138 | const mockData = 4234; 139 | 140 | writeFileSync(mockKey, mockData.toString()); 141 | 142 | mockHasOwnProperty.mockReturnValueOnce(true); 143 | expect(mockFetchJwt).not.toHaveBeenCalled(); 144 | expect(mockFetch).not.toHaveBeenCalled(); 145 | 146 | const fetchedValue = await persistenceHelper.load(mockKey); 147 | expect(fetchedValue).toStrictEqual(4234); 148 | }); 149 | 150 | it("should fail load values from a local file if it doesn't exist, but return 0", async () => { 151 | mockHasOwnProperty.mockReturnValueOnce(true); 152 | expect(mockFetchJwt).not.toHaveBeenCalled(); 153 | expect(mockFetch).not.toHaveBeenCalled(); 154 | 155 | const fetchedValue = await persistenceHelper.load(mockKey); 156 | expect(fetchedValue).toStrictEqual(0); 157 | }); 158 | }); 159 | -------------------------------------------------------------------------------- /ice-phishing/src/persistence.helper.spec.js: -------------------------------------------------------------------------------- 1 | const { PersistenceHelper } = require("./persistence.helper"); 2 | const { existsSync, writeFileSync, unlinkSync } = require("fs"); 3 | const fetch = require("node-fetch"); 4 | 5 | const { Response } = jest.requireActual("node-fetch"); 6 | 7 | jest.mock("node-fetch"); 8 | 9 | const mockDbUrl = "databaseurl.com/"; 10 | const mockJwt = "MOCK_JWT"; 11 | const mockKey = "mock-test-key"; 12 | 13 | // Mock environment variables 14 | const mockHasOwnProperty = jest.fn(); 15 | process.env = { 16 | hasOwnProperty: mockHasOwnProperty, 17 | }; 18 | 19 | // Mock the fetchJwt function of the forta-agent module 20 | const mockFetchJwt = jest.fn(); 21 | jest.mock("forta-agent", () => { 22 | const original = jest.requireActual("forta-agent"); 23 | return { 24 | ...original, 25 | fetchJwt: () => mockFetchJwt(), 26 | }; 27 | }); 28 | 29 | const removePersistentState = () => { 30 | if (existsSync(mockKey)) { 31 | unlinkSync(mockKey); 32 | } 33 | }; 34 | 35 | describe("Persistence Helper test suite", () => { 36 | let persistenceHelper; 37 | let mockFetch = jest.mocked(fetch, true); 38 | 39 | beforeAll(() => { 40 | persistenceHelper = new PersistenceHelper(mockDbUrl); 41 | }); 42 | 43 | beforeEach(() => { 44 | removePersistentState(); 45 | }); 46 | 47 | afterEach(() => { 48 | jest.clearAllMocks(); 49 | }); 50 | 51 | it("should correctly POST a value to the database", async () => { 52 | const mockValue = 101; 53 | 54 | const mockResponseInit = { status: 202 }; 55 | const mockPostMethodResponse = { data: "4234" }; 56 | const mockFetchResponse = new Response(JSON.stringify(mockPostMethodResponse), mockResponseInit); 57 | 58 | mockHasOwnProperty.mockReturnValueOnce(false); 59 | mockFetchJwt.mockResolvedValueOnce(mockJwt); 60 | mockFetch.mockResolvedValueOnce(Promise.resolve(mockFetchResponse)); 61 | 62 | const spy = jest.spyOn(console, "log").mockImplementation(() => {}); 63 | await persistenceHelper.persist(mockValue, mockKey); 64 | 65 | expect(spy).toHaveBeenCalledWith("successfully persisted value to database"); 66 | expect(mockHasOwnProperty).toHaveBeenCalledTimes(1); 67 | expect(mockFetchJwt).toHaveBeenCalledTimes(1); 68 | expect(mockFetch).toHaveBeenCalledTimes(1); 69 | expect(mockFetch.mock.calls[0][0]).toEqual(`${mockDbUrl}${mockKey}`); 70 | expect(mockFetch.mock.calls[0][1].method).toEqual("POST"); 71 | expect(mockFetch.mock.calls[0][1].headers).toEqual({ Authorization: `Bearer ${mockJwt}` }); 72 | expect(mockFetch.mock.calls[0][1].body).toEqual(JSON.stringify(mockValue)); 73 | }); 74 | 75 | it("should correctly store a value to a local file", async () => { 76 | const mockValue = 101; 77 | 78 | mockHasOwnProperty.mockReturnValueOnce(true); 79 | await persistenceHelper.persist(mockValue, mockKey); 80 | 81 | expect(mockHasOwnProperty).toHaveBeenCalledTimes(1); 82 | expect(mockFetchJwt).not.toHaveBeenCalled(); 83 | expect(mockFetch).not.toHaveBeenCalled(); 84 | 85 | expect(existsSync("mock-test-key")).toBeDefined(); 86 | }); 87 | 88 | it("should fail to POST a value to the database", async () => { 89 | const mockValue = 202; 90 | 91 | const mockResponseInit = { status: 305 }; 92 | const mockPostMethodResponse = { data: "4234" }; 93 | const mockFetchResponse = new Response(JSON.stringify(mockPostMethodResponse), mockResponseInit); 94 | 95 | mockHasOwnProperty.mockReturnValueOnce(false); 96 | mockFetchJwt.mockResolvedValueOnce(mockJwt); 97 | mockFetch.mockResolvedValueOnce(mockFetchResponse); 98 | const spy = jest.spyOn(console, "log").mockImplementation(() => {}); 99 | 100 | await persistenceHelper.persist(mockValue, mockKey); 101 | expect(spy).not.toHaveBeenCalledWith("successfully persisted 202 to database"); 102 | 103 | expect(mockHasOwnProperty).toHaveBeenCalledTimes(1); 104 | expect(mockFetchJwt).toHaveBeenCalledTimes(1); 105 | expect(mockFetch).toHaveBeenCalledTimes(1); 106 | }); 107 | 108 | it("should correctly load variable values from the database", async () => { 109 | const mockData = 4234; 110 | 111 | const mockResponseInit = { status: 207 }; 112 | const mockPostMethodResponse = mockData.toString(); 113 | const mockFetchResponse = new Response(JSON.stringify(mockPostMethodResponse), mockResponseInit); 114 | 115 | mockHasOwnProperty.mockReturnValueOnce(false); 116 | mockFetchJwt.mockResolvedValueOnce(mockJwt); 117 | mockFetch.mockResolvedValueOnce(mockFetchResponse); 118 | 119 | const fetchedValue = await persistenceHelper.load(mockKey); 120 | expect(fetchedValue).toStrictEqual(4234); 121 | }); 122 | 123 | it("should fail to load values from the database, but return zero", async () => { 124 | const mockData = 4234; 125 | 126 | const mockResponseInit = { status: 308 }; 127 | const mockPostMethodResponse = mockData.toString(); 128 | const mockFetchResponse = new Response(JSON.stringify(mockPostMethodResponse), mockResponseInit); 129 | 130 | mockHasOwnProperty.mockReturnValueOnce(false); 131 | mockFetchJwt.mockResolvedValueOnce(mockJwt); 132 | mockFetch.mockResolvedValueOnce(mockFetchResponse); 133 | 134 | const fetchedValue = await persistenceHelper.load(mockKey); 135 | expect(fetchedValue).toStrictEqual(0); 136 | }); 137 | 138 | it("should correctly load values from a local file if it exists", async () => { 139 | const mockData = 4234; 140 | 141 | writeFileSync(mockKey, mockData.toString()); 142 | 143 | mockHasOwnProperty.mockReturnValueOnce(true); 144 | expect(mockFetchJwt).not.toHaveBeenCalled(); 145 | expect(mockFetch).not.toHaveBeenCalled(); 146 | 147 | const fetchedValue = await persistenceHelper.load(mockKey); 148 | expect(fetchedValue).toStrictEqual(4234); 149 | }); 150 | 151 | it("should fail load values from a local file if it doesn't exist, but return 0", async () => { 152 | mockHasOwnProperty.mockReturnValueOnce(true); 153 | expect(mockFetchJwt).not.toHaveBeenCalled(); 154 | expect(mockFetch).not.toHaveBeenCalled(); 155 | 156 | const fetchedValue = await persistenceHelper.load(mockKey); 157 | expect(fetchedValue).toStrictEqual(0); 158 | }); 159 | }); 160 | -------------------------------------------------------------------------------- /large-balance-decrease/src/persistence.helper.spec.js: -------------------------------------------------------------------------------- 1 | const { PersistenceHelper } = require("./persistence.helper"); 2 | const { existsSync, writeFileSync, unlinkSync } = require("fs"); 3 | const fetch = require("node-fetch"); 4 | 5 | const { Response } = jest.requireActual("node-fetch"); 6 | jest.mock("node-fetch"); 7 | 8 | const mockDbUrl = "databaseurl.com/"; 9 | const mockJwt = "MOCK_JWT"; 10 | const mockKey = "mock-test-key"; 11 | 12 | // Mock environment variables 13 | const mockHasOwnProperty = jest.fn(); 14 | process.env = { 15 | hasOwnProperty: mockHasOwnProperty, 16 | }; 17 | 18 | // Mock the fetchJwt function of the forta-agent module 19 | const mockFetchJwt = jest.fn(); 20 | jest.mock("forta-agent", () => { 21 | const original = jest.requireActual("forta-agent"); 22 | return { 23 | ...original, 24 | fetchJwt: () => mockFetchJwt(), 25 | }; 26 | }); 27 | 28 | const removePersistentState = () => { 29 | if (existsSync(mockKey)) { 30 | unlinkSync(mockKey); 31 | } 32 | }; 33 | 34 | describe("Persistence Helper test suite", () => { 35 | let persistenceHelper; 36 | let mockFetch = jest.mocked(fetch, true); 37 | 38 | beforeAll(() => { 39 | persistenceHelper = new PersistenceHelper(mockDbUrl); 40 | }); 41 | 42 | beforeEach(() => { 43 | removePersistentState(); 44 | }); 45 | 46 | afterEach(() => { 47 | jest.clearAllMocks(); 48 | }); 49 | 50 | it("should correctly POST a value to the database", async () => { 51 | const mockValue = 101; 52 | 53 | const mockResponseInit = { status: 202 }; 54 | const mockPostMethodResponse = { data: "4234" }; 55 | const mockFetchResponse = new Response(JSON.stringify(mockPostMethodResponse), mockResponseInit); 56 | 57 | mockHasOwnProperty.mockReturnValueOnce(false); 58 | mockFetchJwt.mockResolvedValueOnce(mockJwt); 59 | mockFetch.mockResolvedValueOnce(Promise.resolve(mockFetchResponse)); 60 | 61 | const spy = jest.spyOn(console, "log").mockImplementation(() => {}); 62 | await persistenceHelper.persist(mockValue, mockKey); 63 | 64 | expect(spy).toHaveBeenCalledWith("successfully persisted 101 to database"); 65 | expect(mockHasOwnProperty).toHaveBeenCalledTimes(1); 66 | expect(mockFetchJwt).toHaveBeenCalledTimes(1); 67 | expect(mockFetch).toHaveBeenCalledTimes(1); 68 | expect(mockFetch.mock.calls[0][0]).toEqual(`${mockDbUrl}${mockKey}`); 69 | expect(mockFetch.mock.calls[0][1].method).toEqual("POST"); 70 | expect(mockFetch.mock.calls[0][1].headers).toEqual({ Authorization: `Bearer ${mockJwt}` }); 71 | expect(mockFetch.mock.calls[0][1].body).toEqual(JSON.stringify(mockValue)); 72 | }); 73 | 74 | it("should correctly store a value to a local file", async () => { 75 | const mockValue = 101; 76 | 77 | mockHasOwnProperty.mockReturnValueOnce(true); 78 | await persistenceHelper.persist(mockValue, mockKey); 79 | 80 | expect(mockHasOwnProperty).toHaveBeenCalledTimes(1); 81 | expect(mockFetchJwt).not.toHaveBeenCalled(); 82 | expect(mockFetch).not.toHaveBeenCalled(); 83 | 84 | expect(existsSync("mock-test-key")).toBeDefined(); 85 | }); 86 | 87 | it("should fail to POST a value to the database", async () => { 88 | const mockValue = 202; 89 | 90 | const mockResponseInit = { status: 305 }; 91 | const mockPostMethodResponse = { data: "4234" }; 92 | const mockFetchResponse = new Response(JSON.stringify(mockPostMethodResponse), mockResponseInit); 93 | 94 | mockHasOwnProperty.mockReturnValueOnce(false); 95 | mockFetchJwt.mockResolvedValueOnce(mockJwt); 96 | mockFetch.mockResolvedValueOnce(mockFetchResponse); 97 | const spy = jest.spyOn(console, "log").mockImplementation(() => {}); 98 | 99 | await persistenceHelper.persist(mockValue, mockKey); 100 | expect(spy).not.toHaveBeenCalledWith("successfully persisted 202 to database"); 101 | 102 | expect(mockHasOwnProperty).toHaveBeenCalledTimes(1); 103 | expect(mockFetchJwt).toHaveBeenCalledTimes(1); 104 | expect(mockFetch).toHaveBeenCalledTimes(1); 105 | }); 106 | 107 | it("should correctly load variable values from the database", async () => { 108 | const mockData = 4234; 109 | 110 | const mockResponseInit = { status: 207 }; 111 | const mockPostMethodResponse = mockData.toString(); 112 | const mockFetchResponse = new Response(JSON.stringify(mockPostMethodResponse), mockResponseInit); 113 | 114 | mockHasOwnProperty.mockReturnValueOnce(false); 115 | mockFetchJwt.mockResolvedValueOnce(mockJwt); 116 | mockFetch.mockResolvedValueOnce(mockFetchResponse); 117 | 118 | const fetchedValue = await persistenceHelper.load(mockKey); 119 | expect(fetchedValue).toStrictEqual(4234); 120 | }); 121 | 122 | it("should fail to load values from the database, but return zero", async () => { 123 | const mockData = 4234; 124 | 125 | const mockResponseInit = { status: 308 }; 126 | const mockPostMethodResponse = mockData.toString(); 127 | const mockFetchResponse = new Response(JSON.stringify(mockPostMethodResponse), mockResponseInit); 128 | 129 | mockHasOwnProperty.mockReturnValueOnce(false); 130 | mockFetchJwt.mockResolvedValueOnce(mockJwt); 131 | mockFetch.mockResolvedValueOnce(mockFetchResponse); 132 | 133 | const fetchedValue = await persistenceHelper.load(mockKey); 134 | expect(fetchedValue).toStrictEqual(0); 135 | }); 136 | 137 | it("should correctly load values from a local file if it exists", async () => { 138 | const mockData = 4234; 139 | 140 | writeFileSync(mockKey, mockData.toString()); 141 | 142 | mockHasOwnProperty.mockReturnValueOnce(true); 143 | expect(mockFetchJwt).not.toHaveBeenCalled(); 144 | expect(mockFetch).not.toHaveBeenCalled(); 145 | 146 | const fetchedValue = await persistenceHelper.load(mockKey); 147 | expect(fetchedValue).toStrictEqual(4234); 148 | }); 149 | 150 | it("should fail load values from a local file if it doesn't exist, but return 0", async () => { 151 | mockHasOwnProperty.mockReturnValueOnce(true); 152 | expect(mockFetchJwt).not.toHaveBeenCalled(); 153 | expect(mockFetch).not.toHaveBeenCalled(); 154 | 155 | const fetchedValue = await persistenceHelper.load(mockKey); 156 | expect(fetchedValue).toStrictEqual(0); 157 | }); 158 | }); 159 | -------------------------------------------------------------------------------- /flashbots-transactions-detector/src/persistence.helper.spec.js: -------------------------------------------------------------------------------- 1 | const { PersistenceHelper } = require("./persistence.helper"); 2 | const { existsSync, writeFileSync, unlinkSync } = require("fs"); 3 | const fetch = require("node-fetch"); 4 | 5 | const { Response } = jest.requireActual("node-fetch"); 6 | jest.mock("node-fetch"); 7 | 8 | const mockDbUrl = "databaseurl.com/"; 9 | const mockJwt = "MOCK_JWT"; 10 | const mockKey = "mock-test-key"; 11 | 12 | // Mock environment variables 13 | const mockHasOwnProperty = jest.fn(); 14 | process.env = { 15 | hasOwnProperty: mockHasOwnProperty, 16 | }; 17 | 18 | // Mock the fetchJwt function of the forta-agent module 19 | const mockFetchJwt = jest.fn(); 20 | jest.mock("forta-agent", () => { 21 | const original = jest.requireActual("forta-agent"); 22 | return { 23 | ...original, 24 | fetchJwt: () => mockFetchJwt(), 25 | }; 26 | }); 27 | 28 | const removePersistentState = () => { 29 | if (existsSync(mockKey)) { 30 | unlinkSync(mockKey); 31 | } 32 | }; 33 | 34 | describe("Persistence Helper test suite", () => { 35 | let persistenceHelper; 36 | let mockFetch = jest.mocked(fetch, true); 37 | 38 | beforeAll(() => { 39 | persistenceHelper = new PersistenceHelper(mockDbUrl); 40 | }); 41 | 42 | beforeEach(() => { 43 | removePersistentState(); 44 | }); 45 | 46 | afterEach(() => { 47 | jest.clearAllMocks(); 48 | }); 49 | 50 | it("should correctly POST a value to the database", async () => { 51 | const mockValue = 101; 52 | 53 | const mockResponseInit = { status: 202 }; 54 | const mockPostMethodResponse = { data: "4234" }; 55 | const mockFetchResponse = new Response(JSON.stringify(mockPostMethodResponse), mockResponseInit); 56 | 57 | mockHasOwnProperty.mockReturnValueOnce(false); 58 | mockFetchJwt.mockResolvedValueOnce(mockJwt); 59 | mockFetch.mockResolvedValueOnce(Promise.resolve(mockFetchResponse)); 60 | 61 | const spy = jest.spyOn(console, "log").mockImplementation(() => {}); 62 | await persistenceHelper.persist(mockValue, mockKey); 63 | 64 | expect(spy).toHaveBeenCalledWith("successfully persisted 101 to database"); 65 | expect(mockHasOwnProperty).toHaveBeenCalledTimes(1); 66 | expect(mockFetchJwt).toHaveBeenCalledTimes(1); 67 | expect(mockFetch).toHaveBeenCalledTimes(1); 68 | expect(mockFetch.mock.calls[0][0]).toEqual(`${mockDbUrl}${mockKey}`); 69 | expect(mockFetch.mock.calls[0][1].method).toEqual("POST"); 70 | expect(mockFetch.mock.calls[0][1].headers).toEqual({ Authorization: `Bearer ${mockJwt}` }); 71 | expect(mockFetch.mock.calls[0][1].body).toEqual(JSON.stringify(mockValue)); 72 | }); 73 | 74 | it("should correctly store a value to a local file", async () => { 75 | const mockValue = 101; 76 | 77 | mockHasOwnProperty.mockReturnValueOnce(true); 78 | await persistenceHelper.persist(mockValue, mockKey); 79 | 80 | expect(mockHasOwnProperty).toHaveBeenCalledTimes(1); 81 | expect(mockFetchJwt).not.toHaveBeenCalled(); 82 | expect(mockFetch).not.toHaveBeenCalled(); 83 | 84 | expect(existsSync("mock-test-key")).toBeDefined(); 85 | }); 86 | 87 | it("should fail to POST a value to the database", async () => { 88 | const mockValue = 202; 89 | 90 | const mockResponseInit = { status: 305 }; 91 | const mockPostMethodResponse = { data: "4234" }; 92 | const mockFetchResponse = new Response(JSON.stringify(mockPostMethodResponse), mockResponseInit); 93 | 94 | mockHasOwnProperty.mockReturnValueOnce(false); 95 | mockFetchJwt.mockResolvedValueOnce(mockJwt); 96 | mockFetch.mockResolvedValueOnce(mockFetchResponse); 97 | const spy = jest.spyOn(console, "log").mockImplementation(() => {}); 98 | 99 | await persistenceHelper.persist(mockValue, mockKey); 100 | expect(spy).not.toHaveBeenCalledWith("successfully persisted 202 to database"); 101 | 102 | expect(mockHasOwnProperty).toHaveBeenCalledTimes(1); 103 | expect(mockFetchJwt).toHaveBeenCalledTimes(1); 104 | expect(mockFetch).toHaveBeenCalledTimes(1); 105 | }); 106 | 107 | it("should correctly load variable values from the database", async () => { 108 | const mockData = 4234; 109 | 110 | const mockResponseInit = { status: 207 }; 111 | const mockPostMethodResponse = mockData.toString(); 112 | const mockFetchResponse = new Response(JSON.stringify(mockPostMethodResponse), mockResponseInit); 113 | 114 | mockHasOwnProperty.mockReturnValueOnce(false); 115 | mockFetchJwt.mockResolvedValueOnce(mockJwt); 116 | mockFetch.mockResolvedValueOnce(mockFetchResponse); 117 | 118 | const fetchedValue = await persistenceHelper.load(mockKey); 119 | expect(fetchedValue).toStrictEqual(4234); 120 | }); 121 | 122 | it("should fail to load values from the database, but return zero", async () => { 123 | const mockData = 4234; 124 | 125 | const mockResponseInit = { status: 308 }; 126 | const mockPostMethodResponse = mockData.toString(); 127 | const mockFetchResponse = new Response(JSON.stringify(mockPostMethodResponse), mockResponseInit); 128 | 129 | mockHasOwnProperty.mockReturnValueOnce(false); 130 | mockFetchJwt.mockResolvedValueOnce(mockJwt); 131 | mockFetch.mockResolvedValueOnce(mockFetchResponse); 132 | 133 | const fetchedValue = await persistenceHelper.load(mockKey); 134 | expect(fetchedValue).toStrictEqual(0); 135 | }); 136 | 137 | it("should correctly load values from a local file if it exists", async () => { 138 | const mockData = 4234; 139 | 140 | writeFileSync(mockKey, mockData.toString()); 141 | 142 | mockHasOwnProperty.mockReturnValueOnce(true); 143 | expect(mockFetchJwt).not.toHaveBeenCalled(); 144 | expect(mockFetch).not.toHaveBeenCalled(); 145 | 146 | const fetchedValue = await persistenceHelper.load(mockKey); 147 | expect(fetchedValue).toStrictEqual(4234); 148 | }); 149 | 150 | it("should fail load values from a local file if it doesn't exist, but return 0", async () => { 151 | mockHasOwnProperty.mockReturnValueOnce(true); 152 | expect(mockFetchJwt).not.toHaveBeenCalled(); 153 | expect(mockFetch).not.toHaveBeenCalled(); 154 | 155 | const fetchedValue = await persistenceHelper.load(mockKey); 156 | expect(fetchedValue).toStrictEqual(0); 157 | }); 158 | }); 159 | -------------------------------------------------------------------------------- /flashbots-transactions-detector/README.md: -------------------------------------------------------------------------------- 1 | # Flashbots Transactions Detection Bot 2 | 3 | ## Description 4 | 5 | This bot detects flashbots transactions. 6 | 7 | ## Supported Chains 8 | 9 | - Ethereum 10 | 11 | ## Alerts 12 | 13 | - FLASHBOTS-TRANSACTIONS 14 | 15 | - Fired when the Flashbots API flags a transaction as a flashbots transaction 16 | - Severity is always set to "low" 17 | - Type is always set to "info" 18 | - Metadata: 19 | - from - the address that initiated the tx 20 | - to - the address that was interacted with 21 | - hash - the transaction hash 22 | - blockNumber - the block number of the tx 23 | - `anomalyScore` - score of how anomalous the alert is (0-1) 24 | - Score calculated by finding amount of `FLASHBOTS-TRANSACTIONS` out of the total number of transactions processed by this bot. 25 | - Addresses contain the list of contracts that were impacted 26 | - Labels: 27 | - Label 1: 28 | - `entityType`: The type of the entity, always set to "Address" 29 | - `entity`: The Flashbots' transaction initiator EOA. 30 | - `label`: The type of the label, always set to "Attacker" 31 | - `confidence`: The confidence level of the transaction being suspicious (0-1). Always set to `0.6`. 32 | - Label 2: 33 | - `entityType`: The type of the entity, always set to "Transaction" 34 | - `entity`: The Flashbots' transaction hash 35 | - `label`: The type of the label, always set to "Suspicious" 36 | - `confidence`: The confidence level of the transaction being suspicious (0-1). Always set to `0.7`. 37 | 38 | - FLASHBOTS-TRANSACTIONS-NO-REWARD 39 | 40 | - Fired when the Flashbots API flags a transaction as a flashbots transaction where the miner reward is zero 41 | - Severity is always set to "low" 42 | - Type is always set to "info" 43 | - Metadata: 44 | - from - the address that initiated the tx 45 | - to - the address that was interacted with 46 | - hash - the transaction hash 47 | - blockNumber - the block number of the tx 48 | - `anomalyScore` - score of how anomalous the alert is (0-1) 49 | - Score calculated by finding amount of `FLASHBOTS-TRANSACTIONS` out of the total number of transactions processed by this bot. 50 | - Addresses contain the list of contracts that were impacted 51 | - Labels: 52 | - Label 1: 53 | - `entityType`: The type of the entity, always set to "Address" 54 | - `entity`: The Flashbots' transaction initiator EOA. 55 | - `label`: The type of the label, always set to "Attacker" 56 | - `confidence`: The confidence level of the transaction being suspicious (0-1). Always set to `0.6`. 57 | - Label 2: 58 | - `entityType`: The type of the entity, always set to "Transaction" 59 | - `entity`: The Flashbots' transaction hash 60 | - `label`: The type of the label, always set to "Suspicious" 61 | - `confidence`: The confidence level of the transaction being suspicious (0-1). Always set to `0.7`. 62 | 63 | - FLASHBOTS-SWAP-TRANSACTIONS 64 | 65 | - Fired when the Flashbots API flags a swap as a flashbots transaction 66 | - Severity is always set to "low" 67 | - Type is always set to "info" 68 | - Metadata: 69 | - from - the address that initiated the tx 70 | - to - the address that was interacted with 71 | - hash - the transaction hash 72 | - blockNumber - the block number of the tx 73 | - `anomalyScore` - score of how anomalous the alert is (0-1) 74 | - Score calculated by finding amount of `FLASHBOTS-TRANSACTIONS` out of the total number of transactions processed by this bot. 75 | - Addresses contain the list of contracts that were impacted 76 | - Labels: 77 | - Label 1: 78 | - `entityType`: The type of the entity, always set to "Address" 79 | - `entity`: The Flashbots' transaction initiator EOA. 80 | - `label`: The type of the label, always set to "Attacker" 81 | - `confidence`: The confidence level of the transaction being suspicious (0-1). Always set to `0.6`. 82 | - Label 2: 83 | - `entityType`: The type of the entity, always set to "Transaction" 84 | - `entity`: The Flashbots' transaction hash 85 | - `label`: The type of the label, always set to "Suspicious" 86 | - `confidence`: The confidence level of the transaction being suspicious (0-1). Always set to `0.7`. 87 | 88 | - FLASHBOTS-SWAP-TRANSACTIONS-NO-REWARD 89 | - Fired when the Flashbots API flags a swap as a flashbots transaction where the miner reward is zero 90 | - Severity is always set to "low" 91 | - Type is always set to "info" 92 | - Metadata: 93 | - from - the address that initiated the tx 94 | - to - the address that was interacted with 95 | - hash - the transaction hash 96 | - blockNumber - the block number of the tx 97 | - `anomalyScore` - score of how anomalous the alert is (0-1) 98 | - Score calculated by finding amount of `FLASHBOTS-TRANSACTIONS` out of the total number of transactions processed by this bot. 99 | - Addresses contain the list of contracts that were impacted 100 | - Labels: 101 | - Label 1: 102 | - `entityType`: The type of the entity, always set to "Address" 103 | - `entity`: The Flashbots' transaction initiator EOA. 104 | - `label`: The type of the label, always set to "Attacker" 105 | - `confidence`: The confidence level of the transaction being suspicious (0-1). Always set to `0.6`. 106 | - Label 2: 107 | - `entityType`: The type of the entity, always set to "Transaction" 108 | - `entity`: The Flashbots' transaction hash 109 | - `label`: The type of the label, always set to "Suspicious" 110 | - `confidence`: The confidence level of the transaction being suspicious (0-1). Always set to `0.7`. 111 | 112 | ## Test Data 113 | 114 | In order to test the bot's behavior, replace `flashbotsUrl` variable in `agent.js` at L4, with one of the following urls and run `npm start`. 115 | 116 | - `https://blocks.flashbots.net/v1/blocks?block_number=15725067` [Temple DAO Exploit](https://etherscan.io/tx/0x8c3f442fc6d640a6ff3ea0b12be64f1d4609ea94edd2966f42c01cd9bdcf04b5) 117 | - `https://blocks.flashbots.net/v1/blocks?block_number=15794364` [Olympus DAO Exploit](https://etherscan.io/tx/0x3ed75df83d907412af874b7998d911fdf990704da87c2b1a8cf95ca5d21504cf) 118 | -------------------------------------------------------------------------------- /asset-drained/LICENSE: -------------------------------------------------------------------------------- 1 | Forta Bot License 1.0 2 | --------------------- 3 | This Detection Bot License (“Agreement”) governs your use of the detection bot script and associated documentation files made available by each applicable Developer (as defined below) through the Forta Network (“Detection Bot”). The “Forta Network” means 4 | the collection of smart contracts found at https://github.com/forta-network/forta-contracts that are in production on the Polygon blockchain from time to time. 5 | 6 | 1. Detection Bot License. 7 | (a) The legal person or entity controlling the blockchain address listed as the “Owner” of the Detection Bot (“Developer”) hereby grants you a perpetual (subject to Sections 2 and 6), worldwide, non-exclusive, non-transferable, non-sublicensable, right to access and use the Detection Bot solely in connection with participating in the Forta Network, including, without limitation, to run the Detection Bot on a node in the Forta Network, in accordance with the terms of this Agreement. 8 | 9 | (b) The Developer hereby grants you a perpetual (subject to Sections 2 and 6), worldwide, non-exclusive, non-transferable, sublicensable, right to access and use any alerts or other data generated by the Detection Bot (the “Detection Bot Results”), in accordance with the terms of this Agreement. 10 | 11 | (c) This license shall apply to the Detection Bot so long as it is registered in the Forta Network bot registry smart contract, currently found at the blockchain address 0x61447385B019187daa48e91c55c02AF1F1f3F863, as may be updated from time to time, and has sufficient FORT staked in the Forta Network staking contract, currently found at 0xd2863157539b1D11F39ce23fC4834B62082F6874, as may be updated from time to time. 12 | 13 | (d) The foregoing licenses are contingent on the payment of applicable fees, if any, published or stated at docs.forta.network. You agree to comply with the terms and conditions of any subscription level or fee tier you select and for clarity, any breach thereof shall constitute a material breach of this Agreement and the licenses contained in this Agreement shall immediately terminate in accordance with Section 6 below. For the avoidance of doubt, you are not authorized to make the Detection Bot available to third parties or to sell or otherwise distribute the Detection Bot Results, other than in accordance with your subscription level or fee tier. 14 | 15 | (e) You acknowledge that the Detection Bot and Detection Bot Results, and all intellectual property rights therein, including those rights now known or hereafter developed or discovered, are the exclusive property of Developer or its licensors and that the license contemplated herein grants you no title or rights of ownership in the Detection Bot or Detection Bot Results or any components thereof or any other right or license to the foregoing, other than as explicitly set forth herein. Notwithstanding anything to the contrary in this Agreement, the Detection Bot may include software components provided by a third party that are subject to separate license terms, in which case those license terms will govern such software components. 16 | 17 | 2. Availability of the Detection Bot. 18 | Developer reserves the right to change, revise, update, suspend, discontinue, or otherwise modify the Detection Bot at any time, which may impact the Detection Bot Results. You agree that Developer has no liability whatsoever for any loss or damage caused by your inability to access or use the Detection Bot or Detection Bot Results. Nothing in this Agreement will be construed to obligate Developer to maintain or support the Detection Bot or to supply any corrections, updates, or releases in connection therewith. 19 | 20 | 3. Assumption of Risk. 21 | (a) You acknowledge that there are risks associated with the Detection Bot, including that the Detection Bot Results may not be accurate, and you expressly acknowledge and assume all risks. You further acknowledge that Developer cannot confirm the accuracy of the Detection Bot Results and Developer therefore is not responsible for any consequences related to or negative impacts arising from inaccurate, false, or incomplete Detection Bot Results. You understand and agree that the Detection Bot and Detection Bot Results are offered on a purely non-reliance basis and at your own risk. You further acknowledge that the Detection Bot and Detection Bot Results are offered through the Forta Network, which is a decentralized network of independent node operators and other bot developers that interact on a public blockchain over which Developer has no control. 22 | 23 | (b) You acknowledge that the regulatory regime governing blockchain technologies is uncertain and continually evolving, and new laws, regulations or policies may negatively impact the potential utility of the Detection Bot or Detection Bot Results, and you assume such risk. 24 | 25 | 4. Release. 26 | You hereby release Developer from any liability, loss or damage of any nature arising from any risk you assume pursuant to this section, as well as from any liability, loss or damage arising from use of the Detection Bot or reliance on the Detection Bot Results. 27 | 28 | 5. Limitation of Liability. 29 | (a) DISCLAIMER. THE DETECTION BOT AND DETECTION BOT RESULTS ARE EACH PROVIDED ON AN “AS IS” AND “AS AVAILABLE” BASIS. YOU AGREE THAT YOUR USE OF THE DETECTION BOT AND DETECTION BOT RESULTS WILL BE AT YOUR SOLE RISK. TO THE FULLEST EXTENT PERMITTED BY LAW, DEVELOPER DISCLAIMS ALL WARRANTIES OF ANY KIND, EXPRESS OR IMPLIED, IN CONNECTION WITH THE DETECTION BOT AND THE DETECTION BOT RESULTS AND YOUR USE THEREOF, INCLUDING, BUT NOT LIMITED TO, IMPLIED WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, NONINFRINGEMENT OR TECHNICAL OPERATION OR PERFORMANCE. DEVELOPER ALSO MAKES NO WARRANTIES OR REPRESENTATIONS ABOUT THE ACCURACY OR COMPLETENESS OF THE DETECTION BOT RESULTS. 30 | 31 | (b) NO CONSEQUENTIAL DAMAGES. IN NO EVENT SHALL DEVELOPER BE LIABLE TO YOU UNDER THIS AGREEMENT (WHETHER IN TORT, IN STRICT LIABILITY, IN CONTRACT, OR OTHERWISE) FOR ANY (I) INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES, INCLUDING DAMAGES FOR LOST PROFITS, EVEN IF DEVELOPER HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES, OR (II) DAMAGES THAT EXCEED $100. THE EXISTENCE OF MORE THAN ONE CLAIM WILL NOT ENLARGE OR EXTEND THESE LIMITS. 32 | 33 | 6. Termination. 34 | If you materially breach this Agreement or if you violate any applicable law or regulation, you acknowledge you are prohibited from using the Detection Bot or Detection Bot results thereafter, even if you may be acting on behalf of a third party. -------------------------------------------------------------------------------- /ice-phishing/LICENSE: -------------------------------------------------------------------------------- 1 | Forta Bot License 1.0 2 | --------------------- 3 | This Detection Bot License (“Agreement”) governs your use of the detection bot script and associated documentation files made available by each applicable Developer (as defined below) through the Forta Network (“Detection Bot”). The “Forta Network” means 4 | the collection of smart contracts found at https://github.com/forta-network/forta-contracts that are in production on the Polygon blockchain from time to time. 5 | 6 | 1. Detection Bot License. 7 | (a) The legal person or entity controlling the blockchain address listed as the “Owner” of the Detection Bot (“Developer”) hereby grants you a perpetual (subject to Sections 2 and 6), worldwide, non-exclusive, non-transferable, non-sublicensable, right to access and use the Detection Bot solely in connection with participating in the Forta Network, including, without limitation, to run the Detection Bot on a node in the Forta Network, in accordance with the terms of this Agreement. 8 | 9 | (b) The Developer hereby grants you a perpetual (subject to Sections 2 and 6), worldwide, non-exclusive, non-transferable, sublicensable, right to access and use any alerts or other data generated by the Detection Bot (the “Detection Bot Results”), in accordance with the terms of this Agreement. 10 | 11 | (c) This license shall apply to the Detection Bot so long as it is registered in the Forta Network bot registry smart contract, currently found at the blockchain address 0x61447385B019187daa48e91c55c02AF1F1f3F863, as may be updated from time to time, and has sufficient FORT staked in the Forta Network staking contract, currently found at 0xd2863157539b1D11F39ce23fC4834B62082F6874, as may be updated from time to time. 12 | 13 | (d) The foregoing licenses are contingent on the payment of applicable fees, if any, published or stated at docs.forta.network. You agree to comply with the terms and conditions of any subscription level or fee tier you select and for clarity, any breach thereof shall constitute a material breach of this Agreement and the licenses contained in this Agreement shall immediately terminate in accordance with Section 6 below. For the avoidance of doubt, you are not authorized to make the Detection Bot available to third parties or to sell or otherwise distribute the Detection Bot Results, other than in accordance with your subscription level or fee tier. 14 | 15 | (e) You acknowledge that the Detection Bot and Detection Bot Results, and all intellectual property rights therein, including those rights now known or hereafter developed or discovered, are the exclusive property of Developer or its licensors and that the license contemplated herein grants you no title or rights of ownership in the Detection Bot or Detection Bot Results or any components thereof or any other right or license to the foregoing, other than as explicitly set forth herein. Notwithstanding anything to the contrary in this Agreement, the Detection Bot may include software components provided by a third party that are subject to separate license terms, in which case those license terms will govern such software components. 16 | 17 | 2. Availability of the Detection Bot. 18 | Developer reserves the right to change, revise, update, suspend, discontinue, or otherwise modify the Detection Bot at any time, which may impact the Detection Bot Results. You agree that Developer has no liability whatsoever for any loss or damage caused by your inability to access or use the Detection Bot or Detection Bot Results. Nothing in this Agreement will be construed to obligate Developer to maintain or support the Detection Bot or to supply any corrections, updates, or releases in connection therewith. 19 | 20 | 3. Assumption of Risk. 21 | (a) You acknowledge that there are risks associated with the Detection Bot, including that the Detection Bot Results may not be accurate, and you expressly acknowledge and assume all risks. You further acknowledge that Developer cannot confirm the accuracy of the Detection Bot Results and Developer therefore is not responsible for any consequences related to or negative impacts arising from inaccurate, false, or incomplete Detection Bot Results. You understand and agree that the Detection Bot and Detection Bot Results are offered on a purely non-reliance basis and at your own risk. You further acknowledge that the Detection Bot and Detection Bot Results are offered through the Forta Network, which is a decentralized network of independent node operators and other bot developers that interact on a public blockchain over which Developer has no control. 22 | 23 | (b) You acknowledge that the regulatory regime governing blockchain technologies is uncertain and continually evolving, and new laws, regulations or policies may negatively impact the potential utility of the Detection Bot or Detection Bot Results, and you assume such risk. 24 | 25 | 4. Release. 26 | You hereby release Developer from any liability, loss or damage of any nature arising from any risk you assume pursuant to this section, as well as from any liability, loss or damage arising from use of the Detection Bot or reliance on the Detection Bot Results. 27 | 28 | 5. Limitation of Liability. 29 | (a) DISCLAIMER. THE DETECTION BOT AND DETECTION BOT RESULTS ARE EACH PROVIDED ON AN “AS IS” AND “AS AVAILABLE” BASIS. YOU AGREE THAT YOUR USE OF THE DETECTION BOT AND DETECTION BOT RESULTS WILL BE AT YOUR SOLE RISK. TO THE FULLEST EXTENT PERMITTED BY LAW, DEVELOPER DISCLAIMS ALL WARRANTIES OF ANY KIND, EXPRESS OR IMPLIED, IN CONNECTION WITH THE DETECTION BOT AND THE DETECTION BOT RESULTS AND YOUR USE THEREOF, INCLUDING, BUT NOT LIMITED TO, IMPLIED WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, NONINFRINGEMENT OR TECHNICAL OPERATION OR PERFORMANCE. DEVELOPER ALSO MAKES NO WARRANTIES OR REPRESENTATIONS ABOUT THE ACCURACY OR COMPLETENESS OF THE DETECTION BOT RESULTS. 30 | 31 | (b) NO CONSEQUENTIAL DAMAGES. IN NO EVENT SHALL DEVELOPER BE LIABLE TO YOU UNDER THIS AGREEMENT (WHETHER IN TORT, IN STRICT LIABILITY, IN CONTRACT, OR OTHERWISE) FOR ANY (I) INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES, INCLUDING DAMAGES FOR LOST PROFITS, EVEN IF DEVELOPER HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES, OR (II) DAMAGES THAT EXCEED $100. THE EXISTENCE OF MORE THAN ONE CLAIM WILL NOT ENLARGE OR EXTEND THESE LIMITS. 32 | 33 | 6. Termination. 34 | If you materially breach this Agreement or if you violate any applicable law or regulation, you acknowledge you are prohibited from using the Detection Bot or Detection Bot results thereafter, even if you may be acting on behalf of a third party. 35 | --------------------------------------------------------------------------------