Skip to content

Commit 56bbbbb

Browse files
committed
fix(collateral): clear nodeToMiner in denyReclaimRequest when balances are zero
After a full slash with a pending reclaim, denyReclaimRequest zeroed pending trackers but never cleared nodeToMiner, permanently locking the node to the old owner.
1 parent 608caf6 commit 56bbbbb

File tree

2 files changed

+70
-6
lines changed

2 files changed

+70
-6
lines changed

crates/collateral-contract/src/CollateralUpgradeable.sol

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -435,15 +435,24 @@ contract CollateralUpgradeable is
435435
revert PastDenyTimeout();
436436
}
437437

438-
collateralUnderPendingReclaims[reclaim.hotkey][
439-
reclaim.nodeId
440-
] -= reclaim.amount;
441-
alphaCollateralUnderPendingReclaims[reclaim.hotkey][
442-
reclaim.nodeId
443-
] -= reclaim.alphaAmount;
438+
bytes32 hotkey = reclaim.hotkey;
439+
bytes16 nodeId = reclaim.nodeId;
440+
441+
collateralUnderPendingReclaims[hotkey][nodeId] -= reclaim.amount;
442+
alphaCollateralUnderPendingReclaims[hotkey][nodeId] -= reclaim.alphaAmount;
444443
emit Denied(reclaimRequestId, url, urlContentMd5Checksum);
445444

446445
delete reclaims[reclaimRequestId];
446+
447+
// Clear ownership if all balances and pending reclaims are zero
448+
if (
449+
collaterals[hotkey][nodeId] == 0 &&
450+
alphaCollaterals[hotkey][nodeId] == 0 &&
451+
collateralUnderPendingReclaims[hotkey][nodeId] == 0 &&
452+
alphaCollateralUnderPendingReclaims[hotkey][nodeId] == 0
453+
) {
454+
nodeToMiner[hotkey][nodeId] = address(0);
455+
}
447456
}
448457

449458
/// @notice Allows the trustee to slash a miner's collateral for a specific node

crates/collateral-contract/test/CollateralUpgradeable.t.sol

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -206,4 +206,59 @@ contract CollateralUpgradeableTest is Test {
206206
bytes32 indexed oldAlphaColdkey,
207207
bytes32 indexed newAlphaColdkey
208208
);
209+
210+
/// @dev Test that denyReclaimRequest clears nodeToMiner after full slash
211+
function testDenyAfterFullSlashClearsNodeToMiner() public {
212+
vm.deal(alice, 10 ether);
213+
bytes32 hotkey = bytes32(uint256(1));
214+
bytes16 nodeId = bytes16(uint128(1));
215+
216+
// 1. Alice deposits
217+
vm.prank(alice);
218+
collateral.deposit{value: 5 ether}(hotkey, nodeId, alphaHotkey, 0);
219+
assertEq(collateral.nodeToMiner(hotkey, nodeId), alice);
220+
221+
// 2. Alice starts a reclaim
222+
vm.prank(alice);
223+
collateral.reclaimCollateral(
224+
hotkey,
225+
nodeId,
226+
alphaColdkey,
227+
"https://example.com",
228+
bytes16(0)
229+
);
230+
231+
// 3. Trustee fully slashes (collaterals go to zero, but pending reclaim keeps nodeToMiner)
232+
vm.prank(trustee);
233+
collateral.slashCollateral(
234+
hotkey,
235+
nodeId,
236+
5 ether,
237+
0,
238+
"https://example.com/slash",
239+
bytes16(0)
240+
);
241+
// nodeToMiner should still be set because of pending reclaim
242+
assertEq(collateral.nodeToMiner(hotkey, nodeId), alice);
243+
244+
// 4. Trustee denies the reclaim — should now clear nodeToMiner
245+
vm.prank(trustee);
246+
collateral.denyReclaimRequest(
247+
0,
248+
"https://example.com/deny",
249+
bytes16(0)
250+
);
251+
assertEq(
252+
collateral.nodeToMiner(hotkey, nodeId),
253+
address(0),
254+
"nodeToMiner should be cleared after deny with zero balances"
255+
);
256+
257+
// 5. New miner (bob) can deposit on the same node
258+
address bob = address(0xB0B);
259+
vm.deal(bob, 10 ether);
260+
vm.prank(bob);
261+
collateral.deposit{value: 2 ether}(hotkey, nodeId, alphaHotkey, 0);
262+
assertEq(collateral.nodeToMiner(hotkey, nodeId), bob);
263+
}
209264
}

0 commit comments

Comments
 (0)