Skip to content

Commit 79bc05d

Browse files
refactor: DRY USYC scripts with shared config + Sepolia support
- Extract chain configs, ABIs, helpers into usyc_config.py - Add Sepolia chain config (USDC, USYC, Teller, Entitlements, Oracle) - Refactor subscribe/debug/entitlements scripts to use shared module - Chain-agnostic: USYC_CHAIN=sepolia python scripts/usyc_subscribe.py 20 - Fix: Arc USYC proxy delegates to wrong impl (BSC Teller address)
1 parent 59d1244 commit 79bc05d

4 files changed

Lines changed: 513 additions & 0 deletions

File tree

scripts/usyc_check_entitlements.py

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
"""Check USYC Entitlements status for the deployer wallet.
2+
3+
Usage:
4+
python scripts/usyc_check_entitlements.py
5+
USYC_CHAIN=sepolia python scripts/usyc_check_entitlements.py
6+
"""
7+
8+
from web3 import Web3
9+
10+
from usyc_config import (
11+
ENTITLEMENTS_ABI,
12+
get_chain,
13+
get_w3,
14+
get_wallet,
15+
load_env,
16+
)
17+
18+
19+
def main():
20+
chain = get_chain()
21+
w3 = get_w3(chain)
22+
w3, wallet = get_wallet(chain)
23+
24+
ent_addr = Web3.to_checksum_address(chain.entitlements)
25+
teller_addr = Web3.to_checksum_address(chain.teller)
26+
27+
print(f"Chain: {chain.name} ({chain.chain_id})")
28+
print(f"Wallet: {wallet}")
29+
print(f"Entitlements: {ent_addr}")
30+
print(f"Teller: {teller_addr}")
31+
print()
32+
33+
ent = w3.eth.contract(address=ent_addr, abi=ENTITLEMENTS_ABI)
34+
35+
for fn_name in ["isAllowed", "isEntitled", "allowlist"]:
36+
try:
37+
result = getattr(ent.functions, fn_name)(wallet).call()
38+
print(f" {fn_name}(wallet) = {result}")
39+
except Exception as e:
40+
print(f" {fn_name}(wallet) = ERROR: {str(e)[:80]}")
41+
42+
# DEFAULT_ADMIN_ROLE
43+
try:
44+
default_admin = bytes(32)
45+
result = ent.functions.hasRole(default_admin, wallet).call()
46+
print(f" hasRole(DEFAULT_ADMIN, wallet) = {result}")
47+
except Exception as e:
48+
print(f" hasRole check = ERROR: {str(e)[:80]}")
49+
50+
51+
if __name__ == "__main__":
52+
main()

scripts/usyc_config.py

