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.16k stars 1.69k forks source link

Cheatcode supporting the deployment of linked libraries in scripts #3664

Open Craigson opened 1 year ago

Craigson commented 1 year ago

Component

Forge

Describe the feature you would like

At present it's not possible to deploy contracts and the linked libraries they make use of in the same script.

The current steps to achieve this are as follows:

  1. Deploy libraries via dedicated script
  2. Add the linked libraries to the foundry.toml file using the format outlined in the docs: <file>:<lib>:<address>
  3. Deploy contracts via dedicated script

A possible solution could be the introduction of a cheatcode such as linkLib.

pragma solidity 0.8.9;

import "forge-std/Script.sol";

import {MyLinkedLib} from "../utils/MyLinkedLib.sol";
import {MyContract} from "../contracts/MyContract.sol";

contract DeployAll is Script {
    MyLinkedLib linkedLib;
    MyContract myContract;

    constructor() {}

    function run() {
         uint256 deployerKey = vm.envUint("DEPLOYER_PRIVATE_KEY");

        vm.startBroadcast(deployerKey);

        linkedLib = new LinkedLib();

        vm.linkLib("../utils/MyLinkedLib.sol", address(linkedLib));
        myContract = new MyContract();
    }
}

Additional context

No response

snapple42 commented 1 year ago

Should also clarify that you should be able to link to an existing library that's already on chain. It looked like that functionality is available in example. Also the ability to link multiple libraries. This becomes especially useful when upgrading a facet of an eip2535 diamond contact.

Maybe it in the vm.linkLib pass in an array of libraries and address.

rkrasiuk commented 1 year ago

dynamic linking is explicitly not supported rn. imo such script is hardly reusable unless the linked lib is modified thus it should be split into two different scripts

@joshieDo @mattsse what are your thoughts on supporting this?

mattsse commented 1 year ago

hmm, I can see how this could be useful.

but this comes with additional complexity where we now need to link ad-hoc.

@snapple42 is this example public? would like to take a look.

joshieDo commented 1 year ago

Unless I'm missing something, this is possible for single chain deployment (no createFork stuff).

At present it's not possible to deploy contracts and the linked libraries they make use of in the same script.

You can, they'll get deployed and be present in the broadcast log. You might have to move their addresses to foundry.toml later on if you don't want to keep re-deploying them.

Craigson commented 1 year ago

@joshieDo can you share an example / implementation of this? I cannot find a working implementation that shows how to achieve this.

joshieDo commented 1 year ago

In our test suite: https://github.com/foundry-rs/foundry/blob/96fa8a05ecab85649058337d1095fda2b08c3234/cli/tests/it/script.rs#L320-L330 https://github.com/foundry-rs/foundry/blob/96fa8a05ecab85649058337d1095fda2b08c3234/testdata/cheats/Broadcast.t.sol#L36

You shouldn't have to do anything specific for it to work. It should just work. Maybe I'm misunderstading it?

In the above example the script contract BroadcastTest deploys contract Test which depends on library F. If you notice the test, the first account 0 has a nonce increment of 2, since it deploys F first, and then Test.

Let me know if I understood correctly.

snapple42 commented 1 year ago

hmm, I can see how this could be useful.

but this comes with additional complexity where we now need to link ad-hoc.

@snapple42 is this example public? would like to take a look.

Not really public yet with the Foundry updates, but you can look at our existing diamond (which currently doesn't use foundry, but this is what I'm upgrading) at:

https://louper.dev/diamond/0x1A6f75D56aDC64d22bA4B483011ED84d2593d224?network=binance

Idea would be to upgrade a facet (so deploy a new contract) and link it to existing poolUtil library at:

0x930C0290CEa0499e44Db4cbC97d3313CA19a657c (BSC CHAIN)

From truffle deployment: Linking

That would be good to be able to define the address to link to basically so we can reuse existing code/contracts, and we don't get out of sync with versions.

It's not that the library is huge, but would be convenient to be able to reference it instead of re-deploying it.

joshieDo commented 1 year ago

You should be able to reference it for single-chain deployments (no vm.createFork stuff) on foundry.toml without any re-deployment necessary.

Reference: https://book.getfoundry.sh/reference/config/solidity-compiler#libraries

Craigson commented 1 year ago

@joshieDo , my issue I'm experiencing is that if I'm deploying for example 3 contracts that all make use of a shared linked lib (that has never been deployed before, therefore not already linked in the foundry.toml). If this is the first time the lib is being deployed, 3 versions of the lib are deployed (one for each contract) when doing it via a forge script.

The equivalent in Hardhat might look like this:

 beforeEach(async () => {
    signers = await ethers.getSigners();

    const Lib = await ethers.getContractFactory("MyLibrary");
    const lib = await Lib.deploy();
    await lib.deployed();

    const contractFactory = await ethers.getContractFactory("SampleNft", {
      signer: signers[0],
      libraries: {
        MyLibrary: lib.address,
      },
    });

    contract = await contractFactory.deploy();
  });

