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
9 changes: 9 additions & 0 deletions protocols/3jane/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,15 @@ Only HIGH and CRITICAL alerts dispatch. LOW and MEDIUM alerts—including insura

[Internal timelock monitoring](../timelock/README.md) covers CallScheduled events from the [3Jane 24-hour timelock](https://etherscan.io/address/0x1dccd4628d48a50c1a7adea3848bcc869f08f8c2) and [7-day upgrade timelock](https://etherscan.io/address/0x3d3c41419ab401cd25055e8f9421d7d96d887885) on Mainnet.

Those alerts carry a 3Jane `Protocol Context` section built by
[`utils/llm/threejane_context.py`](../../utils/llm/threejane_context.py):

- `bytes32` arguments are reversed to their `keccak256` pre-image, so a `setConfig` call names the parameter (`MAX_LTV`, `IS_PAUSED`, `DEBT_CAP`, …) and a `grantRole` names the role, each with what it controls. `ProtocolConfig` keys also carry the value stored on-chain right now.
- `RewardsDistributor` calls carry the distribution mode (`useMint`), whether the distributor holds `MINTER_ROLE` on JANE, JANE supply and whether transfers are globally enabled, `maxClaimable` / `totalClaimed` / outstanding, the current `merkleRoot` and epoch, the emissions stored for the three preceding epochs, and how the proposed allocation compares to the epoch before it.
- A key that caps a quantity is rendered next to that quantity — `USD3_SUPPLY_CAP` alongside USD3 `totalAssets`, batched into the same request — so a ceiling raise reads as slack or as unblocking deposits. Register more in `_USAGE_READS`, but only where both sides are denominated the same way.

Add a key or role to `_HASHED_LABELS` in that module when 3Jane introduces one; the hash is derived from the name, so the table cannot drift.

## Running

```bash
Expand Down
33 changes: 33 additions & 0 deletions protocols/3jane/abi/Jane.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
[
{
"inputs": [],
"name": "totalSupply",
"outputs": [{"name": "", "type": "uint256"}],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "transferable",
"outputs": [{"name": "", "type": "bool"}],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [{"name": "account", "type": "address"}],
"name": "balanceOf",
"outputs": [{"name": "", "type": "uint256"}],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{"name": "role", "type": "bytes32"},
{"name": "account", "type": "address"}
],
"name": "hasRole",
"outputs": [{"name": "", "type": "bool"}],
"stateMutability": "view",
"type": "function"
}
]
51 changes: 51 additions & 0 deletions protocols/3jane/abi/RewardsDistributor.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
[
{
"inputs": [],
"name": "useMint",
"outputs": [{"name": "", "type": "bool"}],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "merkleRoot",
"outputs": [{"name": "", "type": "bytes32"}],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "jane",
"outputs": [{"name": "", "type": "address"}],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "maxClaimable",
"outputs": [{"name": "", "type": "uint256"}],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "totalClaimed",
"outputs": [{"name": "", "type": "uint256"}],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "epoch",
"outputs": [{"name": "", "type": "uint256"}],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [{"name": "epoch", "type": "uint256"}],
"name": "epochEmissions",
"outputs": [{"name": "", "type": "uint256"}],
"stateMutability": "view",
"type": "function"
}
]
86 changes: 86 additions & 0 deletions tests/test_protocol_context.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
"""Tests for the protocol-context adapter registry."""

import unittest
from unittest.mock import patch

from utils.calldata.decoder import DecodedCall
from utils.llm import protocol_context
from utils.llm.protocol_context import _Adapter, resolve_protocol_context

TARGET = "0x6b276A2A7dd8b629adBA8A06AD6573d01C84f34E"
TOKEN = "0x333333330522F64EE8d0b3039c460b41670e3404"


class _FakeContext:
def __init__(self, addresses: list[str], labels: dict[str, str]) -> None:
self.addresses = addresses
self.labels = labels


def _call() -> DecodedCall:
return DecodedCall("setConfig", "setConfig(bytes32,uint256)", [("uint256", 1)])


def _adapter(name: str, contexts: list[_FakeContext]) -> _Adapter:
return _Adapter(
name=name,
resolve=lambda protocol, chain_id, calls: contexts,
format_prompt=lambda ctxs: f"{name} prompt",
format_report=lambda ctxs, chain_id, labels: f"{name} report",
)


class TestResolveProtocolContext(unittest.TestCase):
"""Every registered adapter contributes; a failing one is skipped."""

def test_adapters_are_combined(self) -> None:
adapters = (
_adapter("alpha", [_FakeContext([TARGET], {TARGET: "Config"})]),
_adapter("beta", [_FakeContext([TOKEN], {TOKEN: "Token"})]),
)
with patch.object(protocol_context, "_ADAPTERS", adapters):
resolved = resolve_protocol_context("3JANE", 1, [(TARGET, _call())])

self.assertEqual(resolved.prompt, "alpha prompt\n\nbeta prompt")
self.assertEqual(resolved.report, "alpha report\n\nbeta report")
self.assertEqual(resolved.addresses, [TARGET, TOKEN])
self.assertEqual(resolved.labels, {TARGET: "Config", TOKEN: "Token"})

def test_empty_adapter_contributes_nothing(self) -> None:
with patch.object(protocol_context, "_ADAPTERS", (_adapter("alpha", []),)):
resolved = resolve_protocol_context("3JANE", 1, [(TARGET, _call())])

self.assertEqual(resolved.prompt, "")
self.assertEqual(resolved.report, "")
self.assertEqual(resolved.addresses, [])

def test_failing_adapter_does_not_block_the_others(self) -> None:
def explode(protocol: str, chain_id: int, calls: list) -> list:
raise RuntimeError("etherscan down")

broken = _Adapter("broken", explode, lambda c: "x", lambda contexts, chain_id, labels: "x")
adapters = (broken, _adapter("beta", [_FakeContext([TOKEN], {})]))
with patch.object(protocol_context, "_ADAPTERS", adapters):
resolved = resolve_protocol_context("3JANE", 1, [(TARGET, _call())])

self.assertEqual(resolved.prompt, "beta prompt")

def test_existing_labels_reach_the_report_renderer(self) -> None:
seen: dict[str, str] = {}

def capture(contexts: list, chain_id: int, labels: dict[str, str]) -> str:
seen.update(labels)
return "report"

adapter = _Adapter("alpha", lambda p, c, t: [_FakeContext([TOKEN], {TOKEN: "Token"})], lambda c: "p", capture)
with patch.object(protocol_context, "_ADAPTERS", (adapter,)):
resolve_protocol_context("3JANE", 1, [(TARGET, _call())], {TARGET: "Timelock"})

self.assertEqual(seen, {TARGET: "Timelock", TOKEN: "Token"})

def test_registered_adapters_cover_the_known_protocols(self) -> None:
self.assertEqual({adapter.name for adapter in protocol_context._ADAPTERS}, {"infinifi", "3jane"})


if __name__ == "__main__":
unittest.main()
Loading
Loading