Lines changed: 231 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,231 @@
1+
"""Shared USYC chain configs, ABIs, and helpers.
2+
3+
Single source of truth for all USYC-related scripts.
4+
To add a new chain: add an entry to CHAINS and import where needed.
5+
"""
6+
7+
import os
8+
from dataclasses import dataclass
9+
from pathlib import Path
10+
from web3 import Web3
11+
12+
# ── .env loader (idempotent) ────────────────────────────────────────
13+
_env_loaded = False
14+
15+
16+
def load_env() -> None:
17+
"""Load .env from project root (once per process)."""
18+
global _env_loaded
19+
if _env_loaded:
20+
return
21+
env_path = Path(__file__).resolve().parent.parent / ".env"
22+
if env_path.exists():
23+
for line in env_path.read_text().splitlines():
24+
line = line.strip()
25+
if line and not line.startswith("#") and "=" in line:
26+
k, v = line.split("=", 1)
27+
os.environ.setdefault(k.strip(), v.strip())
28+
_env_loaded = True
29+
30+
31+
# ── Chain configs ────────────────────────────────────────────────────
32+
@dataclass(frozen=True)
33+
class ChainConfig:
34+
name: str
35+
chain_id: int
36+
rpc_url: str
37+
explorer_tx: str # URL template: {tx_hash}
38+
usdc: str
39+
usyc: str
40+
teller: str
41+
entitlements: str
42+
private_key_env: str # env var name for the deployer key
43+
44+
45+
CHAINS: dict[str, ChainConfig] = {
46+
"arc": ChainConfig(
47+
name="Arc Testnet",
48+
chain_id=5042002,
49+
rpc_url="https://rpc.testnet.arc.network",
50+
explorer_tx="https://testnet.arcscan.app/tx/{tx_hash}",
51+
usdc="0x3600000000000000000000000000000000000000",
52+
usyc="0xe9185F0c5F296Ed1797AaE4238D26CCaBEadb86C",
53+
teller="0x9fdF14c5B14173D74C08Af27AebFf39240dC105A",
54+
entitlements="0xcc205224862c7641930c87679e98999d23c26113",
55+
private_key_env="ARC_DEPLOYER_PRIVATE_KEY",
56+
),
57+
"sepolia": ChainConfig(
58+
name="Ethereum Sepolia",
59+
chain_id=11155111,
60+
rpc_url=os.getenv("SEPOLIA_RPC_URL", "https://ethereum-sepolia-rpc.publicnode.com"),
61+
explorer_tx="https://sepolia.etherscan.io/tx/{tx_hash}",
62+
usdc="0x1c7D4B196Cb0C7B01d743Fbc6116a902379C7238",
63+
usyc="0x38D3A3f8717F4DB1CcB4Ad7D8C755919440848A3",
64+
teller="0x96424C885951ceb4B79fecb934eD857999e6f82B",
65+
entitlements="0x7f44c5F5551ed651F7298367Ee51f1CBa608571b",
66+
private_key_env="SEPOLIA_DEPLOYER_PRIVATE_KEY",
67+
),
68+
}
69+
70+
DEFAULT_CHAIN = "arc"
71+
72+
73+
def get_chain(name: str | None = None) -> ChainConfig:
74+
"""Return chain config by name, or default."""
75+
load_env()
76+
key = name or os.getenv("USYC_CHAIN", DEFAULT_CHAIN)
77+
if key not in CHAINS:
78+
raise ValueError(f"Unknown chain '{key}'. Available: {list(CHAINS)}")
79+
return CHAINS[key]
80+
81+
82+
# ── Shared ABIs ──────────────────────────────────────────────────────
83+
ERC20_ABI = [
84+
{
85+
"name": "approve",
86+
"type": "function",
87+
"stateMutability": "nonpayable",
88+
"inputs": [
89+
{"name": "spender", "type": "address"},
90+
{"name": "amount", "type": "uint256"},
91+
],
92+
"outputs": [{"name": "", "type": "bool"}],
93+
},
94+
{
95+
"name": "allowance",
96+
"type": "function",
97+
"stateMutability": "view",
98+
"inputs": [
99+
{"name": "owner", "type": "address"},
100+
{"name": "spender", "type": "address"},
101+
],
102+
"outputs": [{"name": "", "type": "uint256"}],
103+
},
104+
{
105+
"name": "balanceOf",
106+
"type": "function",
107+
"stateMutability": "view",
108+
"inputs": [{"name": "account", "type": "address"}],
109+
"outputs": [{"name": "", "type": "uint256"}],
110+
},
111+
{
112+
"name": "decimals",
113+
"type": "function",
114+
"stateMutability": "view",
115+
"inputs": [],
116+
"outputs": [{"name": "", "type": "uint8"}],
117+
},
118+
{
119+
"name": "totalSupply",
120+
"type": "function",
121+
"stateMutability": "view",
122+
"inputs": [],
123+
"outputs": [{"name": "", "type": "uint256"}],
124+
},
125+
{
126+
"name": "name",
127+
"type": "function",
128+
"stateMutability": "view",
129+
"inputs": [],
130+
"outputs": [{"name": "", "type": "string"}],
131+
},
132+
]
133+
134+
TELLER_ABI = [
135+
{
136+
"name": "deposit",
137+
"type": "function",
138+
"stateMutability": "nonpayable",
139+
"inputs": [
140+
{"name": "_assets", "type": "uint256"},
141+
{"name": "_receiver", "type": "address"},
142+
],
143+
"outputs": [{"name": "", "type": "uint256"}],
144+
},
145+
{
146+
"name": "redeem",
147+
"type": "function",
148+
"stateMutability": "nonpayable",
149+
"inputs": [
150+
{"name": "_shares", "type": "uint256"},
151+
{"name": "_receiver", "type": "address"},
152+
{"name": "_account", "type": "address"},
153+
],
154+
"outputs": [{"name": "", "type": "uint256"}],
155+
},
156+
]
157+
158+
ENTITLEMENTS_ABI = [
159+
{
160+
"name": "isAllowed",
161+
"type": "function",
162+
"stateMutability": "view",
163+
"inputs": [{"name": "account", "type": "address"}],
164+
"outputs": [{"name": "", "type": "bool"}],
165+
},
166+
{
167+
"name": "isEntitled",
168+
"type": "function",
169+
"stateMutability": "view",
170+
"inputs": [{"name": "account", "type": "address"}],
171+
"outputs": [{"name": "", "type": "bool"}],
172+
},
173+
{
174+
"name": "allowlist",
175+
"type": "function",
176+
"stateMutability": "view",
177+
"inputs": [{"name": "account", "type": "address"}],
178+
"outputs": [{"name": "", "type": "bool"}],
179+
},
180+
{
181+
"name": "hasRole",
182+
"type": "function",
183+
"stateMutability": "view",
184+
"inputs": [
185+
{"name": "role", "type": "bytes32"},
186+
{"name": "account", "type": "address"},
187+
],
188+
"outputs": [{"name": "", "type": "bool"}],
189+
},
190+
]
191+
192+
193+
# ── Helpers ──────────────────────────────────────────────────────────
194+
def get_w3(chain: ChainConfig | None = None) -> Web3:
195+
"""Return a Web3 instance for the given chain."""
196+
cfg = chain or get_chain()
197+
return Web3(Web3.HTTPProvider(cfg.rpc_url))
198+
199+
200+
def get_wallet(chain: ChainConfig | None = None) -> tuple[Web3, str]:
201+
"""Return (w3, wallet_address) from the chain's private key env var."""
202+
cfg = chain or get_chain()
203+
load_env()
204+
pk = os.getenv(cfg.private_key_env)
205+
if not pk:
206+
raise RuntimeError(
207+
f"Set {cfg.private_key_env} in .env for {cfg.name}"
208+
)
209+
w3 = get_w3(cfg)
210+
return w3, w3.eth.account.from_key(pk).address
211+
212+
213+
def tx_url(chain: ChainConfig, tx_hash: str | bytes) -> str:
214+
"""Build an explorer URL for a tx hash."""
215+
if isinstance(tx_hash, bytes):
216+
tx_hash = "0x" + tx_hash.hex()
217+
return chain.explorer_tx.format(tx_hash=tx_hash)
218+
219+
220+
def check_balances(w3: Web3, wallet: str, chain: ChainConfig) -> dict:
221+
"""Return {usdc, usyc} balances as raw ints."""
222+
usdc = w3.eth.contract(
223+
address=Web3.to_checksum_address(chain.usdc), abi=ERC20_ABI
224+
)
225+
usyc = w3.eth.contract(
226+
address=Web3.to_checksum_address(chain.usyc), abi=ERC20_ABI
227+
)
228+
return {
229+
"usdc": usdc.functions.balanceOf(wallet).call(),
230+
"usyc": usyc.functions.balanceOf(wallet).call(),
231+
}

