Platform: Code4rena
Start Date: 02/10/2023
Pot Size: $1,100,000 USDC
Total HM: 28
Participants: 64
Period: 21 days
Judge: GalloDaSballo
Total Solo HM: 13
Id: 292
League: ETH
Rank: 42/64
Findings: 1
Award: $656.33
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 Selected for report: zero-idea
Also found by: 0x1337, 0xTheC0der, 0xstochasticparrot, Audittens, HE1M, Jeiwan, erebus, gumgumzum, leviticus1129, lsaudit, quarkslab, rvierdiiev
656.3255 USDC - $656.33
https://github.com/code-423n4/2023-10-zksync/blob/c857609bfdc41a0ee2c1b245217a785f66b42a56/code/system-contracts/contracts/Constants.sol#L35 https://github.com/code-423n4/2023-10-zksync/blob/c857609bfdc41a0ee2c1b245217a785f66b42a56/code/system-contracts/contracts/AccountCodeStorage.sol#L93-L95 https://github.com/code-423n4/2023-10-zksync/blob/c857609bfdc41a0ee2c1b245217a785f66b42a56/code/system-contracts/contracts/AccountCodeStorage.sol#L132
EXTCODEHASH
and EXTCODESIZE
return real values for ECADD
and ECMUL
precompiles, as well as for KECCAK256
and EVENT_WRITER
precompiles. This breaks EIP-1052, which zkSync seeks to implement, and may cause incompatibility with the Ethereum VM.
The severity is medium because zkSync tries to achieve the equivalence with EVM (where possible), and any unexpected discrepancy can cause incompatibility issues for projects running on zkSync.
The AccountCodeStorage.getCodeHash() function simulates the behavior of the extcodehash
EVM opcode; the AccountCodeStorage.getCodeSize() function simulates the behavior of the extcodesize
EVM opcode. As per their descriptions:
getCodeHash()
returns:
codeHash - hash of the bytecode according to the EIP-1052 specification.
getCodeSize()
:
// NOTE: zero address and precompiles are a special case, they are contracts, but we // want to preserve EVM invariants (see EIP-1052 specification). That's why we automatically // return
0
length in the following cases: // -codehash(0) == 0
// -account
is a precompile. // -account
is currently being constructed
As per EIP-1052:
The EXTCODEHASH of an precompiled contract is either c5d246... or 0.
However, the two functions fail to correctly identify deployed precompiles. They both use the CURRENT_MAX_PRECOMPILE_ADDRESS
constant, which is defined as (Constants.sol#L35):
uint256 constant CURRENT_MAX_PRECOMPILE_ADDRESS = uint256(uint160(SHA256_SYSTEM_CONTRACT));
However, there are new precompiles deployed after the SHA256 precompile (Constants.sol#L25-L28):
address constant ECRECOVER_SYSTEM_CONTRACT = address(0x01); address constant SHA256_SYSTEM_CONTRACT = address(0x02); address constant ECADD_SYSTEM_CONTRACT = address(0x06); address constant ECMUL_SYSTEM_CONTRACT = address(0x07);
(Also, ECADD
and ECMUL
are said to be precompiles by the documentation.)
The CURRENT_MAX_PRECOMPILE_ADDRESS
constant wasn't updated to include the new precompiles, although it has a note about that (Constants.sol#L30-L34):
/// @dev The current maximum deployed precompile address.
/// Note: currently only two precompiles are deployed:
/// 0x01 - ecrecover
/// 0x02 - sha256
/// Important! So the constant should be updated if more precompiles are deployed.
Here's a test case that demonstrates the issue:
// code/system-contracts/test/AccountCodeStorage.spec.ts it.only('AUDIT undetected precompiles', async () => { await accountCodeStorage .connect(deployerAccount) .storeAccountConstructedCodeHash('0x0000000000000000000000000000000000000006', CONSTRUCTED_BYTECODE_HASH); await accountCodeStorage .connect(deployerAccount) .storeAccountConstructedCodeHash('0x0000000000000000000000000000000000000007', CONSTRUCTED_BYTECODE_HASH); // CORRECT: expect(await accountCodeStorage.getCodeSize('0x0000000000000000000000000000000000000001')) .to.be.eq(0); expect(await accountCodeStorage.getCodeSize('0x0000000000000000000000000000000000000002')) .to.be.eq(0); expect(await accountCodeStorage.getCodeHash('0x0000000000000000000000000000000000000001')) .to.be.eq("0xc5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470"); // hash of empty string expect(await accountCodeStorage.getCodeHash('0x0000000000000000000000000000000000000002')) .to.be.eq("0xc5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470"); // hash of empty string // WRONG: expect(await accountCodeStorage.getCodeSize('0x0000000000000000000000000000000000000006')) .to.be.eq(2097120); expect(await accountCodeStorage.getCodeSize('0x0000000000000000000000000000000000000007')) .to.be.eq(2097120); expect(await accountCodeStorage.getCodeHash('0x0000000000000000000000000000000000000006')) .to.be.eq("0x0100ffffdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef"); expect(await accountCodeStorage.getCodeHash('0x0000000000000000000000000000000000000007')) .to.be.eq("0x0100ffffdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef"); });
Manual review
Consider updating the CURRENT_MAX_PRECOMPILE_ADDRESS
constant to include the addresses of the ECADD
and ECMUL
precompiles.
The status of the KECCAK256
and EVENT_WRITER
is not clear: while they're precompiles (they're stored in the "precompiles" folder), they're deployed in the system contracts address space (Constants.sol#L56, Constants.sol#L62). In case they are also treated as precompiles, consider improving the precompiles detection in the AccountCodeStorage.getCodeHash()
and AccountCodeStorage.getCodeSize()
functions so these addresses are also detected as precompiles (simply updating CURRENT_MAX_PRECOMPILE_ADDRESS
might not work because there's a gap between the precompile addresses and the system contract addresses).
Other
#0 - c4-pre-sort
2023-11-02T14:06:17Z
141345 marked the issue as duplicate of #142
#1 - c4-judge
2023-11-23T19:31:10Z
GalloDaSballo marked the issue as satisfactory
#2 - c4-judge
2023-11-23T19:32:17Z
GalloDaSballo marked the issue as selected for report
#3 - c4-judge
2023-11-23T19:38:47Z
GalloDaSballo marked issue #888 as primary and marked this issue as a duplicate of 888