Reproduced Exploit
Karak — [H-02] An operator can create a `NativeVault` that is silently unslashable
1. In Karak, an operator deploys a NativeVault via Core.deployVaults(), passing extraData = (manager, slashStore, nodeImplementation). 2. NativeVault.initialize() stores that slashStore verbatim, with no validation against the protocol's whitelisted slashing handler for the asset.
Chain
Other
Category
access-control
Date
Jul 2024
Source
AuditVault
EVM Playground
Source-level debugger — step opcodes and Solidity in sync
The attack is replayed in an in-browser EVM preloaded with the exact dumped fork state. The execution tree shows every call; step by Solidity line or by opcode across all depths — source, Stack, Memory, Storage, Transient storage and Return value stay in sync. Click a tree node, opcode, or source line to jump. No backend, no live RPC.
Source & credit. Reproduction of a public audit finding curated by AuditVault — the original finding: 41066-h-02-the-operator-can-create-a-nativevault-that-can-be-silen. Standalone Foundry PoC and full write-up: 41066-h-02-the-operator-can-create-a-nativevault-that-can-be-silen_exp in the
evm-hack-registrymirror.
Vulnerability classes: vuln/access-control/missing-validation · vuln/logic/missing-check · vuln/dos/permanent
Reproduction: a self-contained Foundry PoC that compiles & runs in an isolated project with only
forge-std— no fork, no RPC, noanvil_state(the vulnerable restaking contracts are deployed locally). Full trace: output.txt. PoC: test/41066-h-02-the-operator-can-create-a-nativevault-that-can-be-silen_exp.sol.
Key info#
| Impact | HIGH — a misbehaving operator can make their own vault permanently unslashable, keeping 100% of stake at zero risk; slashing (the protocol's core accountability mechanism) is defeated |
| Protocol | Karak — restaking (Core / NativeVault / SlasherLib) |
| Vulnerable contract | NativeVault — initialize() (stores operator slashStore unvalidated) + slashAssets() (reverts unless whitelisted handler == that slashStore) |
| Bug class | Operator-controlled extraData.slashStore is stored with no validation against the protocol's whitelisted slashing handler; the mismatch makes every real slash revert |
| Finding | Code4rena — 2024-07 Karak · #41066 |
| Report | code4rena.com/reports/2024-07-karak |
| Source | AuditVault |
| Status | Audit finding — caught in review (not exploited on-chain). Reproduced here as a standalone local PoC. |
| Compiler | ^0.8.24 (PoC) |
This is an audit finding, not a historical on-chain incident — there is no
attack transaction. The PoC deploys a faithful minimal Core/NativeVault
locally and demonstrates the defeated slash with a control vault for contrast.
TL;DR#
- In Karak, an operator deploys a
NativeVaultviaCore.deployVaults(), passingextraData = (manager, slashStore, nodeImplementation). NativeVault.initialize()stores thatslashStoreverbatim, with no validation against the protocol's whitelisted slashing handler for the asset.- When the protocol slashes,
SlasherLibcallsslashAssets()with the whitelisted handler (assetSlashingHandlers[asset]).slashAssets()reverts unless that handler equals the operator'sslashStore:if (slashingHandler != self.slashStore) revert NotSlashStore();. - So an operator who sets
slashStoreto any other address makes every real slash revert — the vault is permanently unslashable. - A malicious operator can therefore misbehave (get earmarked for slashing) and keep 100% of their stake. The PoC's honest control vault slashes 100→90 ETH; the poisoned vault stays at 100 ETH.
The vulnerable code#
NativeVault is reduced to the exact accounting the bug depends on; the missing
validation and the reverting check are preserved verbatim.
initialize() — stores the operator's slashStore with no validation (root cause)#
function initialize(address _core, address _asset, bytes memory extraData) external {
...
// @> operator-supplied slashStore is stored verbatim — never checked against the
// protocol's whitelisted slashing handler for `asset`.
(address manager, address slashStore,) = abi.decode(extraData, (address, address, address));
_state().slashStore = slashStore;
_state().manager = manager;
}
slashAssets() — reverts unless the whitelisted handler == that slashStore#
function slashAssets(uint256 totalAssetsToSlash, address slashingHandler) external onlyOwner returns (uint256) {
State storage self = _state();
if (slashingHandler != self.slashStore) revert NotSlashStore(); // @> NativeVault.sol:L308
...
}
SlasherLib passes the whitelisted handler (so the mismatch is guaranteed)#
NativeVault(vaults[i]).slashAssets(
earmarkedStakes[i], assetSlashingHandlers[NativeVault(vaults[i]).asset()]
);
Root cause#
A missing input-validation check: deployVaults() → initialize() never
verifies the operator-chosen slashStore against assetSlashingHandlers[asset].
The slashAssets() guard then compares the protocol's handler against that
operator's value, so any mismatch — trivially arranged by the operator — turns
the guard into a permanent DoS of slashing for that vault.
Preconditions#
- An operator can call
deployVaults()with arbitraryextraData(permissionless for operators — the intended entrypoint). - The protocol slashes via the whitelisted
assetSlashingHandlers[asset](its normal, correct behavior). - No timing or capital advantage needed; the operator simply picks a bad
slashStoreat creation.
Attack walkthrough#
From output.txt, two vaults contrasted:
- Honest control — vault created with
slashStore == whitelisted handler. Protocol slashes 10 ETH →totalAssetsdrops 100 → 90. Slashing works. - Attack — operator creates a vault with
slashStore = address(666). - Protocol attempts the same 10 ETH slash;
slashAssets()seeswhitelistedHandler != slashStoreand reverts withNotSlashStore(). - The misbehaving operator's stake is untouched:
totalAssets == 100 ETH— it can never be penalized. The PoC asserts both the revert and the retained stake; the Playground mints the 100 ETH of shielded stake as aSHIELDmarker.
Diagrams#
Remediation#
Validate the operator-supplied slashStore against the protocol's whitelisted
handler at vault creation (or derive it from the whitelist rather than accepting
it from extraData):
// in initialize()/deployVaults(), after decoding extraData:
require(slashStore == core.assetSlashingHandlers(asset), "invalid slashStore");
The Karak team's noted mitigation is to register a verified slashingHandler for
ETH on Core and verify the operator uses the correct slashStore.
How to reproduce#
cd ~/RustroverProjects/audits/evm-hack-registry/41066-h-02-the-operator-can-create-a-nativevault-that-can-be-silen_exp
forge test -vvv
# Fully local — no fork, no RPC, no anvil_state required.
# Expected: both tests PASS:
# test_honestVault_isSlashable (control: honest vault slashes 100 -> 90)
# test_createUnslashableVault (attack: slash reverts; operator keeps 100 ETH)
PoC source: test/41066-h-02-the-operator-can-create-a-nativevault-that-can-be-silen_exp.sol
— a faithful minimal Core/NativeVault with the unvalidated initialize and
the verbatim slashAssets check, plus honest-vs-poisoned contrast.
Reference: finding #41066 in the Code4rena 2024-07 Karak audit · curated by AuditVault
Sources & further analysis#
Reproductions & code
- Standalone PoC + full trace: 41066-h-02-the-operator-can-create-a-nativevault-that-can-be-silen_exp (evm-hack-registry mirror).
- AuditVault finding: 41066-h-02-the-operator-can-create-a-nativevault-that-can-be-silen.
Alerts & third-party analyses
These dashboards index community alerts tweets, post-mortems, and independent write-ups. Reach them through the protocol name above to cross-check this reproduction against other analyses.