scripts/usyc_debug.py

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
"""Debug USYC Teller failures — check contract state, decode failed TX.
2+
3+
Usage:
4+
python scripts/usyc_debug.py <tx_hash>
5+
USYC_CHAIN=sepolia python scripts/usyc_debug.py <tx_hash>
6+
"""
7+
8+
import sys
9+
from web3 import Web3
10+
11+
from usyc_config import (
12+
ERC20_ABI,
13+
TELLER_ABI,
14+
get_chain,
15+
get_w3,
16+
tx_url,
17+
load_env,
18+
)
19+
20+
21+
def main():
22+
if len(sys.argv) < 2:
23+
print("Usage: python scripts/usyc_debug.py <failed_tx_hash>")
24+
sys.exit(1)
25+
26+
tx_hash = sys.argv[1]
27+
chain = get_chain()
28+
w3 = get_w3(chain)
29+
load_env()
30+
31+
print(f"Chain: {chain.name} ({chain.chain_id})")
32+
print(f"Block: {w3.eth.block_number}")
33+
print()
34+
35+
# USYC info
36+
usyc = w3.eth.contract(
37+
address=Web3.to_checksum_address(chain.usyc), abi=ERC20_ABI
38+
)
39+
try:
40+
print(f"USYC name: {usyc.functions.name().call()}")
41+
print(f"USYC decimals: {usyc.functions.decimals().call()}")
42+
print(f"USYC totalSupply: {usyc.functions.totalSupply().call()}")
43+
except Exception as e:
44+
print(f"USYC info error: {e}")
45+
print()
46+
47+
# Teller config
48+
TELLER_VIEW_ABI = TELLER_ABI + [
49+
{
50+
"name": fn,
51+
"type": "function",
52+
"stateMutability": "view",
53+
"inputs": [],
54+
"outputs": [{"name": "", "type": "address"}],
55+
}
56+
for fn in ["entitlements", "usyc", "usdc"]
57+
] + [
58+
{
59+
"name": "paused",
60+
"type": "function",
61+
"stateMutability": "view",
62+
"inputs": [],
63+
"outputs": [{"name": "", "type": "bool"}],
64+
},
65+
]
66+
67+
teller = w3.eth.contract(
68+
address=Web3.to_checksum_address(chain.teller), abi=TELLER_VIEW_ABI
69+
)
70+
for fn in ["entitlements", "usyc", "usdc", "paused"]:
71+
try:
72+
result = getattr(teller.functions, fn)().call()
73+
print(f" Teller.{fn}() = {result}")
74+
except Exception as e:
75+
print(f" Teller.{fn}() = ERROR: {str(e)[:60]}")
76+
print()
77+
78+
# Fetch failed TX receipt
79+
print(f"Fetching TX: {tx_hash}")
80+
try:
81+
receipt = w3.eth.get_transaction_receipt(tx_hash)
82+
print(f" Status: {receipt.status} (0=reverted)")
83+
print(f" Gas used: {receipt.gasUsed}")
84+
print(f" Logs: {len(receipt.logs)}")
85+
for i, log in enumerate(receipt.logs[:5]):
86+
print(f" Log {i}: addr={log.address} topics={[t.hex() for t in log.topics]} data={log.data.hex()[:80]}")
87+
except Exception as e:
88+
print(f" Error fetching receipt: {e}")
89+
sys.exit(1)
90+
91+
# Simulate via eth_call
92+
print()
93+
print("Simulating via eth_call...")
94+
try:
95+
tx = w3.eth.get_transaction(tx_hash)
96+
w3.eth.call({
97+
"from": tx["from"],
98+
"to": tx["to"],
99+
"data": tx["input"],
100+
"value": tx["value"],
101+
"gas": tx["gas"],
102+
}, "latest")
103+
print(" Call succeeded (unexpected for a failed TX)")
104+
except Exception as e:
105+
print(f" Revert reason: {str(e)[:500]}")
106+
107+
108+
if __name__ == "__main__":
109+
main()

0 commit comments

Comments
 (0)