Skip to content

Commit

Permalink
Use alert rate pkg for large transfer out
Browse files Browse the repository at this point in the history
  • Loading branch information
0xtaf committed Apr 20, 2023
1 parent 3aaf8d8 commit 9788885
Show file tree
Hide file tree
Showing 4 changed files with 58 additions and 51 deletions.
3 changes: 2 additions & 1 deletion large-transfer-out-py/requirements.txt
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
forta_agent>=0.1.9
setuptools>=61.3.1
setuptools>=61.3.1
bot-alert-rate>=0.0.4
4 changes: 2 additions & 2 deletions large-transfer-out-py/requirements_dev.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
-r requirements.txt
pytest==6.2.5
pytest-env==0.6.2
pytest==7.2.1
pytest-env==0.6.2
47 changes: 21 additions & 26 deletions large-transfer-out-py/src/agent.py
Original file line number Diff line number Diff line change
@@ -1,57 +1,47 @@
from src.keys import BOT_ID
from os import environ
import forta_agent
from forta_agent import Finding, FindingType, FindingSeverity, get_json_rpc_url, EntityType
from src.constants import THRESHOLDS, DAY_LOOKBACK_WINDOW
from web3 import Web3
from bot_alert_rate import calculate_alert_rate, ScanCountType
from src.keys import ZETTABLOCK_KEY

web3 = Web3(Web3.HTTPProvider(get_json_rpc_url()))
CHAIN_ID = -1
ALERT_COUNT = 0 # stats to emit anomaly score
DENOMINATOR_COUNT = 0 # stats to emit anomaly score

def initialize():
"""
this function initializes the state variables that are tracked across tx and blocks
it is called from test to reset state between tests
"""
global ALERT_COUNT
ALERT_COUNT = 0

global DENOMINATOR_COUNT
DENOMINATOR_COUNT = 0
def initialize():
environ["ZETTABLOCK_API_KEY"] = ZETTABLOCK_KEY


def detect_suspicious_native_transfers(w3, transaction_event: forta_agent.transaction_event.TransactionEvent) -> list:
findings = []
global CHAIN_ID
global ALERT_COUNT
global DENOMINATOR_COUNT
if CHAIN_ID == -1:
CHAIN_ID = w3.eth.chainId

# filter the transaction logs for any Tether transfers
value = transaction_event.transaction.value
if value > 0:
DENOMINATOR_COUNT += 1

if value >= THRESHOLDS[CHAIN_ID][1]:
to = transaction_event.to
from_ = transaction_event.from_

block_number = transaction_event.block_number
BLOCK_TIME = 15 # seconds
older_block_number = block_number - (24 * 60 * 60 * DAY_LOOKBACK_WINDOW)//BLOCK_TIME
older_value = w3.eth.get_balance(Web3.toChecksumAddress(from_), block_identifier=older_block_number)
current_value = w3.eth.get_balance(Web3.toChecksumAddress(from_), block_identifier=block_number)
older_block_number = block_number - \
(24 * 60 * 60 * DAY_LOOKBACK_WINDOW)//BLOCK_TIME
older_value = w3.eth.get_balance(Web3.toChecksumAddress(
from_), block_identifier=older_block_number)
current_value = w3.eth.get_balance(
Web3.toChecksumAddress(from_), block_identifier=block_number)

if older_value < THRESHOLDS[CHAIN_ID][0]:
ALERT_COUNT += 1

anomaly_score = (ALERT_COUNT * 1.0) / DENOMINATOR_COUNT

labels = [{"entity": from_,
"entity_type": EntityType.Address,
"label": "attacker",
"confidence": 0.3}]
"entity_type": EntityType.Address,
"label": "attacker",
"confidence": 0.3}]

