Technology

Toward Ethereum Equivalence #2 — Changes in Precompiled Contract Addresses

Precompiled contracts have changed with Klaytn v1.8.0. In this post, we will explain the reasons for these changes as well as their effect on the existing contracts utilizing precompiled contracts.

TL;DR

  • Any contracts deployed before the v.1.8.0 hard fork which are using precompiled contracts with direct in-contract calls will work the same as before the hard fork.
  • Smart contracts deployed before the v.1.8.0 hard fork which are using precompiled contracts using delegated calls, may function differently depending on the context of the caller.

Purpose of precompiled contract changes

In order to support the precompiled contracts that have been newly added to Ethereum, Klaytn’s existing precompiled contract addresses have been mapped to new addresses in order not to overlap with the existing Ethereum assignments.

Precompiled contracts to be changed

In the above table, the address of Klaytn’s vmLog has been changed because it overlaps with Ethreum’s Blake2F. Klaytn’s other precompiled contracts like feePayer and validateSender do not yet overlap with Ethereum’s precompiled contracts, however they have also been modified to prevent future overlaps.

Effect of the precompiled contract address changes

To minimize the impact of the precompiled contract address changes, Klaytn also stores the latest hard fork (protocol upgrade) version data when storing a contract reference in the chaindata of the mainnet. This allows users to know whether a contract has been deployed prior to the Istanbul hard fork.

Hard fork refers to the upgrading of the core protocol running on the Klaytn blockchain. All nodes constituting the blockchain have to implement the upgrade. Since Klaytn chains do not allow forking at the consensus level, the term “hard fork” often used in the crypto jargon is meant here as “protocol upgrade”.

When Klaytn runs a contract, it checks whether the contract was deployed before the Istanbul hard fork. If it is deployed before the hard fork, it uses the Constantinople precompiled contract map, which is the same one from the time of deployment. That way, the deployed contract can function the same way even after the hard fork.

However, a contract calling a precompiled contract utilizing delegateCall could potentially be problematic. This command executes a contract not from the callee but the caller’s perspective. If a precompiled contract’s caller is an Externally Owned Account (EOA), it does not have access to the hard fork version data and will always use the latest version of the precompiled contract map. Therefore if an EOA was to use DelegateCall to call a precompiled contract and its expected behavior to be in-line with the pre-Istanbul precompiled address map, an error will occur.

When using call for precompiled contracts

If you are using calls for precompiled contracts in a contract deployed before the hard fork, you won’t have any problems. It will work as expected even if the precompiled contract address changes following the hard fork. (The code below is a sample calling a feepayer precompiled contract.)

Even if the precompiled contract address has changed, the called contract can tell “at which hard fork it was deployed”, so it can use a precompiled contract address map that matches the hard fork version to call the precompiled contract.

The image below demonstrates a contract using feePayer(0x0a), a precompiled contract deployed before the Istanbul hard fork.

Let’s assume that we called a function in Contract A that calls feePayer after the hard fork (address change). Contract A is still using the old address 0x0a to call feePayer. The caller (msg.sender) of the precompiled contract feePayer is Contract A, and since we know that Contract A was deployed before the hard fork, we can use a corresponding precompiled contract map to make a call to feePayer.

To sum up, if you are calling a precompiled contract using the call method, your smart contracts and related codebase will not be affected by the address change.

When using delegateCall for precompiled contracts

Most of the contracts deployed before the hard fork implementation will work the same as before. However, if the contract intends to call precompiled contracts 0x09, 0x0a or 0x0b using delegateCall, you may encounter an error. (Refer to some examples in Klaytn Docs using the call methods in different inline assembly functions)

To help explain the source of error, we will use our previous Example contract, but this time using delegatecall method instead of a direct call method. Let’s say that we deployed this Example contract which sends delegateCalls to the feePayer with the address 0x0a; 0x0a was the assigned feePayer address before the hard fork. (The code below is a sample that makes a delegateCall to a feePayer precompiled contract.)

Then what would happen if we call the feePayer function in the Example contract after the precompiled contract map has been updated after the hard fork? This answer depends on the context of the calling account assigned to msg.sender.

The feePayer function in the Example Contract is executed and makes a delegateCall to the precompiled contract. If the Example calling address (msg.sender) is an EOA, it will always use the latest precompiled contract map, since EOAs do not store any hard fork data, as previously mentioned.

In this Example contract, the delegatecall call is made within the EOA’s context instead of contract A’s. The EOA’s context is based on when the transaction is executed, whether 0x0a is a feePayer contract or not depends on the implementation of the hard fork for the block number at which the transaction is executed.

In such cases, behavior will work in a way that is different from the initial behavior of the deployed Example contract due to the fact that the address 0x0a is no longer the address of a feePayer precompiled contract at the time of the call, and the msg.sender context of EOA accounts only recognizes the latest precompiled contracts map.

Similarly, if our Example contract (delegatecall) was to be called by another contract, for instance, Contract A calls Example Contract (rather than an EOA account), then the msg.sender context is that of Contract A. If Contract A was deployed before the Istanbul hard fork, its context would dictate the same result as an EOA call. That is to say, a contract deployed before the hard-fork has a context that still assumes that feePayer is at address 0x0a.

However, if a calling contract is deployed after the Istanbul hard fork then its context dictates that the latest precompiled map is used, and therefore the call will resolve correctly.

Conclusion

If you are using the call method to call a precompiled contract in a contract deployed before the hard fork, it won’t be affected by the change.

But if you are using the delegateCall method, there could be issues depending on the caller’s context. Please be mindful of this and redeploy your contract if necessary. For such situations we have two recommendations:

  • When deploying new contracts which interact with precompiled contracts, use the call method whenever possible instead of the delegatecall method; if the delegatecall method is used then be aware of the above described behaviors
  • If old deployed contracts have logic where a delegated call to a precompiled contract is exposed to EOA contexts, then consider creating and deploying a wrapper contract around this functionality, this will ensure that any delegatecall will assume the wrapper contract context; the wrapper contract must be deployed after the v1.8.0 hard fork for the correct context to be certain

Article Series