Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

eXch Funding V2 Update #559

Open
wants to merge 4 commits into
base: @v2-bots
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 7 additions & 10 deletions exch-funding-py/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,23 +1,20 @@
# Build stage: compile Python dependencies
FROM python:3.9-alpine as builder
FROM python:3.10-alpine as builder
RUN apk update
RUN apk add alpine-sdk
RUN python3 -m pip install --upgrade pip
COPY requirements.txt ./
COPY forta_bot-0.2.0.tar.gz ./
RUN python3 -m pip install --user -r requirements.txt

# Final stage: copy over Python dependencies and install production Node dependencies
FROM node:12-alpine
# this python version should match the build stage python version
RUN apk add python3
# Final stage: copy over Python dependencies
FROM python:3.10-alpine
COPY --from=builder /root/.local /root/.local
ENV PATH=/root/.local:$PATH
ENV NODE_ENV=production
# Uncomment the following line to enable agent logging
ENV FORTA_ENV=production
# Uncomment the following line to enable logging
LABEL "network.forta.settings.agent-logs.enable"="true"
WORKDIR /app
COPY ./src ./src
COPY package*.json ./
COPY LICENSE.md ./
RUN npm ci --production
CMD [ "npm", "run", "start:prod" ]
CMD [ "python3", "./src/agent.py" ]
2,290 changes: 932 additions & 1,358 deletions exch-funding-py/package-lock.json

Large diffs are not rendered by default.

52 changes: 29 additions & 23 deletions exch-funding-py/package.json
Original file line number Diff line number Diff line change
@@ -1,8 +1,17 @@
{
"name": "exch-funding",
"displayName": "eXch Funding",
"version": "0.0.1",
"name": "exch-funding-beta",
"displayName": "eXch Funding (beta)",
"version": "0.1.0",
"engines": {
"node": ">=20"
},
"description": "Detecting addresses being funded by eXch",
"chainSettings": {
"1": {
"shards": 4,
"target": 3
}
},
"repository": "https://github.com/forta-network/forta-bot-sdk/tree/main/exch-funding-py",
"licenseUrl": "https://github.com/forta-network/forta-bot-sdk/blob/master/starter-project/LICENSE.md",
"promoUrl": "https://forta.org",
Expand All @@ -12,28 +21,25 @@
"scripts": {
"postinstall": "python3 -m pip install -r requirements_dev.txt",
"start": "npm run start:dev",
"start:dev": "nodemon --watch src --watch forta.config.json -e py --exec \"forta-agent run\"",
"start:prod": "forta-agent run --prod",
"tx": "forta-agent run --tx",
"block": "forta-agent run --block",
"range": "forta-agent run --range",
"alert": "forta-agent run --alert",
"sequence": "forta-agent run --sequence",
"file": "forta-agent run --file",
"publish": "forta-agent publish",
"info": "forta-agent info",
"logs": "forta-agent logs",
"push": "forta-agent push",
"disable": "forta-agent disable",
"enable": "forta-agent enable",
"keyfile": "forta-agent keyfile",
"stake": "forta-agent stake",
"start:dev": "nodemon --watch src --watch forta.config.json -e py --exec \"python3 ./src/agent.py\"",
"start:prod": "node ./src/agent.js",
"tx": "forta-bot run --tx",
"block": "forta-bot run --block",
"range": "forta-bot run --range",
"alert": "forta-bot run --alert",
"sequence": "forta-bot run --sequence",
"file": "forta-bot run --file",
"publish": "forta-bot publish",
"info": "forta-bot info",
"logs": "forta-bot logs",
"push": "forta-bot push",
"disable": "forta-bot disable",
"enable": "forta-bot enable",
"keyfile": "forta-bot keyfile",
"stake": "forta-bot stake",
"test": "python3 -m pytest"
},
"dependencies": {
"forta-agent": "^0.1.48"
},
"devDependencies": {
"nodemon": "^2.0.8"
"forta-bot-cli": "file:forta-bot-cli-0.2.0.tgz"
}
}
5 changes: 3 additions & 2 deletions exch-funding-py/requirements.txt
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
forta_agent>=0.1.27
setuptools>=61.3.1
forta_bot-0.2.0.tar.gz
setuptools>=61.3.1
async-lru==2.0.4
3 changes: 2 additions & 1 deletion exch-funding-py/requirements_dev.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
-r requirements.txt
pytest==7.2.1
pytest-env==0.6.2
pytest-env==0.6.2
pytest-asyncio==0.23.4
74 changes: 41 additions & 33 deletions exch-funding-py/src/agent.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,14 @@
import asyncio
from web3 import AsyncWeb3
import logging
import sys