findings.append(Finding({
'name': 'Large Native Transfer Out',
Expand All @@ -60,7 +50,12 @@ def detect_suspicious_native_transfers(w3, transaction_event: forta_agent.transa
'severity': FindingSeverity.Low,
'type': FindingType.Info,
'metadata': {
'anomaly_score': anomaly_score,
'anomaly_score': calculate_alert_rate(
CHAIN_ID,
BOT_ID,
'LARGE-TRANSFER-OUT',
ScanCountType.TRANSFER_COUNT,
),
'to': to,
'from': from_,
'current_block': block_number,
Expand Down
55 changes: 33 additions & 22 deletions large-transfer-out-py/src/agent_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,9 @@
w3 = Web3Mock()
real_w3 = Web3(Web3.HTTPProvider(get_json_rpc_url()))


class TestLargeTransferOut:

def test_large_transfer_no_alert(self):
agent.initialize()

Expand All @@ -27,14 +28,16 @@ def test_large_transfer_no_alert(self):
})

findings = agent.detect_suspicious_native_transfers(w3, tx_event)
assert len(findings) == 0, "this should have not triggered a finding as the account had assets 1 day ago"
assert len(
findings) == 0, "this should have not triggered a finding as the account had assets 1 day ago"

def test_preformance(self):
agent.initialize()

global real_w3
tx = real_w3.eth.get_transaction('0x39ed9312dabfe228ab03659192540da18b97f89eb7b89abaa9a6da03011e9668')

tx = real_w3.eth.get_transaction(
'0x39ed9312dabfe228ab03659192540da18b97f89eb7b89abaa9a6da03011e9668')

global large_transfer_tx_event
large_transfer_tx_event = create_transaction_event({
'transaction': {
Expand All @@ -50,7 +53,8 @@ def test_preformance(self):
'logs': []}
})

tx = real_w3.eth.get_transaction('0xc8a4877b4b3ed9e1cbd22bcbd8d6f6e78b8d70e96475dfa4a4b9751bf0c08a29')
tx = real_w3.eth.get_transaction(
'0xc8a4877b4b3ed9e1cbd22bcbd8d6f6e78b8d70e96475dfa4a4b9751bf0c08a29')
global small_transfer_tx_event
small_transfer_tx_event = create_transaction_event({
'transaction': {
Expand All @@ -74,22 +78,25 @@ def test_preformance(self):
# Arbitrum: 1s, 5 -> 200ms
# Optimism: 24s, 150 -> 160ms
# Fantom: 1s, 5 -> 200ms

# we're assuming 10% of tx will contain a large transfer
# so our target for polygon is 5 tx with a large transfer and 45 without large transfers

processing_runs = 10
processing_time_large_transfers_avg_ms = timeit.timeit('agent.detect_suspicious_native_transfers(real_w3, large_transfer_tx_event)', number=processing_runs, globals=globals()) * 1000 / processing_runs

processing_time_small_transfers_ms = timeit.timeit('agent.detect_suspicious_native_transfers(real_w3, small_transfer_tx_event)', number=processing_runs, globals=globals()) * 1000 / processing_runs
assert (processing_time_large_transfers_avg_ms * 0.05 + processing_time_small_transfers_ms * 0.95)/2 < 40, "processing time should be less than 43ms. If not, this bot is unlikely to keep up with fast chains, like Polygon"
processing_time_large_transfers_avg_ms = timeit.timeit(
'agent.detect_suspicious_native_transfers(real_w3, large_transfer_tx_event)', number=processing_runs, globals=globals()) * 1000 / processing_runs

processing_time_small_transfers_ms = timeit.timeit(
'agent.detect_suspicious_native_transfers(real_w3, small_transfer_tx_event)', number=processing_runs, globals=globals()) * 1000 / processing_runs
assert (processing_time_large_transfers_avg_ms * 0.05 + processing_time_small_transfers_ms * 0.95) / \
2 < 40, "processing time should be less than 43ms. If not, this bot is unlikely to keep up with fast chains, like Polygon"

def test_gera_coin_attacker(self):
agent.initialize()

tx = real_w3.eth.get_transaction('0x39ed9312dabfe228ab03659192540da18b97f89eb7b89abaa9a6da03011e9668')

tx = real_w3.eth.get_transaction(
'0x39ed9312dabfe228ab03659192540da18b97f89eb7b89abaa9a6da03011e9668')

tx_event = create_transaction_event({
'transaction': {
'hash': tx.hash,
Expand All @@ -105,11 +112,11 @@ def test_gera_coin_attacker(self):
})

findings = agent.detect_suspicious_native_transfers(real_w3, tx_event)
assert len(findings) == 1, "the gera coin attacker tx should have been detected"
assert len(
findings) == 1, "the gera coin attacker tx should have been detected"

def test_large_transfer_alert(self):
def test_large_transfer_alert(self, mocker):
agent.initialize()

tx_event = create_transaction_event({
'transaction': {
'hash': "0",
Expand All @@ -125,10 +132,14 @@ def test_large_transfer_alert(self):
})

findings = agent.detect_suspicious_native_transfers(w3, tx_event)
assert len(findings) == 1, "this should have triggered a finding as account obtained assets within the last day"

assert findings[0].metadata["anomaly_score"] == 1.0, "should have anomaly score of 1.0"
assert findings[0].labels[0].toDict()["entity"] == ADDRESS_WITHOUT_LARGE_BALANCE, "should have EOA address as label"
assert findings[0].labels[0].toDict()["entity_type"] == EntityType.Address, "should have label_type address"
assert findings[0].labels[0].toDict()["label"] == 'attacker', "should have attacker as label"
assert findings[0].labels[0].toDict()["confidence"] == 0.3, "should have 0.3 as label confidence"
assert len(
findings) == 1, "this should have triggered a finding as account obtained assets within the last day"

assert findings[0].labels[0].toDict(
)["entity"] == ADDRESS_WITHOUT_LARGE_BALANCE, "should have EOA address as label"
assert findings[0].labels[0].toDict(
)["entity_type"] == EntityType.Address, "should have label_type address"
assert findings[0].labels[0].toDict(
)["label"] == 'attacker', "should have attacker as label"
assert findings[0].labels[0].toDict(
)["confidence"] == 0.3, "should have 0.3 as label confidence"

0 comments on commit 9788885

Please sign in to comment.