Reproduced Exploit
Kakarot — precompile allowlist keys the caller code address
exec_precompile authorizes Kakarot precompiles with caller_code_address and never looks at caller_address. Delegatecall runs another contract's bytecode at the caller's address, and immutables in that bytecode still name the listed contract. The precompile therefore treats the delegatecall as a cal…
Chain
zkSync
Category
access-control
Date
Sep 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, Balances (native / ERC-20 / NFT), 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: 45168-h-01-unauthorized-contracts-can-bypass-precompile-authorizat. Standalone Foundry PoC and full write-up: 45168-h-01-unauthorized-contracts-can-bypass-precompile-authorizat_exp in the
evm-hack-registrymirror.
Vulnerability classes: vuln/access-control/broken-logic · vuln/access-control/missing-auth
Key info#
| Protocol | Kakarot zkEVM |
| Finding | Code4rena, September 2024, AuditVault 45168 |
| Vulnerable code | exec_precompile whitelists caller_code_address |
| Harm | An unlisted contract pulls 1000 custodied units. An execution-address check pays 0. |
| Chain | local synthetic (audited zkEVM rule, modeled in Solidity) |
TL;DR#
exec_precompile authorizes Kakarot precompiles with caller_code_address and never looks at caller_address. Delegatecall runs another contract's bytecode at the caller's address, and immutables in that bytecode still name the listed contract. The precompile therefore treats the delegatecall as a call from listed code.
The precompile custodies 1000 units. The unlisted contract delegatecalls the listed code and receives all 1000. guardedPull on the execution address returns 0. A direct call that presents the unlisted contract as the code address reverts.
The vulnerable code#
The Cairo check is is_caller_whitelisted(caller_code_address). The synthetic precompile is the same predicate. The code address is the listed contract's immutable, which delegatecall does not rewrite.
function privilegedPull(address callerCode, address to, uint256 amount) external {
// @> VULN: the allowlist is the caller code address, not the execution address.
require(allowed[callerCode], "unauthorized");
asset.transfer(to, amount);
}
The execution-address control:
function guardedPull(address execution, uint256 amount) external view returns (uint256) {
if (!allowed[execution]) return 0;
return amount;
}
Root cause#
The allowlist key is the code being executed, not the account whose storage and balance the call runs against. Delegatecall splits those two. Any listed contract that can be delegatecalled, including one that forwards a caller-supplied recipient, becomes a pass for every other contract.
Preconditions#
One whitelisted contract whose bytecode calls the precompile and carries its own address as an immutable. The precompile holds 1000 units. No second block.
Attack walkthrough#
- The listed code address is on the allowlist. This contract's address is not.
- Delegatecall
pull(this, 1000)on the listed code. The immutable still names the listed contract. The execution address is this contract. privilegedPullaccepts the code address and transfers 1000.- This contract's balance is 1000 and the precompile's balance is 0.
guardedPull(this, 1000)returns 0.- A direct
privilegedPullthat passes this contract as the code address reverts.
Diagrams#
Impact#
The unlisted contract takes the 1000 units the precompile was holding for listed code. The same pull guarded on the execution address pays 0.
Remediation#
Authorize caller_address, or reject delegatecall into custom precompiles. Listed contracts must not assume address(this) is their own account.
How to reproduce#
cd 45168-h-01-unauthorized-contracts-can-bypass-precompile-authorizat_exp
forge test -vv
Sources#
- AuditVault finding: https://github.com/Auditware/AuditVault/blob/main/findings/45168-h-01-unauthorized-contracts-can-bypass-precompile-authorizat.md
- Code4rena report: https://code4rena.com/reports/2024-09-kakarot
Sources & further analysis#
Reproductions & code
- Standalone PoC + full trace: 45168-h-01-unauthorized-contracts-can-bypass-precompile-authorizat_exp (evm-hack-registry mirror).
- AuditVault finding: 45168-h-01-unauthorized-contracts-can-bypass-precompile-authorizat.
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.