Skip to content
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 union-chain-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,301 changes: 939 additions & 1,362 deletions union-chain-funding-py/package-lock.json

Large diffs are not rendered by default.

46 changes: 23 additions & 23 deletions union-chain-funding-py/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
{
"name": "union-chain-funding",
"displayName": "Union Chain Funding",
"version": "0.0.1",
"name": "union-chain-funding-beta",
"displayName": "Union Chain Funding (beta)",
"version": "0.1.0",
"engines": {
"node": ">=20"
},
"description": "Detecting addresses being funded by Union Chain",
"repository": "https://github.com/forta-network/forta-bot-sdk/tree/main/union-chain-funding-py",
"licenseUrl": "https://github.com/forta-network/forta-bot-sdk/blob/master/starter-project/LICENSE.md",
Expand All @@ -12,28 +15,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"
}
}
8 changes: 8 additions & 0 deletions union-chain-funding-py/publish.log
Original file line number Diff line number Diff line change
@@ -1,2 +1,10 @@
Fri, 02 Feb 2024 09:31:05 GMT: successfully added agent id 0x025251bd9b67b18804249a61a19f8dd45e3dd30caba295ed2cdc9392039f6272 with manifest Qmb7n5yVufoesWrRvDMVJP3ztDu9ff49g6LXgron87UQ9v
Fri, 02 Feb 2024 09:43:52 GMT: successfully updated agent id 0x025251bd9b67b18804249a61a19f8dd45e3dd30caba295ed2cdc9392039f6272 with manifest QmcdjzJRJrrJGM4ch7aA2DVaigu4LPG3QgK7msfumi48MH
Mon, 19 Feb 2024 21:59:14 GMT: successfully updated bot id 0x76dbc4cb62a4c158d6f284f063d53f3c1230d97273596f5e9b0c2e6ac46e62d9 with manifest QmT8CmhrmZ3Up1X956u9zLvXaRpPcAhnhkwYN9JWDH4obL
Mon, 19 Feb 2024 22:15:05 GMT: successfully updated bot id 0x76dbc4cb62a4c158d6f284f063d53f3c1230d97273596f5e9b0c2e6ac46e62d9 with manifest QmfAffD7VtNijVK8kBQG1oEWReS6SkQCARbpQ4MJPz8ip8
Mon, 19 Feb 2024 22:35:33 GMT: successfully updated bot id 0x76dbc4cb62a4c158d6f284f063d53f3c1230d97273596f5e9b0c2e6ac46e62d9 with manifest QmbKjhS6GGquUHUugAviLBRKm3w4L4CujMyfUZ6naoKhvy
Tue, 20 Feb 2024 14:46:55 GMT: successfully updated bot id 0x76dbc4cb62a4c158d6f284f063d53f3c1230d97273596f5e9b0c2e6ac46e62d9 with manifest Qma75Rg73XpNmAUUjMSQeAnKsVPRrgdYvEGh8u3KtzcgWU
Tue, 20 Feb 2024 14:53:19 GMT: successfully updated bot id 0x76dbc4cb62a4c158d6f284f063d53f3c1230d97273596f5e9b0c2e6ac46e62d9 with manifest QmbBjezJTMBJdcvHcmGV4znDyCRb9vgYnj36Y8tHfEHYBY
Tue, 20 Feb 2024 15:35:21 GMT: successfully updated bot id 0x76dbc4cb62a4c158d6f284f063d53f3c1230d97273596f5e9b0c2e6ac46e62d9 with manifest QmStBYj2bvDdYrcARhaTMMsUpgoL8FdMEAaCGb29D64ncV
Tue, 20 Feb 2024 15:54:37 GMT: successfully updated bot id 0x76dbc4cb62a4c158d6f284f063d53f3c1230d97273596f5e9b0c2e6ac46e62d9 with manifest QmcZQcd9q94SkPfJrSaSRbEaQfXCeS6ARCTc9jYP2jApyA
Tue, 20 Feb 2024 16:22:25 GMT: successfully updated bot id 0x76dbc4cb62a4c158d6f284f063d53f3c1230d97273596f5e9b0c2e6ac46e62d9 with manifest QmNZ9q5f2nVjfKdD1ZLtM1xoJYte6u9dNPjENqywM2d5v3
5 changes: 3 additions & 2 deletions union-chain-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
5 changes: 3 additions & 2 deletions union-chain-funding-py/requirements_dev.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
-r requirements.txt
pytest==6.2.5
pytest-env==0.6.2
pytest==7.2.1
pytest-env==0.6.2
pytest-asyncio==0.23.4
73 changes: 42 additions & 31 deletions union-chain-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 FundingUnionChainFindings

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

# Logging set up
root = logging.getLogger()
Expand Down Expand Up @@ -38,67 +37,79 @@ 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')


def is_new_account(w3, address, block_number):
return w3.eth.get_transaction_count(Web3.toChecksumAddress(address), block_number) == 0
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 detect_union_chain_funding(w3, transaction_event):
async def detect_union_chain_funding(w3, transaction_event):
global LOW_VOL_ALERT_COUNT
global NEW_EOA_ALERT_COUNT
global DENOMINATOR_COUNT
global CHAIN_ID

print(f"Transaction hash: {transaction_event.hash}")

findings = []

native_value = transaction_event.transaction.value / 1e18

if (not transaction_event.to):
return findings

if (native_value > 0 and (native_value < UNION_CHAIN_THRESHOLD or is_new_account(w3, transaction_event.to, transaction_event.block_number)) and not is_contract(w3, transaction_event.to)):
native_value = transaction_event.transaction.value / 1e18

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 < UNION_CHAIN_THRESHOLD or is_new_acc) and not is_contr):
DENOMINATOR_COUNT += 1

"""
if the transaction is from Union Chain, 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_ == UNION_CHAIN_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_ == UNION_CHAIN_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(FundingUnionChainFindings.funding_union_chain(transaction_event, "new-eoa", score, CHAIN_ID))
elif native_value < UNION_CHAIN_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(FundingUnionChainFindings.funding_union_chain(transaction_event, "low-amount", score, CHAIN_ID))
return findings


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

return handle_transaction

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

real_handle_transaction = provide_handle_transaction(web3)

async def main():
initialize()

await asyncio.gather(
scan_ethereum({
'rpc_url': "https://eth-mainnet.g.alchemy.com/v2",
'rpc_key_id': "e698634d-79c2-44fe-adf8-f7dac20dd33c",
'local_rpc_url': "1",
'handle_transaction': handle_transaction
}),
run_health_check()
)

def handle_transaction(transaction_event):
return real_handle_transaction(transaction_event)
if __name__ == "__main__":
asyncio.run(main())
Loading