from forta_agent import get_json_rpc_url, Web3
from forta_bot import scan_ethereum, TransactionEvent, get_chain_id, run_health_check
from hexbytes import HexBytes
from functools import lru_cache
from async_lru import alru_cache

from src.constants import *
from src.findings import FundingExchFindings

# Initialize web3
web3 = Web3(Web3.HTTPProvider(get_json_rpc_url()))
from constants import *
from findings import FundingExchFindings

# Logging set up
root = logging.getLogger()
Expand Down Expand Up @@ -38,26 +37,26 @@ def initialize():
DENOMINATOR_COUNT = 0

global CHAIN_ID
CHAIN_ID = web3.eth.chain_id
CHAIN_ID = get_chain_id()


@lru_cache(maxsize=100000)
def is_contract(w3, address):
@alru_cache(maxsize=100000)
async def is_contract(w3, address):
"""
this function determines whether address is a contract
:return: is_contract: bool
"""
if address is None:
return True
code = w3.eth.get_code(Web3.toChecksumAddress(address))
code = await w3.eth.get_code(w3.to_checksum_address(address))
return code != HexBytes('0x')

async def is_new_account(w3, address, block_number):
if address is None:
return True
return await w3.eth.get_transaction_count(w3.to_checksum_address(address), block_number) == 0

def is_new_account(w3, address, block_number):
return w3.eth.get_transaction_count(Web3.toChecksumAddress(address), block_number) == 0


def detect_exch_funding(w3, transaction_event):
async def detect_exch_funding(w3, transaction_event):
global LOW_VOL_ALERT_COUNT
global NEW_EOA_ALERT_COUNT
global DENOMINATOR_COUNT
Expand All @@ -71,34 +70,43 @@ def detect_exch_funding(w3, transaction_event):
if (not transaction_event.to):
return findings

if (native_value > 0 and (native_value < EXCH_THRESHOLD or is_new_account(w3, transaction_event.to, transaction_event.block_number)) and not is_contract(w3, transaction_event.to)):
is_new_acc = await is_new_account(w3, transaction_event.to, transaction_event.block_number)
is_contr = await is_contract(w3, transaction_event.to)

if (native_value > 0 and (native_value < EXCH_THRESHOLD or is_new_acc) and not is_contr):
DENOMINATOR_COUNT += 1

"""
if the transaction is from eXch, and not to a contract: check if transaction count is 0,
else check if value sent is less than the threshold
"""
if (transaction_event.from_ == EXCH_ADDRESS and not is_contract(w3, transaction_event.to)):
if is_new_account(w3, transaction_event.to, transaction_event.block_number):
if (transaction_event.from_ == EXCH_ADDRESS and not is_contr):
if is_new_acc:
NEW_EOA_ALERT_COUNT += 1
score = (1.0 * NEW_EOA_ALERT_COUNT) / DENOMINATOR_COUNT
score = str((1.0 * NEW_EOA_ALERT_COUNT) / DENOMINATOR_COUNT)
findings.append(FundingExchFindings.funding_exch(transaction_event, "new-eoa", score, CHAIN_ID))
elif native_value < EXCH_THRESHOLD:
LOW_VOL_ALERT_COUNT += 1
score = (1.0 * LOW_VOL_ALERT_COUNT) / DENOMINATOR_COUNT
score = str((1.0 * LOW_VOL_ALERT_COUNT) / DENOMINATOR_COUNT)
findings.append(FundingExchFindings.funding_exch(transaction_event, "low-amount", score, CHAIN_ID))
return findings


def provide_handle_transaction(w3):
def handle_transaction(transaction_event):
return detect_exch_funding(w3, transaction_event)

return handle_transaction


real_handle_transaction = provide_handle_transaction(web3)


def handle_transaction(transaction_event):
return real_handle_transaction(transaction_event)
async def handle_transaction(transaction_event: TransactionEvent, web3: AsyncWeb3.AsyncHTTPProvider):
return await detect_exch_funding(web3, transaction_event)

async def main():
initialize()

await asyncio.gather(
scan_ethereum({
'rpc_url': "https://eth-mainnet.g.alchemy.com/v2",
'rpc_key_id': "420b57cc-c2cc-442c-8fd8-901d70a835a5",
'local_rpc_url': "1",
'handle_transaction': handle_transaction
}),
run_health_check()
)

if __name__ == "__main__":
asyncio.run(main())
Loading