foundry-rs / foundry

Foundry is a blazing fast, portable and modular toolkit for Ethereum application development written in Rust.
https://getfoundry.sh
Apache License 2.0
8.34k stars 1.77k forks source link

feat(compatibility): add `zkSync` support #7624

Open nbaztec opened 7 months ago

nbaztec commented 7 months ago

Component

Forge

Describe the feature you would like

Foundry zkSync Support

To enable Foundry compatibility with zkSync, we created the fork https://github.com/matter-labs/foundry-zksync. This fork introduces changes to support smart contract compilation, deployment, testing, and interaction on zkSync Era.

This feature request is a proposal to integrate zkSync support into Foundry. It details an approach designed to be minimally invasive, complemented by a subsequent request for zksolc support in foundry-compilers. For more info related to zksolc please refer to here, and here.

Objective

The goal is to integrate zkSync support into Foundry, facilitating community and maintainer dialogue on how best to have these changes accepted.

Below is a brief outline on the differences that are accounted for in the proposed approach.

Differences

zkSync VM

The zkSync VM is fundamentally different from the EVM, in it, it uses an entirely different register-based architecture.

Storage

The Storage is as well different from the EVM's account based trie. In the zkSync VM there's a single global trie. Account balances, nonces, etc. are stored under the hashes slots for special system contracts.

Bytecode

Owing to a different VM, the corresponding bytecode is as well different than what is compiled with solc. In comparison, the zkSync VM is compiled with zksolc, which supports all opcodes, with a few exceptions.

Proposed Approach

1. Compilation

The foundry-identified files are passed through zksolc to generate zkSync VM bytecode. This was then matched using the contract's name with its respective EVM/solc counterpart - finally giving us a DualCompiledContract containing both evm and zk bytecodes and their respective hashes (which also differ in how they are computed). This "registry" of compiled contracts was propagated down to the Executors and to the tracers to perform specialized operations in the context of interoperability between the two scopes, namely EVM and ZK.

We're currently in the process of migrating the compilation logic to foundry-compilers, but the concept of compiling the same contract for both environments and passing it down to the internals would probably remain.

2. forge

The proposed approach leverages the CheatcodeTracer and an early implementation can be reviewed on the dev branch of foundry-zksync.

Consider the following contract:

contract Counter {
    uint256 value;

    function setValue(uint256 _value) {
        value = _value;
    }

    function getValue() view returns (uint256) {
        return value;
    }
}

contract CounterTest is Test {
    Counter counter;

    function setUp() {
        counter = new Counter();
    }

    function testSetValue() {
        counter.setValue(10);
        assertEq(10, counter.getValue());
    }
}

We assume the test contract is being tested for deployment on zkSync.

  1. Foundry compiles the test suite with solc and then zksolc. Both bytecodes are bundled as DualCompiledContract and passed down to the Executor, till the CheatcodeTracer.
  2. All interactions from the default CALLER account to the deployed test contract (including ensure_success calls) are handled as normal EVM calls. Except,
    1. address.balance , which are intercepted as opcodes and retrieve the data from ZK-storage
    2. block.timestamp, block.number, which are updated with the ZK-specific context on the Env directly.
  3. Any CALL or CREATE is intercepted in the tracer (with call and create hooks) and translated into a ZK transactions. This includes
    1. Fetching ZK-equivalent bytecode
    2. Fetching correct account nonce, etc.
    3. Marking the callee as EOA (in ZK terms) to bypass EIP-3607 restriction
  4. The transaction is then sent to ZK-VM where it returns the statediff, which is applied on the journaled_state, and the result returned back.
  5. Any console.log() in the ZK-VM execution is translated back for foundry to pick up

This forms the basic premise of our implementation that foundry gets to do foundry-specific operations, and only at the necessary stage with invoke the zkSync VM when passing the --zksync flag.

Challenges

Plan

Feature Flag

It is proposed to put all zkSync related features behind a zksync feature flag. Rust nightly would be required to compile with this flag, as the zksync libraries currently depend on nightly rust.

Foundry zkSync

A single foundry-zksync module will contain all zkSync specific logic, and translations. Other parts of foundry source will simply use this module.

Foundry Compilers

zksolc compilation would initially be added to foundry-compilers that would later be used in foundry.

Forge Commands

Forge commands like test, create, build, script would be incrementally supported as pull requests.

Conclusion

This is obviously the result of work and testing over several months, and is offered as the best case from our current perspective to integrate foundry into the zkSync ecosystem, while maintaining the same level of foundry's user experience. On that front, we'd like to get the thoughts of foundry developers on anything we may have missed, misunderstood, or vaguely underestimated, in our proposed implementation.

Contributors

@aon, @Deniallugo, @dutterbutter, @HermanObst, @Jrigada, @Karrq, @nbaztec

Additional context

No response

grandizzy commented 1 week ago

@nbaztec we're supportive in adding such and rolling it in post v1, reviewed https://gist.github.com/nbaztec/205ac47e8e68bbb3264402965816a413 and would like to continue discussion here on impl. CC @zerosnacks @klkvr @DaniPopes

zerosnacks commented 1 week ago

My preference goes to strategy 1a using generics: https://gist.github.com/nbaztec/205ac47e8e68bbb3264402965816a413#method-1-strategy for performance reasons + type safety at compile time over 1b given that it is not necessary to switch between strategies at runtime. One of my concerns is that the change would be quite invasive making it challenging to roll out in parts. A practical next step could be for ZKSync to implement an example in a draft PR exploring the integration as well as verifying the proposed workaround of Arc/Arc<Mutex<_>> containers with an example.

popzxc commented 1 week ago

A practical next step could be for ZKSync to implement an example in a draft PR exploring the integration as well as verifying the proposed workaround of Arc/Arc<Mutex<_>> containers with an example.

Yup, that's our plan. We're currently analyzing the areas where the implementation paths diverge, and want to initially build a PoC that would at least demonstrate the required abstractions. For that PoC we may probably use dynamic dispatch, as it's easier to implement, and then it will be rewritten to use static typing.

nbaztec commented 6 days ago

We have an example PR that I made in the making of the abstraction doc using dynamic dispatch (as it involved the least amount of code changes) here https://github.com/matter-labs/foundry-zksync/pull/692/files

It's currently not tracking the latest main but captures the essence of the implementation.