Skip to content
Open
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
32 changes: 32 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -1087,6 +1087,38 @@ The command provides:
- Cost in RBTC and Wei
- Recommended gas limits (with buffers)
- Optimization tips (if applicable)

### 13. Liquidation Risk (Stress Testing)

The `risk` command group simulates liquidation stress scenarios for Rootstock DeFi lending protocols. It can model price shocks, estimate bad debt/collateral deficits, and generate structured reports.

> **Note**:
> - Currently focused on **Sovryn v1**.
> - Price data is fetched from CoinGecko; repeated runs may hit rate limits (`429 Too Many Requests`). If that happens, retry after a short wait.

#### Simulate

```bash
# Simulate a 40% market shock
rsk-cli risk simulate --shock 40

# Simulate a 40% shock but only for a specific asset
rsk-cli risk simulate --shock 40 --asset rbtc
```

#### Sandbox

```bash
# Compare default parameters vs custom LTV/threshold
rsk-cli risk sandbox --ltv 65 --threshold 80
```

#### Report

```bash
# Machine-readable JSON output (CI/CD friendly)
rsk-cli risk report --format json
```
=======
>>>>>>> main

Expand Down
3 changes: 3 additions & 0 deletions bin/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import { validateAndFormatAddressRSK } from "../src/utils/index.js";
import { rnsUpdateCommand } from "../src/commands/rnsUpdate.js";
import { rnsTransferCommand } from "../src/commands/rnsTransfer.js";
import { rnsRegisterCommand } from "../src/commands/rnsRegister.js";
import { registerRiskCommands } from "../src/commands/risk/index.js";

interface CommandOptions {
testnet?: boolean;
Expand Down Expand Up @@ -101,6 +102,8 @@ program
.description("CLI tool for interacting with Rootstock blockchain")
.version("1.4.0", "-v, --version", "Display the current version");

registerRiskCommands(program);

program
.command("wallet")
.description(
Expand Down
66 changes: 33 additions & 33 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

12 changes: 11 additions & 1 deletion src/commands/attestation.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import chalk from "chalk";
import ora from "ora";
import { EAS, SchemaEncoder } from "@ethereum-attestation-service/eas-sdk";
import ViemProvider from "../utils/viemProvider.js";
import { AttestationResult } from "../utils/types.js";
import { GraphQLService } from "../utils/graphqlService.js";
import { createRequire } from "module";

const require = createRequire(import.meta.url);

type AttestationCommandOptions = {
testnet: boolean;
Expand Down Expand Up @@ -88,6 +90,11 @@ async function setupEAS(params: AttestationCommandOptions) {
? EAS_CONTRACTS.testnet
: EAS_CONTRACTS.mainnet;

// Use CommonJS entry via require() to avoid ESM specifier issues in certain environments.
const { EAS } = require("@ethereum-attestation-service/eas-sdk") as {
EAS: new (address: `0x${string}`) => any;
};

const eas = new EAS(easAddress);
eas.connect(walletClient as any);

Expand All @@ -112,6 +119,9 @@ async function createAttestation(params: AttestationCommandOptions): Promise<Att
stopSpinner(params, spinner);
startSpinner(params, spinner, "⏳ Creating attestation...");

const { SchemaEncoder } = require("@ethereum-attestation-service/eas-sdk") as {
SchemaEncoder: new (schema: string) => any;
};
const schemaEncoder = new SchemaEncoder(params.data);
const encodedData = schemaEncoder.encodeData(JSON.parse(params.data));

Expand Down
67 changes: 67 additions & 0 deletions src/commands/risk/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { Command } from "commander";
import { riskSimulateCommand } from "./simulate.js";
import { riskSandboxCommand } from "./sandbox.js";
import { riskReportCommand } from "./report.js";

export function registerRiskCommands(program: Command): void {
const risk = program
.command("risk")
.description("Liquidation stress testing and risk analysis for Rootstock DeFi protocols");

risk
.command("simulate")
.description("Simulate liquidation cascades under price shocks")
.requiredOption("--shock <percentage>", "Price shock percentage to apply", (value: string) =>
parseFloat(value)
)
.option("--asset <symbol>", "Limit the shock to a specific asset (e.g. rbtc)")
.action(async (options: { shock: number; asset?: string }) => {
await riskSimulateCommand({
shock: options.shock,
asset: options.asset,
isExternal: false,
});
});

risk
.command("sandbox")
.description("Experiment with custom LTV and liquidation thresholds")
.option("--ltv <percentage>", "Maximum LTV (e.g. 65 for 65%)", (value: string) =>
parseFloat(value)
)
.option(
"--threshold <percentage>",
"Liquidation threshold (e.g. 80 for 80%)",
(value: string) => parseFloat(value)
)
.action(async (options: { ltv?: number; threshold?: number }) => {
await riskSandboxCommand({
ltv: options.ltv,
threshold: options.threshold,
isExternal: false,
});
});

risk
.command("report")
.description("Generate structured risk reports for CI/CD and monitoring")
.option(
"--format <format>",
"Output format: json|table (default: json)",
"json"
)
.option(
"--shock <percentage>",
"Price shock percentage to apply (default: 40)",
(value: string) => parseFloat(value)
)
.action(async (options: { format?: string; shock?: number }) => {
const fmt = options.format === "table" ? "table" : "json";
await riskReportCommand({
format: fmt,
shock: options.shock,
isExternal: false,
});
});
}

Loading