Skip to content
Merged
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
2 changes: 2 additions & 0 deletions protocols/infinifi/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,8 @@ Governance monitoring will be monitored via Tenderly alerts on the following add
- `TIMELOCK_SHORT`: [`0x4B174afbeD7b98BA01F50E36109EEE5e6d327c32`](https://etherscan.io/address/0x4B174afbeD7b98BA01F50E36109EEE5e6d327c32)
- `TIMELOCK_LONG`: [`0x3D18480CC32B6AB3B833dCabD80E76CfD41c48a9`](https://etherscan.io/address/0x3D18480CC32B6AB3B833dCabD80E76CfD41c48a9)

For RWA escrow governance calls, the linked AI report resolves the owning farm, accounting asset, normalized `totalAssets`, and configured non-accounting ERC20 targets into a deterministic `Protocol Context` section.

**Deployer Address**:

- `0xdecaDAc8778D088A30eE811b8Cc4eE72cED9Bf22`
Expand Down
21 changes: 21 additions & 0 deletions tests/test_ai_explainer.py
Original file line number Diff line number Diff line change
Expand Up @@ -334,6 +334,27 @@ def test_sole_token_map_skips_ambiguous_targets(self) -> None:
self.assertEqual(mapping, {"0xaaa": one})


class TestProtocolContextSection(unittest.TestCase):
"""Protocol adapters can add verified facts to the prompt."""

def test_section_included(self) -> None:
calls = [DecodedCall(function_name="setRate", signature="setRate(address,uint256)")]
result = _build_prompt(
target="0xT",
value=0,
decoded_calls=calls,
simulation=None,
protocol_context="Farm: New Silver 2 Senior\nAccounting asset: USDC",
)
self.assertIn("--- Protocol Context", result)
self.assertIn("Farm: New Silver 2 Senior", result)
self.assertIn("Accounting asset: USDC", result)

def test_system_prompt_distinguishes_whitelist_from_accounting_asset(self) -> None:
self.assertIn("Distinguish", SYSTEM_INSTRUCTIONS)
self.assertIn("non-accounting ERC20 targets", SYSTEM_INSTRUCTIONS)


class TestCollectUniqueAddresses(unittest.TestCase):
"""Targets and address args are gathered once, deduped, checksummed."""

Expand Down
276 changes: 276 additions & 0 deletions tests/test_infinifi_context.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,276 @@
"""Tests for Infinifi-specific LLM farm and token enrichment."""

import unittest
from unittest.mock import MagicMock, patch

from utils.calldata.decoder import DecodedCall
from utils.erc20_metadata import ERC20Metadata
from utils.llm import infinifi_context
from utils.llm.infinifi_context import (
InfinifiEscrowContext,
TokenContext,
_candidate_addresses,
_EscrowState,
_farm_matches_escrow,
_FarmRecord,
_fetch_farm_records,
_fetch_whitelist_targets,
_looks_like_escrow,
_resolve_configured_tokens,
_TokenCandidate,
format_infinifi_prompt,
format_infinifi_report,
resolve_infinifi_context,
)

MANAGER = "0x11F6FAb3f4D8635880C3e80cbae8AEF8136D4189"
ESCROW = "0x6439eb9DADC7977BC1ADC027B10Fb1749AF869A5"
FARM = "0x79e1B8e45932A7C802eA3dAb3844e5DEa68d971f"
USDC = "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"
DROP = "0xE4C72b4dE5b0F9ACcEA880Ad0b1F944F85A9dAA0"


def _set_rate_call() -> DecodedCall:
return DecodedCall(
function_name="setRate",
signature="setRate(address,uint256)",
params=[("address", ESCROW), ("uint256", 1_067_660_000_000_000_000)],
)


def _resolved_context() -> InfinifiEscrowContext:
return InfinifiEscrowContext(
escrow_address=ESCROW,
farm_address=FARM,
farm_name="New Silver 2 Senior",
farm_slug="new-silver-senior",
accounting_asset=TokenContext(USDC, "USD Coin", "USDC", 6),
total_assets_raw=3_003_294_554_623,
configured_tokens=(
TokenContext(
DROP,
"New Silver Series 2 DROP",
"NS2DRP",
18,
),
),
)


class TestResolveInfinifiContext(unittest.TestCase):
def setUp(self) -> None:
infinifi_context.reset_cache()

@patch.object(infinifi_context, "_resolve_configured_tokens")
@patch.object(infinifi_context, "_farm_matches_escrow", return_value=True)
@patch.object(infinifi_context, "_read_token")
@patch.object(infinifi_context, "_fetch_farm_records")
@patch.object(infinifi_context, "_read_escrow_state")
def test_resolves_farm_accounting_asset_and_configured_token(
self,
mock_escrow: MagicMock,
mock_farms: MagicMock,
mock_token: MagicMock,
_mock_relationship: MagicMock,
mock_configured_tokens: MagicMock,
) -> None:
state = _EscrowState(ESCROW, FARM, USDC, 3_003_294_554_623)
mock_escrow.side_effect = lambda _chain, address: state if address.lower() == ESCROW.lower() else None
mock_farms.return_value = (_FarmRecord(FARM, "New Silver 2 Senior", "new-silver-senior"),)
mock_token.return_value = TokenContext(USDC, "USD Coin", "USDC", 6)
mock_configured_tokens.return_value = _resolved_context().configured_tokens

result = resolve_infinifi_context("INFINIFI", 1, [(MANAGER, _set_rate_call())])

self.assertEqual(result, [_resolved_context()])
self.assertIn(DROP, result[0].addresses)
self.assertIn("New Silver Series 2 DROP", result[0].labels[DROP])

@patch.object(infinifi_context, "_read_escrow_state")
def test_skips_other_protocols_without_lookups(self, mock_read: MagicMock) -> None:
self.assertEqual(resolve_infinifi_context("AAVE", 1, [(MANAGER, _set_rate_call())]), [])
mock_read.assert_not_called()

@patch.object(infinifi_context, "_read_escrow_state", side_effect=RuntimeError("RPC down"))
def test_lookup_failure_does_not_block_alert(self, _mock_read: MagicMock) -> None:
self.assertEqual(resolve_infinifi_context("INFINIFI", 1, [(MANAGER, _set_rate_call())]), [])

@patch.object(infinifi_context, "_farm_matches_escrow")
@patch.object(infinifi_context, "_read_token", return_value=TokenContext(USDC, "USD Coin", "USDC", 6))
@patch.object(infinifi_context, "_fetch_farm_records", return_value=())
@patch.object(infinifi_context, "_read_escrow_state", return_value=_EscrowState(ESCROW, FARM, USDC, 1))
def test_rejects_escrow_without_infinifi_farm(
self,
_mock_escrow: MagicMock,
_mock_farms: MagicMock,
_mock_token: MagicMock,
mock_relationship: MagicMock,
) -> None:
self.assertEqual(resolve_infinifi_context("INFINIFI", 1, [(MANAGER, _set_rate_call())]), [])
mock_relationship.assert_not_called()


class TestFarmRelationship(unittest.TestCase):
@patch.object(infinifi_context.ChainManager, "get_client")
def test_requires_farm_escrow_getter_to_match_candidate(self, mock_client: MagicMock) -> None:
escrow_call = MagicMock()
escrow_call.call.return_value = ESCROW
farm = MagicMock()
farm.functions.escrow.return_value = escrow_call
mock_client.return_value.get_contract.return_value = farm

self.assertTrue(_farm_matches_escrow(1, FARM, ESCROW))
self.assertFalse(_farm_matches_escrow(1, FARM, MANAGER))


class TestConfiguredTokenDiscovery(unittest.TestCase):
@patch.object(infinifi_context.ChainManager, "get_client")
def test_reconstructs_current_whitelist_from_events(self, mock_client: MagicMock) -> None:
event_reader = MagicMock()
event_reader.get_logs.return_value = [
{"args": {"target": DROP, "enabled": True}},
{"args": {"target": USDC, "enabled": True}},
{"args": {"target": DROP, "enabled": False}},
{"args": {"target": DROP, "enabled": True}},
]
contract = MagicMock()
contract.events.WhitelistUpdated.return_value = event_reader
mock_client.return_value.get_contract.return_value = contract

self.assertEqual(_fetch_whitelist_targets(1, ESCROW), [DROP, USDC])

@patch.object(infinifi_context, "_read_token")
@patch.object(infinifi_context, "_fetch_whitelist_targets")
def test_keeps_only_non_accounting_erc20_targets(
self,
mock_candidates: MagicMock,
mock_read: MagicMock,
) -> None:
zero_token = "0x333333330522F64EE8d0b3039c460b41670e3404"
mock_candidates.return_value = [
USDC,
DROP,
zero_token,
]
drop = TokenContext(DROP, "New Silver Series 2 DROP", "NS2DRP", 18)
mock_read.side_effect = [drop, None]

state = _EscrowState(ESCROW, FARM, USDC, 0)
self.assertEqual(_resolve_configured_tokens(1, state), (drop,))
self.assertEqual(mock_read.call_count, 2)


class TestInfinifiContextFormatting(unittest.TestCase):
def test_prompt_names_farm_and_drop_token(self) -> None:
result = format_infinifi_prompt([_resolved_context()])
self.assertIn("New Silver 2 Senior", result)
self.assertIn("New Silver Series 2 DROP", result)
self.assertIn("3,003,294.554623 USDC", result)
self.assertIn("Configured non-accounting ERC20 target", result)

def test_report_links_all_context_addresses(self) -> None:
context = _resolved_context()
report = format_infinifi_report([context], 1, context.labels)
self.assertIn("**Farm:** New Silver 2 Senior", report)
self.assertIn(f"https://etherscan.io/address/{FARM}", report)
self.assertIn(f"https://etherscan.io/address/{DROP}", report)
self.assertIn("New Silver Series 2 DROP", report)
self.assertIn("Configured non-accounting ERC-20 targets", report)


class TestReadToken(unittest.TestCase):
@patch.object(infinifi_context.ChainManager, "get_client")
@patch.object(infinifi_context, "fetch_erc20_metadata", return_value=ERC20Metadata("NS2DRP", 18))
def test_reads_name_on_chain(self, _mock_meta: MagicMock, mock_client: MagicMock) -> None:
name_call = MagicMock()
name_call.call.return_value = "New Silver Series 2 DROP"
contract = MagicMock()
contract.functions.name.return_value = name_call
mock_client.return_value.get_contract.return_value = contract

token = infinifi_context._read_token(1, _TokenCandidate(DROP, "fallback"))

self.assertEqual(token, TokenContext(DROP, "New Silver Series 2 DROP", "NS2DRP", 18))


class TestCandidateAddresses(unittest.TestCase):
def test_collects_target_and_address_params(self) -> None:
call = DecodedCall(
function_name="setRate",
signature="setRate(address,uint256)",
params=[("address", ESCROW), ("uint256", 1)],
)
result = _candidate_addresses([(MANAGER, call)])
self.assertEqual(result, [MANAGER, ESCROW])

def test_dedupes_across_calls(self) -> None:
call = DecodedCall(function_name="setRate", signature="setRate(address,uint256)", params=[("address", ESCROW)])
self.assertEqual(_candidate_addresses([(ESCROW, call), (MANAGER, call)]), [ESCROW, MANAGER])


class TestLooksLikeEscrow(unittest.TestCase):
def test_requires_all_three_getters(self) -> None:
abi = [
{"type": "function", "name": "assetToken", "outputs": []},
{"type": "function", "name": "owner", "outputs": []},
{"type": "function", "name": "totalAssets", "outputs": []},
]
self.assertTrue(_looks_like_escrow(abi))
self.assertFalse(_looks_like_escrow(abi[:-1]))
self.assertFalse(_looks_like_escrow([]))


class TestFarmLookupAndParsing(unittest.TestCase):
def setUp(self) -> None:
infinifi_context.reset_cache()

@patch.object(infinifi_context, "fetch_json")
def test_fetch_farm_records_parses_api_shape(self, mock_fetch: MagicMock) -> None:
mock_fetch.return_value = {
"code": "OK",
"data": {
"farms": [
{"name": "new-silver-senior", "label": "New Silver 2 Senior", "address": FARM},
{"name": "broken", "label": "", "address": "not-an-address"},
{"name": "no-address", "label": "", "address": None},
]
},
}
records = _fetch_farm_records()
self.assertEqual(records, (_FarmRecord(FARM, "New Silver 2 Senior", "new-silver-senior"),))

@patch.object(infinifi_context, "fetch_json", return_value={"code": "ERROR"})
def test_fetch_farm_records_empty_on_bad_response(self, _mock_fetch: MagicMock) -> None:
self.assertEqual(_fetch_farm_records(), ())


class TestFormattingEdgeCases(unittest.TestCase):
def test_report_omits_configured_tokens_section_when_empty(self) -> None:
context = InfinifiEscrowContext(
escrow_address=ESCROW,
farm_address=FARM,
farm_name="New Silver 2 Senior",
farm_slug="new-silver-senior",
accounting_asset=TokenContext(USDC, "USD Coin", "USDC", 6),
total_assets_raw=3_003_294_554_623,
configured_tokens=(),
)
report = format_infinifi_report([context], 1, context.labels)
self.assertIn("**Farm:** New Silver 2 Senior", report)
self.assertNotIn("Configured non-accounting ERC-20 targets", report)

@patch.object(infinifi_context, "_fetch_farm_records")
@patch.object(infinifi_context, "_read_token", return_value=None)
@patch.object(infinifi_context, "_read_escrow_state", return_value=_EscrowState(ESCROW, FARM, USDC, 1))
def test_skips_escrow_with_non_erc20_accounting_asset(
self,
_mock_escrow: MagicMock,
_mock_token: MagicMock,
mock_farms: MagicMock,
) -> None:
self.assertEqual(resolve_infinifi_context("INFINIFI", 1, [(MANAGER, _set_rate_call())]), [])
mock_farms.assert_not_called()


if __name__ == "__main__":
unittest.main()
7 changes: 7 additions & 0 deletions tests/test_llm_report.py
Original file line number Diff line number Diff line change
Expand Up @@ -264,6 +264,13 @@ def test_sections_in_order(self) -> None:
self.assertLess(report.index("## Summary"), report.index("## Call Flow"))
self.assertLess(report.index("## Call Flow"), report.index("## Analysis"))

def test_protocol_context_is_deterministic_section_before_analysis(self) -> None:
ctx = _add_farms_ctx(protocol_context="- **Farm:** New Silver 2 Senior")
report = build_report("Updates the rate.", "Long analysis.", ctx, "LOW")
self.assertIn("## Protocol Context\n\n- **Farm:** New Silver 2 Senior", report)
self.assertLess(report.index("## Call Flow"), report.index("## Protocol Context"))
self.assertLess(report.index("## Protocol Context"), report.index("## Analysis"))

def test_metadata_header(self) -> None:
report = build_report("Summary.", "Analysis.", _add_farms_ctx(label_address=TIMELOCK), "HIGH")
self.assertIn("- **Protocol:** INFINIFI", report)
Expand Down
13 changes: 13 additions & 0 deletions utils/llm/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,19 @@ Filtering on "is it actually an ERC20" is what makes this need no configuration

The system prompt treats **exactly one** resolved token as verified decimals — state the amount and symbol, no hedge. Zero or several tokens keeps the hedge, since normalizing would be a guess. The Call Flow also annotates raw `uint*` values with `(≈ 5,369,214.23 JANE)`, but only above `10 ** (decimals - 3)` so an epoch number like `43` isn't rendered as `0.000000000000000043 JANE`.

### 5e. Infinifi Escrow Context (`utils/llm/infinifi_context.py`)

Infinifi RWA rate-manager calls target `RWAEscrowRateManager` and pass the affected escrow as an address argument. The generic Related Tokens resolver only inspects the direct call target, so it cannot identify the farm or tokens behind that escrow.

For Infinifi mainnet alerts, the adapter:

1. Identifies candidate `RWAEscrow` contracts by their verified ABI (`assetToken()`, `owner()`, and `totalAssets()`).
2. Matches the owner address to the public Infinifi farm API and verifies that the farm's on-chain `escrow()` getter returns the candidate.
3. Reads the accounting asset and current total assets on-chain.
4. Reconstructs the escrow's current whitelist from `WhitelistUpdated` events and identifies non-accounting targets that verify as ERC20 tokens. Token names, symbols, and decimals are read on-chain.

The result is added to the LLM prompt as verified protocol context and rendered independently in the Wavey Gist under `## Protocol Context`. The report distinguishes the escrow's accounting asset from non-accounting ERC20 targets it is allowed to interact with; whitelist membership does not establish how a token is valued downstream. Failures are best-effort and never block the governance alert.

### 6. LLM Prompt & Completion (`utils/llm/ai_explainer.py`)

The prompt is split into a **system** prompt (static instructions) and a **user** prompt (per-tx context). `complete(prompt, system_prompt=...)` passes the system block via the provider's native system role, which improves instruction-following and lets the Anthropic provider mark it `cache_control: ephemeral` — repeated alerts within the cache window pay for the (large) instruction prompt only once. The static block (`SYSTEM_INSTRUCTIONS`) enforces brevity:
Expand Down
Loading