If deployment is handled via docker, for example, it's not practical to first deploy lib/s, then copy/paste into the foundry.toml before deploying the contracts.

Craigson commented 1 year ago

@joshieDo @mattsse hoping to get some more feedback here, as this is an issue I'm currently facing with a large deployment. When linked libs reference each other, foundry is deploying duplicate versions of the external libs. In my main project, I have 8 external libraries, but a total of 12 are getting deployed, presumably of the cross-linking in the libs, which leads to wasted funds on deployment and issues with verification. Here's an example that reproduces the issue:

// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;

library LibA {
    function doubleIt(uint256[] storage values) external {
        uint256 length = values.length;
        for (uint i; i < length; ) {
            values[i] *= 2;
            unchecked { ++i; }
        }
    }
}

library LibB {
    function tripleIt(uint256[] storage values) external {
        uint256 length = values.length;
        for (uint i; i < length; ) {
            values[i] *= 3;
            unchecked { ++i; }
        }
    }
}

library LibC {
    function quadrupleIt(uint256[] storage values) external {
        LibA.doubleIt(values);
        LibA.doubleIt(values);
    }
}

contract LibRegistry {
    address public a;
    address public b;
    address public c;

    constructor () {
        a = address(LibA);
        b = address(LibB);
        c = address(LibC);
    }
}

Where LibA gets deployed on two separate occasions.

Waiting for receipts.
⠂ [00:00:00] [#################################################################] 5/5 receipts (0.0s)
##### anvil-hardhat
✅ Hash: 0xd56e4e4131e2eaf0e4dd78c71b70fb2c586fca55c22301665420d99d59cc1609
Contract Address: 0x8464135c8f25da09e49bc8782676a84730c318bc
Block: 1
Paid: 0.000444498687845124 ETH (117892 gas * 3.770388897 gwei)

##### anvil-hardhat
✅ Hash: 0x33210eb0677a6510e1aeb54147d1c93b3aaf58ef9aa234b74f68e4adcb0c211f
Contract Address: 0x71c95911e9a5d330f4d621842ec243ee1343292e
Block: 2
Paid: 0.000444498687845124 ETH (117892 gas * 3.770388897 gwei)

##### anvil-hardhat
✅ Hash: 0xb74a5bf8b8b4487e9fdd65f5f6092b99df5b06b120acb53cd1d013c1907b10dc
Contract Address: 0x948b3c65b89df0b4894abe91e6d02fe579834f8f
Block: 2
Paid: 0.000444498687845124 ETH (117892 gas * 3.770388897 gwei)

##### anvil-hardhat
✅ Hash: 0xe1bddcf803699eb478066b58fc72f85f05b9ad9f57a3bfc7851dbf4aff70717f
Contract Address: 0x712516e61c8b383df4a63cfe83d7701bce54b03e
Block: 2
Paid: 0.000504493115974188 ETH (133804 gas * 3.770388897 gwei)

##### anvil-hardhat
✅ Hash: 0x643d191ad78d1dda2796e71814a1849dc164fa40e1e0c190c1a11537d6c930c1
Contract Address: 0xbcf26943c0197d2ee0e5d05c716be60cc2761508
Block: 2
Paid: 0.000623071846784838 ETH (165254 gas * 3.770388897 gwei)

The run-latest.json includes the 3 libraries, but doesn't take into account the duplicates. It's also been causing issues with verification, where I can't verify certain contracts that are referenced by other contracts.

  "libraries": [
    "src/LibRegistry.sol:LibA:0x948b3c65b89df0b4894abe91e6d02fe579834f8f",
    "src/LibRegistry.sol:LibB:0x71c95911e9a5d330f4d621842ec243ee1343292e",
    "src/LibRegistry.sol:LibC:0x712516e61c8b383df4a63cfe83d7701bce54b03e"
  ],
alfredolopez80 commented 1 year ago

@joshieDo , my issue I'm experiencing is that if I'm deploying for example 3 contracts that all make use of a shared linked lib (that has never been deployed before, therefore not already linked in the foundry.toml). If this is the first time the lib is being deployed, 3 versions of the lib are deployed (one for each contract) when doing it via a forge script.

The equivalent in Hardhat might look like this:

 beforeEach(async () => {
    signers = await ethers.getSigners();

    const Lib = await ethers.getContractFactory("MyLibrary");
    const lib = await Lib.deploy();
    await lib.deployed();

    const contractFactory = await ethers.getContractFactory("SampleNft", {
      signer: signers[0],
      libraries: {
        MyLibrary: lib.address,
      },
    });

    contract = await contractFactory.deploy();
  });

If deployment is handled via docker, for example, it's not practical to first deploy lib/s, then copy/paste into the foundry.toml before deploying the contracts.

This is the example, is possible replicate that in Foundry Forge?? because is a lot usefull for develop, in diamond pattern !!, because avoid redeploy in two steps and reduce a lot the time!!!