Skip to content
Closed
Show file tree
Hide file tree
Changes from 7 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
1 change: 1 addition & 0 deletions crates/basilica-miner/tests/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,7 @@ mod tests {
let slash_tx = contract.slashCollateral(
FixedBytes::from_slice(&hotkey),
FixedBytes::from_slice(&executor_id.to_be_bytes()),
amount,
url.to_owned(),
FixedBytes::from_slice(&url_checksum.to_be_bytes()),
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ use collateral_contract::config::CONTRACT_DEPLOYED_BLOCK_NUMBER;
use collateral_contract::{Deposit, Reclaimed, Slashed};
use hex::ToHex;
use sqlx::Row;
use tracing::warn;
use tracing::{error, warn};

impl SimplePersistence {
pub async fn create_collateral_scanned_blocks_table(&self) -> Result<(), anyhow::Error> {
Expand Down Expand Up @@ -175,16 +175,17 @@ impl SimplePersistence {
{
Some((id, collateral)) => {
let now = Utc::now().to_rfc3339();
let query = "UPDATE collateral_status SET collateral = ?, miner = ? , url = ? , url_content_md5_checksum = ?, updated_at = ? WHERE id = ?";
if slashed.amount != collateral {
warn!(
"Slashed amount {} does not match collateral {} in database",
slashed.amount, collateral
let query = "UPDATE collateral_status SET collateral = collateral - ?, miner = ? , url = ? , url_content_md5_checksum = ?, updated_at = ? WHERE id = ?";
if slashed.slashAmount > collateral {
error!(
"Slashed amount {} is greater than collateral {} in database",
slashed.slashAmount, collateral
);
return Err(anyhow::anyhow!("Slashed amount is greater than collateral"));
}

sqlx::query(query)
.bind("0".to_string())
.bind(slashed.slashAmount.to_string())
.bind(format!(
"0x{}",
Address::ZERO.as_slice().encode_hex::<String>()
Expand Down Expand Up @@ -236,7 +237,7 @@ mod tests {
hotkey: FixedBytes::from_slice(&hk),
executorId: FixedBytes::from_slice(&ex),
miner: Address::from_slice(&[0u8; 20]),
amount: U256::from(amount),
slashAmount: U256::from(amount),
url: String::new(),
urlContentMd5Checksum: FixedBytes::from_slice(&[0u8; 16]),
}
Expand Down Expand Up @@ -355,7 +356,7 @@ mod tests {
.fetch_one(persistence.pool())
.await
.unwrap();
assert_eq!(coll_after_slash, "0");
assert_eq!(coll_after_slash, "100");
}

#[tokio::test]
Expand Down
2 changes: 2 additions & 0 deletions crates/collateral-contract/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,7 @@ collateral-cli tx slash-collateral \
--private-key $PRIVATE_KEY \
--hotkey 0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef \
--executor-id 123 \
--slash-amount 1000000000000000000 \
--url "https://evidence.example.com/slash-proof" \
--url-content-md5-checksum aab03e786183b16c8a0b15f6b40ff607

Expand All @@ -203,6 +204,7 @@ collateral-cli --network testnet tx slash-collateral \
--private-key $PRIVATE_KEY \
--hotkey fedcba9876543210fedcba9876543210fedcba9876543210fedcba9876543210 \
--executor-id 999 \
--slash-amount 1000000000000000000 \
--url "https://audit.testnet.com/violations/999" \
--url-content-md5-checksum 098f6bcd4621d373cade4e832627b4f6
```
Expand Down
1 change: 1 addition & 0 deletions crates/collateral-contract/flow.sh
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ collateral-cli --network "$NETWORK" --contract-address "$CONTRACT_ADDRESS" tx sl
--private-key "$PRIVATE_KEY" \
--hotkey "$HOTKEY" \
--executor-id "$EXECUTOR_ID" \
--slash-amount 10 \
--url https://www.tplr.ai/ \
--url-content-md5-checksum 269ff519d1140a175941ea4b00ccbe0d

Expand Down
4 changes: 3 additions & 1 deletion crates/collateral-contract/src/CollateralUpgradableABI.json
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,7 @@
"inputs": [
{ "name": "hotkey", "type": "bytes32", "internalType": "bytes32" },
{ "name": "executorId", "type": "bytes16", "internalType": "bytes16" },
{ "name": "slashAmount", "type": "uint256", "internalType": "uint256" },
{ "name": "url", "type": "string", "internalType": "string" },
{
"name": "urlContentMd5Checksum",
Expand Down Expand Up @@ -616,7 +617,7 @@
"internalType": "address"
},
{
"name": "amount",
"name": "slashAmount",
"type": "uint256",
"indexed": false,
"internalType": "uint256"
Expand Down Expand Up @@ -706,6 +707,7 @@
"name": "InsufficientCollateralForReclaim",
"inputs": []
},
{ "type": "error", "name": "InsufficientCollateralForSlash", "inputs": [] },
{ "type": "error", "name": "InvalidDepositMethod", "inputs": [] },
{ "type": "error", "name": "InvalidInitialization", "inputs": [] },
{ "type": "error", "name": "NotInitializing", "inputs": [] },
Expand Down
23 changes: 17 additions & 6 deletions crates/collateral-contract/src/CollateralUpgradeable.sol
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ contract CollateralUpgradeable is
bytes32 indexed hotkey,
bytes16 indexed executorId,
address indexed miner,
uint256 amount,
uint256 slashAmount,
string url,
bytes16 urlContentMd5Checksum
);
Expand All @@ -101,6 +101,7 @@ contract CollateralUpgradeable is
error ReclaimNotFound();
error TransferFailed();
error InsufficientCollateralForReclaim();
error InsufficientCollateralForSlash();

/// @notice Initializes the upgradeable collateral contract
/// @param netuid The netuid of the subnet
Expand Down Expand Up @@ -265,7 +266,6 @@ contract CollateralUpgradeable is
}

collaterals[hotkey][executorId] -= amount;
executorToMiner[hotkey][executorId] = address(0);

emit Reclaimed(reclaimRequestId, hotkey, executorId, miner, amount);

Expand All @@ -274,6 +274,10 @@ contract CollateralUpgradeable is
if (!success) {
revert TransferFailed();
}

if (collaterals[hotkey][executorId] == 0) {
executorToMiner[hotkey][executorId] = address(0);
}
}

/// @notice Allows the trustee to deny a pending reclaim request before the timeout expires
Expand Down Expand Up @@ -321,6 +325,7 @@ contract CollateralUpgradeable is
function slashCollateral(
bytes32 hotkey,
bytes16 executorId,
uint256 slashAmount,
string calldata url,
bytes16 urlContentMd5Checksum
) external onlyTrustee {
Expand All @@ -330,20 +335,26 @@ contract CollateralUpgradeable is
revert AmountZero();
}

collaterals[hotkey][executorId] = 0;
if (slashAmount > amount) {
revert InsufficientCollateralForSlash();
}

collaterals[hotkey][executorId] = amount - slashAmount;
address miner = executorToMiner[hotkey][executorId];

// burn the collateral
(bool success, ) = payable(address(0)).call{value: amount}("");
(bool success, ) = payable(address(0)).call{value: slashAmount}("");
if (!success) {
revert TransferFailed();
}
executorToMiner[hotkey][executorId] = address(0);
if (amount == slashAmount) {
executorToMiner[hotkey][executorId] = address(0);
}
emit Slashed(
hotkey,
executorId,
miner,
amount,
slashAmount,
url,
urlContentMd5Checksum
);
Expand Down
4 changes: 3 additions & 1 deletion crates/collateral-contract/src/lib.rs

Large diffs are not rendered by default.

29 changes: 19 additions & 10 deletions crates/collateral-contract/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,9 @@ enum TxCommands {
/// Executor ID as string
#[arg(long)]
executor_id: String,
/// Amount to slash in in wei
#[arg(long)]
slash_amount: String,
/// URL for proof of slashing
#[arg(long)]
url: String,
Expand Down Expand Up @@ -213,7 +216,7 @@ async fn handle_tx_command(
let executor_uuid = Uuid::parse_str(&executor_id)?;

println!(
"Depositing {} wei for executor {} with hotkey {}",
"Depositing {} in wei for executor {} with hotkey {}",
amount, executor_id, hotkey
);
collateral_contract::deposit(
Expand Down Expand Up @@ -286,21 +289,27 @@ async fn handle_tx_command(
private_key,
hotkey,
executor_id,
slash_amount,
url,
url_content_md5_checksum,
} => {
let hotkey_bytes = parse_hotkey(&hotkey)?;
let checksum = parse_md5_checksum(&url_content_md5_checksum)?;
let executor_uuid = Uuid::parse_str(&executor_id)?;
let amount_u256 = parse_u256(&slash_amount)?;
if amount_u256.is_zero() {
anyhow::bail!("slash_amount must be > 0 wei");
}

println!(
"Slashing collateral for executor {} with hotkey {}",
executor_id, hotkey
"Slashing collateral for executor {} with hotkey {} amount {}",
executor_id, hotkey, slash_amount
);
collateral_contract::slash_collateral(
&private_key,
hotkey_bytes,
executor_uuid.into_bytes(),
amount_u256,
&url,
checksum,
network_config,
Expand Down Expand Up @@ -331,7 +340,7 @@ async fn handle_query_command(
}
QueryCommands::MinCollateralIncrease => {
let result = collateral_contract::min_collateral_increase(network_config).await?;
println!("Minimum collateral increase: {} wei", result);
println!("Minimum collateral increase: {} in wei", result);
}
QueryCommands::ExecutorToMiner {
hotkey,
Expand Down Expand Up @@ -365,7 +374,7 @@ async fn handle_query_command(
)
.await?;
println!(
"Collateral for executor {}: {} wei",
"Collateral for executor {}: {} in wei",
executor_id_clone, result
);
}
Expand All @@ -376,7 +385,7 @@ async fn handle_query_command(
println!(" Hotkey: {}", hex::encode(result.hotkey));
println!(" Executor ID: {}", Uuid::from_bytes(result.executor_id));
println!(" Miner: {}", result.miner);
println!(" Amount: {} wei", result.amount);
println!(" Amount: {} in wei", result.amount);
println!(" Deny timeout: {}", result.deny_timeout);
}
}
Expand Down Expand Up @@ -461,7 +470,7 @@ fn print_events_pretty(events: &HashMap<u64, Vec<CollateralEvent>>) {
hex::encode(deposit.executorId.as_slice())
);
println!(" Miner: {}", deposit.miner);
println!(" Amount: {} wei", deposit.amount);
println!(" Amount: {} in wei", deposit.amount);
}
CollateralEvent::Reclaimed(reclaimed) => {
println!(" Type: Reclaimed");
Expand All @@ -472,7 +481,7 @@ fn print_events_pretty(events: &HashMap<u64, Vec<CollateralEvent>>) {
hex::encode(reclaimed.executorId.as_slice())
);
println!(" Miner: {}", reclaimed.miner);
println!(" Amount: {} wei", reclaimed.amount);
println!(" Amount: {} in wei", reclaimed.amount);
}
CollateralEvent::Slashed(slashed) => {
println!(" Type: Slashed");
Expand All @@ -482,7 +491,7 @@ fn print_events_pretty(events: &HashMap<u64, Vec<CollateralEvent>>) {
hex::encode(slashed.executorId.as_slice())
);
println!(" Miner: {}", slashed.miner);
println!(" Amount: {} wei", slashed.amount);
println!(" Amount: {} in wei", slashed.slashAmount);
println!(" URL: {}", slashed.url);
println!(
" URL Content MD5: {}",
Expand Down Expand Up @@ -527,7 +536,7 @@ fn print_events_json(events: &HashMap<u64, Vec<CollateralEvent>>) -> Result<()>
"hotkey": hex::encode(slashed.hotkey.as_slice()),
"executorId": hex::encode(slashed.executorId.as_slice()),
"miner": slashed.miner.to_string(),
"amount": slashed.amount.to_string(),
"slashAmount": slashed.slashAmount.to_string(),
"url": slashed.url,
"urlContentMd5Checksum": hex::encode(slashed.urlContentMd5Checksum.as_slice())
})
Comment on lines +539 to 542
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Breaking JSON schema: renamed Slashed.amount → slashAmount. Provide backward‑compat alias or gate behind a flag.

Existing consumers expecting "amount" will break.

Option A (add alias now; deprecate later):

                 CollateralEvent::Slashed(slashed) => {
                     serde_json::json!({
                         "type": "Slashed",
                         "hotkey": hex::encode(slashed.hotkey.as_slice()),
                         "executorId": hex::encode(slashed.executorId.as_slice()),
                         "miner": slashed.miner.to_string(),
-                        "slashAmount": slashed.slashAmount.to_string(),
+                        "slashAmount": slashed.slashAmount.to_string(),
+                        "amount": slashed.slashAmount.to_string(), // backward-compat
                         "url": slashed.url,
                         "urlContentMd5Checksum": hex::encode(slashed.urlContentMd5Checksum.as_slice())
                     })
                 }

Option B: Gate under a CLI flag (e.g., --json-version=2) and keep v1 shape by default.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
"slashAmount": slashed.slashAmount.to_string(),
"url": slashed.url,
"urlContentMd5Checksum": hex::encode(slashed.urlContentMd5Checksum.as_slice())
})
"slashAmount": slashed.slashAmount.to_string(),
"amount": slashed.slashAmount.to_string(), // backward-compat
"url": slashed.url,
"urlContentMd5Checksum": hex::encode(slashed.urlContentMd5Checksum.as_slice())
})
🤖 Prompt for AI Agents
crates/collateral-contract/src/main.rs around lines 539 to 542: the JSON output
renamed the field "amount" → "slashAmount", which breaks existing consumers;
either restore a backward-compatible alias or gate the new shape behind a
JSON-version flag. Fix option A (quick): when building the JSON object include
both keys, e.g. add "amount": slashed.slashAmount (marked deprecated in
comments) alongside "slashAmount" so v1 clients continue to work. Fix option B
(preferred for explicitness): add a CLI/config option (e.g., --json-version with
default "1"), keep current output for version 1, and when json-version=2 emit
"slashAmount" instead of "amount"; update help text and tests accordingly.

Expand Down
11 changes: 7 additions & 4 deletions docs/collateral-contract.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,11 @@ This smart contract is **generic** and works with **any Bittensor subnet**.

We provide the CLI to interact with collateral contract, the details could be found in [`README.md`](/crates/collateral-contract/README.md)

+Main adaptations:
+- Extends `executorToMiner` to a double map keyed by (`hotkey`, `executorId`).
+- Adopts OpenZeppelin’s `UUPSUpgradeable` pattern so the collateral contract can be upgraded without losing state.
+- Introduces partial slashing: `slashCollateral(miner, executorId, slashAmount, ...)` validates and applies `slashAmount` (cannot exceed the executor’s current collateral). The (`hotkey`, `executorId`) entry is cleared only when the remaining collateral reaches zero.

## ⚖️ A Note on Slashing Philosophy

The power to slash collateral carries weight — it protects subnet quality, but also risks abuse if unchecked.
Expand Down Expand Up @@ -104,9 +109,7 @@ Below is a typical sequence for integrating and using this collateral contract w
- Each miner **creates an Ethereum (H160) wallet**, links it to their hotkey, and funds it with enough TAO for transaction fees.
- Miners **retrieve** the owner's contract address from the chain or another trusted source.
- Upon confirmation, miners **deposit** collateral by calling the contract's `deposit(executorUuid)` function, specifying the **executor UUID** to associate the collateral with specific executors.
- Confirm on-chain that your collateral has been successfully locked for that miner

- Confirm on-chain that your collateral has been successfully locked for that your executor
- Confirm on-chain that your collateral has been successfully locked for that your miner and the specified executor.

- **Slashing Misbehaving Miners**
If a miner is found violating subnet rules (e.g., returning invalid responses), the subnet owner (admin) or an authorized slasher **calls** `slashCollateral()` with the `miner`, `slashAmount`, `executorUuid`, and justification details to reduce the miner’s collateral.
Expand Down Expand Up @@ -262,4 +265,4 @@ Miner's reclaim request will be declined when his executor is rented by customer

### What will happen when a miner's deposit is slashed?

Miner will lose deposited amount for violated executor; miner need to deposit for that executor again if they want to keep getting rewards for executor.
The miner may lose some or all collateral (via `slashAmount`) associated with the violated executor. the remaining collateral will continue to be used for miners.