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 automation/jobs.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ profiles:
- { name: "morpho-markets", script: protocols/morpho/markets.py }
- { name: "morpho-markets-v2", script: protocols/morpho/markets_v2.py }
- { name: "aave", script: protocols/aave/main.py }
- { name: "spark", script: protocols/spark/main.py }
- { name: "lido-steth", script: protocols/lido/steth/main.py }
- { name: "lrt-pegs-curve", script: protocols/lrt-pegs/curve/main.py }
- { name: "lrt-pegs-fluid", script: protocols/lrt-pegs/fluid/main.py }
Expand All @@ -49,6 +50,7 @@ profiles:
- { name: "compound-proposals", script: protocols/compound/proposals.py }
- { name: "fluid-proposals", script: protocols/fluid/proposals.py }
- { name: "maker-proposals", script: protocols/maker/proposals.py }
- { name: "spark-proposals", script: protocols/spark/proposals.py }

daily:
cron: "19 8 * * *"
Expand Down
15 changes: 15 additions & 0 deletions monitoring.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -343,6 +343,21 @@ protocols:
- name: "Safe Multisig"
description: "3/6 multisig monitoring on Mainnet, Optimism, and Arbitrum"

spark:
display_name: "Spark"
description: "SparkLend utilization and Spark governance monitoring"
cadence: "Hourly"
tasks:
- protocols/spark/main.py
- protocols/spark/proposals.py
monitors:
- name: "Utilization"
description: "SparkLend mainnet market utilization above 99%"
- name: "Governance Proposals"
description: "New open proposals in Snapshot space sparkfi.eth (Spark governance portal)"
- name: "DSPause / Timelock"
description: "Tenderly alert on plot() in DSPause covering Spark spells (16-hour execution delay via Sky governance)"

