When the operator stakes their vault to the DSS the DSS gains the ability to slash the operator if they are not performing the tasks as it is needed. That ability is used to punish bad operators and the DSS should only be able to slash vaults that are assigned to them and no other vault. However due to the way that slashing is being finalized the DSS can slash the operator even after they have unstaked their vault from the DSS. That issue happens due to the following flow missing an important validation:
The DSS decides to request a slashing of the operator's vault by calling the requestSlashing function in the Core.sol which checks if the operator is registered to the DSS and their vault is staked to that DSS:
https://github.com/code-423n4/2024-07-karak/blob/f5e52fdcb4c20c4318d532a9f08f7876e9afb321/src/Core.sol#L220-L231
function finalizeSlashing(CoreLib.Storage storage self, QueuedSlashing memory queuedSlashing) external {
bytes32 slashRoot = calculateRoot(queuedSlashing);
if (!self.slashingRequests[slashRoot]) revert InvalidSlashingParams();
if (queuedSlashing.timestamp + Constants.SLASHING_VETO_WINDOW > block.timestamp) { //@audit no max time or active vault checks, the DSS can start a slashing and wait until the operator has staked their vault to another DSS and then once again slash it even though it doesn't belong to the DSS
revert MinSlashingDelayNotPassed();
}
delete self.slashingRequests[slashRoot];
for (uint256 i = 0; i < queuedSlashing.vaults.length; i++) {
IKarakBaseVault(queuedSlashing.vaults[i]).slashAssets(
queuedSlashing.earmarkedStakes[i],
self.assetSlashingHandlers[IKarakBaseVault(queuedSlashing.vaults[i]).asset()]
);
}
IDSS dss = queuedSlashing.dss;
HookLib.callHookIfInterfaceImplemented({
dss: dss,
data: abi.encodeWithSelector(dss.finishSlashingHook.selector, queuedSlashing.operator),
interfaceId: dss.finishSlashingHook.selector,
ignoreFailure: true,
hookCallGasLimit: self.hookCallGasLimit,
supportsInterfaceGasLimit: self.supportsInterfaceGasLimit,
hookGasBuffer: self.hookGasBuffer
});
}
Impact
However as can be seen here the library doesn't check wether the vault is still staked to the DSS as such the DSS can request the slashing, wait an arbitrarily long time and even after the operator has unstaked their vault from the DSS they can finalize the slashing thus punishing the operator even after they have unstaked or even registered to another DSS.
Proof of Concept
Create a Vault
Register to a DSS
Stake the vault to the DSS
Request slashing as the DSS
Wait more than 2 days
Unstake the vault or even register to another DSS
Finalize the slashing and slash the operator's funds
Tools Used
Manual review
Recommended Mitigation Steps
Check whether the operator is still staked to the DSS when finalizing the slashing.
Lines of code
https://github.com/code-423n4/2024-07-karak/blob/f5e52fdcb4c20c4318d532a9f08f7876e9afb321/src/entities/SlasherLib.sol#L94-L124 https://github.com/code-423n4/2024-07-karak/blob/f5e52fdcb4c20c4318d532a9f08f7876e9afb321/src/entities/SlasherLib.sol#L126-L151 https://github.com/code-423n4/2024-07-karak/blob/f5e52fdcb4c20c4318d532a9f08f7876e9afb321/src/Core.sol#L220-L231 https://github.com/code-423n4/2024-07-karak/blob/f5e52fdcb4c20c4318d532a9f08f7876e9afb321/src/Core.sol#L248-L256
Vulnerability details
When the operator stakes their vault to the DSS the DSS gains the ability to slash the operator if they are not performing the tasks as it is needed. That ability is used to punish bad operators and the DSS should only be able to slash vaults that are assigned to them and no other vault. However due to the way that slashing is being finalized the DSS can slash the operator even after they have unstaked their vault from the DSS. That issue happens due to the following flow missing an important validation: The DSS decides to request a slashing of the operator's vault by calling the
requestSlashing
function in theCore.sol
which checks if the operator is registered to the DSS and their vault is staked to that DSS: https://github.com/code-423n4/2024-07-karak/blob/f5e52fdcb4c20c4318d532a9f08f7876e9afb321/src/Core.sol#L220-L231then the request is decoded in the
SlasherLib.sol
and stored in theslashingRequests
for future use: https://github.com/code-423n4/2024-07-karak/blob/f5e52fdcb4c20c4318d532a9f08f7876e9afb321/src/entities/SlasherLib.sol#L94-L124After some time the DSS can call the
finalizeSlashing
function which will validate whether the root of that request is stored in the mapping and whether the veto period of 2 days has passed and if so slash the vault of the operator as can be seen in those two functions: https://github.com/code-423n4/2024-07-karak/blob/f5e52fdcb4c20c4318d532a9f08f7876e9afb321/src/Core.sol#L248-L256https://github.com/code-423n4/2024-07-karak/blob/f5e52fdcb4c20c4318d532a9f08f7876e9afb321/src/entities/SlasherLib.sol#L126-L151
Impact
However as can be seen here the library doesn't check wether the vault is still staked to the DSS as such the DSS can request the slashing, wait an arbitrarily long time and even after the operator has unstaked their vault from the DSS they can finalize the slashing thus punishing the operator even after they have unstaked or even registered to another DSS.
Proof of Concept
Tools Used
Manual review
Recommended Mitigation Steps
Check whether the operator is still staked to the DSS when finalizing the slashing.
Assessed type
Invalid Validation