title : Incorrect max precompile address
잘못된 precompile 주소 max 설정이 영향을 미쳤다.
zkSync 는 zkProof 생성 때문에 자체 vm을 사용하는데 이게 EVM 구조랑 조금 다르다.
일반적인 evm 에서의 precompile은 빠르게 실행만 되면 되기 때문에 native로 구현된다.
zkSync의 경우에는 모든 연산이 ZKP로 증명 되어야 하기 때문에 이에 맞춰 precompiles 또한 vm위에서 실행되는 형태(yul)로 구현된다.
/system-contracts/contracts/precompiles 쪽을 보면 yul로 구현된 타원곡선상 연산, Ecrecover, keccak256, SHA256 이 있다.
/// @dev All the system contracts introduced by zkSync have their addresses
/// started from 2^15 in order to avoid collision with Ethereum precompiles.
uint160 constant SYSTEM_CONTRACTS_OFFSET = 0x8000; // 2^15
/// @dev All the system contracts must be located in the kernel space,
/// i.e. their addresses must be below 2^16.
uint160 constant MAX_SYSTEM_CONTRACT_ADDRESS = 0xffff; // 2^16 - 1
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);
/system-contracts/contracts/Constants.sol 을 보면 각 precompiles의 주소가 적혀있다.
/// @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.
uint256 constant CURRENT_MAX_PRECOMPILE_ADDRESS = uint256(uint160(SHA256_SYSTEM_CONTRACT));
더 밑을 보면 note: 가 있는데
현재 2개의 precompiles가 배포되어 있다고 한다.
추가로 추가적인 precompiles가 배포되면 업데이트 해야 한다고 적혀있다.
그런데 위에 ECADD와 ECMUL precompiles가 추가 되었는데 CURRENT_MAX_PRECOMIPLES에 들어가는 값은 SHA256_SYSTEM_CONTRACT 이다.
이러면 CURRENT_MAX_PRECOMPILES가 0x02가 된다.
/// @notice Simulate the behavior of the `extcodehash` EVM opcode.
/// @param _input The 256-bit account address.
/// @return codeHash - hash of the bytecode according to the EIP-1052 specification.
function getCodeHash(uint256 _input) external view override returns (bytes32) {
// We consider the account bytecode hash of the last 20 bytes of the input, because
// according to the spec "If EXTCODEHASH of A is X, then EXTCODEHASH of A + 2**160 is X".
address account = address(uint160(_input));
if (uint160(account) <= CURRENT_MAX_PRECOMPILE_ADDRESS) {
return EMPTY_STRING_KECCAK;
}
bytes32 codeHash = getRawCodeHash(account);
// The code hash is equal to the `keccak256("")` if the account is an EOA with at least one transaction.
// Otherwise, the account is either deployed smart contract or an empty account,
// for both cases the code hash is equal to the raw code hash.
if (codeHash == 0x00 && NONCE_HOLDER_SYSTEM_CONTRACT.getRawNonce(account) > 0) {
codeHash = EMPTY_STRING_KECCAK;
}
// The contract is still on the constructor, which means it is not deployed yet,
// so set `keccak256("")` as a code hash. The EVM has the same behavior.
else if (Utils.isContractConstructing(codeHash)) {
codeHash = EMPTY_STRING_KECCAK;
}
return codeHash;
}
해당 코드에서 의도 대로라면 precompile은 EMPTY_STRING_KECCAK을 반환해야 한다.
그런데 앞에서 잘못 설정된 CURRENT_MAX_PRECOMPILE_ADDRESS로 인해서 ECADD와 ECMUL 주소를 넣을 시 EMPTY_STRING_KECCAK 상수 대신 0 이 반환된다.
0이 반환되는 이유
zkSync의 precompile은 일반 컨트랙트 처럼 Account CodeStorage에 bytecode hash를 등록하지 않음.
- Precompile은 EraVM이 특수 경로로 실행하는 코드임
- 일반 컨트랙트 배포 플로우 (ContractDeployer)를 타지 않음
- 따라서 AccountCodeStorage에 code hash가 기록되지 않아서 getRawCodeHash() 가 0x00을 반환함.