stables:
display_name: "Stablecoins"
description: "Cross-protocol stablecoin peg, oracle, and large-transfer monitoring"
Expand Down
6 changes: 6 additions & 0 deletions protocols/safe/addresses.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,12 @@
], # aave Governance Guardian Safe
["AAVE", "polygon-main", "0x1A0581dd5C7C3DA4Ba1CDa7e0BcA7286afc4973b"],
["AAVE", "arbitrum-main", "0x1A0581dd5C7C3DA4Ba1CDa7e0BcA7286afc4973b"],
[
"SPARK",
"mainnet",
"0x44efFc473e81632B12486866AA1678edbb7BEeC3",
"SparkLend Freezer Multisig",
],
[
"MORPHO",
"mainnet",
Expand Down
26 changes: 22 additions & 4 deletions protocols/spark/README.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,27 @@
# Spark

**Monitoring - DISABLED** because we don't have any active strategies.

## Utilization

Github actions run hourly and send telegram message if there is a market with utilization above 95%. [Python script code](https://github.com/yearn/monitoring/blob/main/spark/main.py).
`main.py` checks utilization rates of active, non-deprecated [SparkLend](https://etherscan.io/address/0xC13e21B648A5Ee794902342038FF3aDAB66BE987) markets on mainnet (Aave v3 fork, same logic as `protocols/aave/main.py`): WETH, wstETH, WBTC, USDT, weETH, cbBTC, USDS, PYUSD. Deprecated markets (DAI, sDAI, USDC, sUSDS, LBTC) are excluded. Sends a Telegram alert when a market's utilization is above 99%.

Run: `python protocols/spark/main.py`

## Governance

### Proposals Script

`proposals.py` monitors new governance proposals from the [Spark governance portal](https://app.spark.fi/spk/governance), which is backed by the Snapshot space [`sparkfi.eth`](https://snapshot.box/#/s:sparkfi.eth) (queried via `https://hub.snapshot.org/graphql`). Sends Telegram alerts for new proposals whose voting is still open and caches the created timestamp of the last processed proposal to avoid duplicate messages.

Run: `python protocols/spark/proposals.py`

### Tenderly Alerts

Spark is a Sky star (subDAO): approved proposals are implemented through the Sky governance cycle. An executive spell is scheduled by calling `plot()` on [DSPause](https://etherscan.io/address/0xbE286431454714F511008713973d3B053A2d38f3) and, after the 16-hour delay, cast through the [PauseProxy](https://etherscan.io/address/0xBE8E3e3618f7474F8cB1d074A26afFef007E98FB), which forwards Spark-related payloads to the [Spark SubDAO Proxy](https://etherscan.io/address/0x3300f198988e4C9C63F75dF86De36421f06af8c4) for execution.

Tenderly alert on `plot()` called in DSPause covers scheduling of all spells, including Spark ones (this is the same contract already covered by the Maker DSPause alert, see `protocols/maker/README.md`). Note that the PauseProxy (`0xBE8E3e...`) itself has no `plot()` function — only `exec()`.

To get the proposal data from a received alert:

Possible improvements: add bad debt to risk dao and trigger telegram bot if there is some. Add governance part after the End Game.
1. see the tx from the alert on Tenderly
2. find the spell address in the trace
3. match it with the proposal on the [Spark governance portal](https://app.spark.fi/spk/governance) or [Sky voting portal](https://vote.sky.money/executive)
174 changes: 86 additions & 88 deletions protocols/spark/main.py
Original file line number Diff line number Diff line change
@@ -1,122 +1,120 @@
import json
"""
SparkLend monitoring script for tracking utilization rates of assets.

SparkLend is an Aave v3 fork, so utilization is computed the same way as in
protocols/aave/main.py: debt = aToken totalSupply - available underlying.
"""

from utils.abi import load_abi
from utils.alert import Alert, AlertSeverity, send_alert
from utils.chains import Chain
from utils.logger import get_logger
from utils.telegram import send_error_message
from utils.web3_wrapper import ChainManager

PROTOCOL = "spark"
logger = get_logger(PROTOCOL)

with open("protocols/aave/abi/AToken.json") as f:
abi_data = json.load(f)
if isinstance(abi_data, dict):
abi_atoken = abi_data["result"]
elif isinstance(abi_data, list):
abi_atoken = abi_data

mainnet_addresses = [
[
"0x78f897F0fE2d3B5690EbAe7f19862DEacedF10a7",
"0x83F20F44975D03b1b09e64809B757c47f942BEeA",
], # asdai, sdai
[
"0x377C3bd93f2a2984E1E7bE6A5C22c525eD4A4815",
"0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
], # ausdc, usdc
[
"0x4197ba364AE6698015AE5c1468f54087602715b2",
"0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599",
], # awbtc, wbtc
[
"0x59cD1C87501baa753d0B5B5Ab5D8416A45cD71DB",
"0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2",
], # aweth, weth
[
"0x12B54025C112Aa61fAce2CDB7118740875A566E9",
"0x7f39C581F595B53c5cb19bD0b3f8dA6c935E2Ca0",
], # awsteth, wsteth
[
"0x4DEDf26112B3Ec8eC46e7E31EA5e123490B05B8B",
"0x6B175474E89094C44Da98b954EedeAC495271d0F",
], # adai, dai
[
"0xe7dF13b8e3d6740fe17CBE928C7334243d86c92f",
"0xdAC17F958D2ee523a2206206994597C13D831ec7",
], # ausdt, usdt
[
"0x9985dF20D7e9103ECBCeb16a84956434B6f06ae8",
"0xae78736Cd615f374D3085123A210448E74Fc6393",
], # areth, reth
[
"0xb3973D459df38ae57797811F2A1fd061DA1BC123",
"0xcbB7C0000aB88B473b1f5aFd9ef808440eed33Bf",
], # spcwbtc, cbwbtc
[
"0x3CFd5C0D4acAA8Faee335842e4f31159fc76B008",
"0xCd5fE23C85820F7B72D0926FC9b05b43E359b7ee",
], # spweeth, weeth
]

# TODO: Add different threshold UR's for each asset
ABI_ATOKEN = load_abi("protocols/aave/abi/AToken.json")

# Active, non-deprecated SparkLend reserves (Pool 0xC13e21B648A5Ee794902342038FF3aDAB66BE987).
# Deprecated markets (DAI, sDAI, USDC, sUSDS, LBTC) and frozen reserves are excluded.
ADDRESSES_BY_CHAIN = {
# spToken, underlying, symbol
Chain.MAINNET: [
(
"0x59cD1C87501baa753d0B5B5Ab5D8416A45cD71DB",
"0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2",
"WETH",
),
(
"0x12B54025C112Aa61fAce2CDB7118740875A566E9",
"0x7f39C581F595B53c5cb19bD0b3f8dA6c935E2Ca0",
"wstETH",
),
(
"0x4197ba364AE6698015AE5c1468f54087602715b2",
"0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599",
"WBTC",
),
(
"0xe7dF13b8e3d6740fe17CBE928C7334243d86c92f",
"0xdAC17F958D2ee523a2206206994597C13D831ec7",
"USDT",
),
(
"0x3CFd5C0D4acAA8Faee335842e4f31159fc76B008",
"0xCd5fE23C85820F7B72D0926FC9b05b43E359b7ee",
"weETH",
),
(
"0xb3973D459df38ae57797811F2A1fd061DA1BC123",
"0xcbB7C0000aB88B473b1f5aFd9ef808440eed33Bf",
"cbBTC",
),
(
"0xC02aB1A5eaA8d1B114EF786D9bde108cD4364359",
"0xdC035D45d973E3EC169d2276DDab16f1e407384F",
"USDS",
),
(
"0x779224df1c756b4EDD899854F32a53E8c2B2ce5d",
"0x6c3ea9036406852006290770BEdFcAbA0e23A0e8",
"PYUSD",
),
],
}

THRESHOLD_UR = 0.99


def print_stuff(chain_name, token_name, ur):
logger.info("Chain: %s, Token: %s, UR: %s", chain_name, token_name, ur)
def print_stuff(chain_name: str, token_name: str, ur: float) -> None:
logger.debug(f"Chain: {chain_name}, Token: {token_name}, UR: {ur}")
if ur > THRESHOLD_UR:
message = f"**BEEP BOP**\n💎 Market asset: {token_name}\n📊 Utilization rate: {ur:.2%}\n🌐 Chain: {chain_name}"
send_alert(Alert(AlertSeverity.MEDIUM, message, PROTOCOL))


# Function to process assets for a specific network
def process_assets(chain, addresses):
# Get Web3 client using ChainManager
def process_assets(chain: Chain) -> None:
client = ChainManager.get_client(chain)
addresses = ADDRESSES_BY_CHAIN[chain]

# Prepare all contracts and batch calls
with client.batch_requests() as batch:
for atoken_address, underlying_token_address in addresses:
atoken = client.eth.contract(address=atoken_address, abi=abi_atoken)
underlying_token = client.eth.contract(address=underlying_token_address, abi=abi_atoken)

# Add all calls to the batch
batch.add(atoken.functions.totalSupply())
batch.add(underlying_token.functions.balanceOf(atoken_address))
batch.add(underlying_token.functions.symbol())

# Execute all calls at once
responses = batch.execute()
expected_responses = len(addresses) * 3
for sptoken_address, underlying_token_address, _ in addresses:
sptoken = client.eth.contract(address=sptoken_address, abi=ABI_ATOKEN)
underlying_token = client.eth.contract(address=underlying_token_address, abi=ABI_ATOKEN)

batch.add(sptoken.functions.totalSupply())
batch.add(underlying_token.functions.balanceOf(sptoken_address))

responses = client.execute_batch(batch)
expected_responses = len(addresses) * 2
if len(responses) != expected_responses:
raise ValueError(f"Expected {expected_responses} responses from batch, got: {len(responses)}")

# Process results
expected_responses = len(addresses) * 3
if len(responses) != expected_responses:
raise ValueError(f"Expected {expected_responses} responses, got: {len(responses)}")
for i, (_, _, token_symbol) in enumerate(addresses):
ts = responses[i * 2] # totalSupply
av = responses[i * 2 + 1] # balanceOf

for i in range(0, len(responses), 3):
ts = responses[i] # totalSupply
av = responses[i + 1] # balanceOf
token_name = responses[i + 2] # symbol

if None in (ts, av, token_name):
continue

# Calculate debt and utilization rate
debt = ts - av
ur = debt / ts if ts != 0 else 0

print_stuff(chain.name, token_name, ur)
print_stuff(chain.name, token_symbol, ur)


# Main function
def main():
logger.info("Processing Mainnet assets...")
process_assets(Chain.MAINNET, mainnet_addresses)
def main() -> None:
for chain in [Chain.MAINNET]:
logger.info("Processing %s assets...", chain.name)
try:
process_assets(chain)
except Exception as e:
logger.error("Error processing %s: %s", chain.name, e)
send_error_message(f"Error processing SparkLend assets on {chain.name}: {e}", PROTOCOL)


# Run the main function
if __name__ == "__main__":
main()
from utils.runner import run_with_alert

run_with_alert(main, PROTOCOL)
Loading