From 24902af90fb7d4a185eadf219442c11e83606d79 Mon Sep 17 00:00:00 2001 From: bohendo Date: Thu, 26 Sep 2024 14:12:25 -0400 Subject: [PATCH 01/29] init stateful fuzz harness --- .gitignore | 4 + pkg/vault/echidna.yml | 4 + pkg/vault/medusa.json | 84 +++ pkg/vault/package.json | 2 + pkg/vault/test/foundry/fuzz/FuzzHarness.sol | 518 ++++++++++++++++++ .../test/foundry/utils/VaultMockDeployer.sol | 42 ++ 6 files changed, 654 insertions(+) create mode 100644 pkg/vault/echidna.yml create mode 100644 pkg/vault/medusa.json create mode 100644 pkg/vault/test/foundry/fuzz/FuzzHarness.sol create mode 100644 pkg/vault/test/foundry/utils/VaultMockDeployer.sol diff --git a/.gitignore b/.gitignore index 80d03e33c..27fc8f05d 100644 --- a/.gitignore +++ b/.gitignore @@ -50,3 +50,7 @@ slither/ # Misc .vscode/ .idea/ + +# Fuzz tests +crytic-export/ +pkg/vault/medusa-corpus/ diff --git a/pkg/vault/echidna.yml b/pkg/vault/echidna.yml new file mode 100644 index 000000000..6e54bd145 --- /dev/null +++ b/pkg/vault/echidna.yml @@ -0,0 +1,4 @@ +testMode: "assertion" +coverage: false +cryticArgs: ["--foundry-out-directory=forge-artifacts", "--foundry-compile-all"] +allowFFI: true diff --git a/pkg/vault/medusa.json b/pkg/vault/medusa.json new file mode 100644 index 000000000..55fcffb84 --- /dev/null +++ b/pkg/vault/medusa.json @@ -0,0 +1,84 @@ +{ + "fuzzing": { + "workers": 10, + "workerResetLimit": 50, + "timeout": 0, + "testLimit": 0, + "shrinkLimit": 5000, + "callSequenceLength": 100, + "corpusDirectory": "medusa-corpus", + "coverageEnabled": true, + "targetContracts": ["FuzzHarness"], + "predeployedContracts": {}, + "targetContractsBalances": [], + "constructorArgs": {}, + "deployerAddress": "0x30000", + "senderAddresses": [ + "0x10000", + "0x20000", + "0x30000" + ], + "blockNumberDelayMax": 60480, + "blockTimestampDelayMax": 604800, + "blockGasLimit": 1250000000, + "transactionGasLimit": 125000000, + "testing": { + "stopOnFailedTest": false, + "stopOnFailedContractMatching": false, + "stopOnNoTests": true, + "testAllContracts": false, + "traceAll": false, + "assertionTesting": { + "enabled": true, + "testViewMethods": false, + "panicCodeConfig": { + "failOnCompilerInsertedPanic": false, + "failOnAssertion": true, + "failOnArithmeticUnderflow": false, + "failOnDivideByZero": false, + "failOnEnumTypeConversionOutOfBounds": false, + "failOnIncorrectStorageAccess": false, + "failOnPopEmptyArray": false, + "failOnOutOfBoundsArrayAccess": false, + "failOnAllocateTooMuchMemory": false, + "failOnCallUninitializedVariable": false + } + }, + "propertyTesting": { + "enabled": true, + "testPrefixes": [ + "property_" + ] + }, + "optimizationTesting": { + "enabled": true, + "testPrefixes": [ + "optimize_" + ] + }, + "targetFunctionSignatures": [], + "excludeFunctionSignatures": [] + }, + "chainConfig": { + "codeSizeCheckDisabled": true, + "cheatCodes": { + "cheatCodesEnabled": true, + "enableFFI": true + } + } + }, + "compilation": { + "platform": "crytic-compile", + "platformConfig": { + "target": ".", + "solcVersion": "", + "exportDirectory": "", + "args": ["--foundry-out-directory=forge-artifacts", "--foundry-compile-all"] + } + }, + "logging": { + "level": "info", + "logDirectory": "", + "noColor": false + } +} diff --git a/pkg/vault/package.json b/pkg/vault/package.json index d36babae4..a7345ec60 100644 --- a/pkg/vault/package.json +++ b/pkg/vault/package.json @@ -29,6 +29,8 @@ "test:forge": "yarn build && ./forge-test.sh", "test:forgeonly": "forge test -vvv --no-match-test Fork", "test:stress": "FOUNDRY_PROFILE=intense forge test -vvv", + "fuzz:medusa": "medusa fuzz --config medusa.json", + "fuzz:echidna": "echidna . --contract FuzzHarness --config echidna.yml", "coverage": "./coverage.sh forge", "coverage:hardhat": "./coverage.sh hardhat", "coverage:all": "./coverage.sh all", diff --git a/pkg/vault/test/foundry/fuzz/FuzzHarness.sol b/pkg/vault/test/foundry/fuzz/FuzzHarness.sol new file mode 100644 index 000000000..e308d7922 --- /dev/null +++ b/pkg/vault/test/foundry/fuzz/FuzzHarness.sol @@ -0,0 +1,518 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity ^0.8.24; + +import "forge-std/Test.sol"; + +import { FixedPoint } from "@balancer-labs/v3-solidity-utils/contracts/math/FixedPoint.sol"; +import { IBasePool } from "@balancer-labs/v3-interfaces/contracts/vault/IBasePool.sol"; +import { IVaultMock } from "@balancer-labs/v3-interfaces/contracts/test/IVaultMock.sol"; +import { Rounding } from "@balancer-labs/v3-interfaces/contracts/vault/VaultTypes.sol"; +import { StablePool } from "@balancer-labs/v3-pool-stable/contracts/StablePool.sol"; +import { WeightedPool } from "@balancer-labs/v3-pool-weighted/contracts/WeightedPool.sol"; + +import { VaultMockDeployer } from "../utils/VaultMockDeployer.sol"; +import { BasePoolMath } from "../../../contracts/BasePoolMath.sol"; + +contract FuzzHarness is Test { + using FixedPoint for uint256; + + event Debug(string, uint256); + + // If this flag is false, assertion tests will never fail! But optimizations will work. + // Changing to false will disable optimizations but allow the assertion tests to run + bool private constant ASSERT_MODE = false; + + uint256 private constant _MAX_BALANCE = 2 ** (128) - 1; // from PackedTokenBalance.sol + uint256 private constant _MIN_WEIGHT = 1e16; // 1% (from WeightedPool.sol) + uint256 private constant _MINIMUM_TRADE_AMOUNT = 1e6; + uint256 private constant _POOL_MINIMUM_TOTAL_SUPPLY = 1e6; + + IVaultMock private vault; + IBasePool private stablePool; + IBasePool private weightedPool; + + // State management + uint256[] weightedBalanceLive; + uint256[] stableBalanceLive; + uint256 weightedBPTSupply; + uint256 stableBPTSupply; + + // State vars for optimization mode + uint256 rateDecrease = 0; + uint256 bptProfit = 0; + + constructor() { + if (address(vault) == address(0)) { + uint256 vaultMockMinTradeAmount = 0; + uint256 vaultMockMinWrapAmount = 0; + vault = IVaultMock(address(VaultMockDeployer.deploy(vaultMockMinTradeAmount, vaultMockMinWrapAmount))); + } + uint256[] memory initialBalances = new uint256[](3); + initialBalances[0] = 10e18; + initialBalances[1] = 20e18; + initialBalances[2] = 30e18; + createNewWeightedPool(33e16, 33e16, initialBalances); + createNewStablePool(1000, initialBalances); + } + + //////////////////////////////////////// + // Pool Creation + + function createNewWeightedPool(uint256 weight1, uint256 weight2, uint256[] memory initialBalances) private { + uint256[] memory weights = new uint256[](3); + weights[0] = bound(weight1, _MIN_WEIGHT, 98e16); // any weight between min & 100%-2(min) + uint256 remainingWeight = 99e16 - weights[0]; // weights 0 + 1 must <= 100%-min so there's >=1% left for weight 2 + weights[1] = bound(weight2, _MIN_WEIGHT, remainingWeight); + remainingWeight = 100e16 - (weights[0] + weights[1]); + weights[2] = remainingWeight; + WeightedPool.NewPoolParams memory params = WeightedPool.NewPoolParams({ + name: "My Custom Pool", + symbol: "MCP", + numTokens: 3, + normalizedWeights: weights, + version: "1.0.0" + }); + weightedPool = IBasePool(new WeightedPool(params, vault)); + // Initialize liquidity for this new weighted pool + initialBalances = boundBalanceLength(initialBalances, false); + for (uint256 i; i < initialBalances.length; i++) { + if (initialBalances[i] < 1 ether) initialBalances[i] += 1 ether; + weightedBalanceLive.push(initialBalances[0]); + initialBalances[i] = initialBalances[0]; + } + weightedBPTSupply += mockInitialize(weightedPool, initialBalances); + } + + function createNewStablePool(uint256 amplificationParameter, uint256[] memory initialBalances) private { + StablePool.NewPoolParams memory params = StablePool.NewPoolParams({ + name: "My Custom Pool", + symbol: "MCP", + amplificationParameter: bound(amplificationParameter, 1, 5000), + version: "1.0.0" + }); + stablePool = IBasePool(new StablePool(params, vault)); + // Initialize liquidity for this new stable pool + initialBalances = boundBalanceLength(initialBalances, true); + for (uint256 i; i < initialBalances.length; i++) { + if (initialBalances[i] < 1 ether) initialBalances[i] += 1 ether; + stableBalanceLive.push(initialBalances[i]); + } + stableBPTSupply += mockInitialize(stablePool, initialBalances); + } + + //////////////////////////////////////// + // Optimizations + + function optimize_rateDecrease() public view returns (int256) { + return int256(rateDecrease); + } + + function optimize_bptProfit() public view returns (int256) { + return int256(bptProfit); + } + + //////////////////////////////////////// + // Symmetrical Add/Remove Liquidity + + function computeAddAndRemoveLiquiditySingleToken( + uint256 tokenIndex, + uint256 bptMintAmt, + uint256 swapFeePercentage, + bool useStablePool + ) public { + tokenIndex = boundTokenIndex(useStablePool, tokenIndex); + bptMintAmt = boundBptMint(useStablePool, bptMintAmt); + + // deposit tokenAmt to mint exactly bptMintAmt + uint256 tokenAmt = computeAddLiquiditySingleTokenExactOut( + tokenIndex, bptMintAmt, swapFeePercentage, useStablePool + ); + + // withdraw exactly tokenAmt to burn bptBurnAmt + uint256 bptBurnAmt = computeRemoveLiquiditySingleTokenExactIn( + tokenIndex, tokenAmt, swapFeePercentage, useStablePool + ); + + emit Debug("BPT minted while adding liq:", bptMintAmt); + emit Debug("BPT burned while removing the same liq:", bptBurnAmt); + bptProfit = bptMintAmt - bptBurnAmt; + if (ASSERT_MODE) { + assert(bptProfit <= 0); + } + } + + function computeRemoveAndAddLiquiditySingleToken( + uint256 tokenIndex, + uint256 tokenAmt, + uint256 swapFeePercentage, + bool useStablePool + ) public { + tokenIndex = boundTokenIndex(useStablePool, tokenIndex); + tokenAmt = boundTokenWithdraw(useStablePool, tokenAmt, tokenIndex); + + // withdraw exactly tokenAmt to burn bptBurnAmt + uint256 bptBurnAmt = computeRemoveLiquiditySingleTokenExactOut( + tokenIndex, tokenAmt, swapFeePercentage, useStablePool + ); + + // deposit exactly tokenAmt to mint bptMintAmt + uint256[] memory exactAmounts = new uint256[](getBalancesLength(useStablePool)); + exactAmounts[tokenIndex] = tokenAmt; + uint256 bptMintAmt = computeAddLiquidityUnbalanced( + exactAmounts, swapFeePercentage, useStablePool + ); + + emit Debug("BPT burned while removing liq:", bptBurnAmt); + emit Debug("BPT minted while adding the same liq:", bptMintAmt); + bptProfit = bptMintAmt - bptBurnAmt; + if (ASSERT_MODE) { + assert(bptProfit <= 0); + } + } + + function computeAddAndRemoveAddLiquidityMultiToken( + uint256 bptMintAmt, + uint256 swapFeePercentage, + bool useStablePool + ) public { + bptMintAmt = boundBptMint(useStablePool, bptMintAmt); + + // mint exactly bptMintAmt to deposit tokenAmts + uint256[] memory tokenAmts = computeProportionalAmountsIn( + bptMintAmt, useStablePool + ); + + // withdraw exactly tokenAmts to burn bptBurnAmt + // No computeRemoveLiquidityUnbalanced fn available, need to go one at a time to accomplish this + uint256 bptBurnAmt = 0; + for (uint256 i = 0; i < tokenAmts.length; i++) { + bptBurnAmt += computeRemoveLiquiditySingleTokenExactOut( + i, tokenAmts[i], swapFeePercentage, useStablePool + ); + } + + emit Debug("BPT minted while adding liquidity:", bptMintAmt); + emit Debug("BPT burned while removing same liquidity:", bptBurnAmt); + bptProfit = bptMintAmt - bptBurnAmt; + if (ASSERT_MODE) { + assert(bptProfit <= 0); + } + } + + function computeRemoveAndAddLiquidityMultiToken( + uint256 bptBurnAmt, + uint256 swapFeePercentage, + bool useStablePool + ) public { + bptBurnAmt = boundBptBurn(useStablePool, bptBurnAmt); + + // burn exactly bptBurnAmt to withdraw tokenAmts + uint256[] memory tokenAmts = computeProportionalAmountsOut(bptBurnAmt, useStablePool); + + // deposit exactly tokenAmts to mint bptMintAmt + uint256 bptMintAmt = computeAddLiquidityUnbalanced(tokenAmts, swapFeePercentage, useStablePool); + + emit Debug("BPT burned while removing liquidity:", bptBurnAmt); + emit Debug("BPT minted while adding the same liquidity:", bptMintAmt); + bptProfit = bptMintAmt - bptBurnAmt; + if (ASSERT_MODE) { + assert(bptProfit <= 0); + } + } + + //////////////////////////////////////// + // Rate Invariants + + function computeProportionalAmountsIn( + uint256 bptAmountOut, + bool useStablePool + ) public returns(uint256[] memory amountsIn) { + assumeValidTradeAmount(bptAmountOut); + bptAmountOut = boundBptMint(useStablePool, bptAmountOut); + (uint256[] memory balances, uint256 bptTotalSupply) = loadState(useStablePool); + uint256 rateBefore = getBptRate(getPool(useStablePool), balances, bptTotalSupply); + + amountsIn = BasePoolMath.computeProportionalAmountsIn(balances, bptTotalSupply, bptAmountOut); + + uint256[] memory balancesAfter = sumBalances(balances, amountsIn); + uint256 rateAfter = getBptRate( + getPool(useStablePool), + balancesAfter, + bptTotalSupply + bptAmountOut + ); + updateState(useStablePool, balancesAfter, 0, bptAmountOut); + rateDecrease = rateBefore - rateAfter; + if (ASSERT_MODE) { + assert(rateDecrease <= 0); + } + } + + function computeProportionalAmountsOut( + uint256 bptAmountIn, + bool useStablePool + ) public returns(uint256[] memory amountsOut) { + assumeValidTradeAmount(bptAmountIn); + bptAmountIn = boundBptBurn(useStablePool, bptAmountIn); + (uint256[] memory balances, uint256 bptTotalSupply) = loadState(useStablePool); + uint256 rateBefore = getBptRate(getPool(useStablePool), balances, bptTotalSupply); + + amountsOut = BasePoolMath.computeProportionalAmountsOut(balances, bptTotalSupply, bptAmountIn); + + uint256[] memory balancesAfter = subBalances(balances, amountsOut); + uint256 rateAfter = getBptRate( + getPool(useStablePool), + balancesAfter, + bptTotalSupply - bptAmountIn + ); + updateState(useStablePool, balancesAfter, bptAmountIn, 0); + rateDecrease = rateBefore - rateAfter; + if (ASSERT_MODE) { + assert(rateDecrease <= 0); + } + } + + function computeAddLiquidityUnbalanced( + uint256[] memory exactAmounts, + uint256 swapFeePercentage, + bool useStablePool + ) public returns(uint256 bptAmountOut) { + exactAmounts = boundBalanceLength(exactAmounts, useStablePool); + (uint256[] memory balances, uint256 bptTotalSupply) = loadState(useStablePool); + for (uint256 i = 0; i < exactAmounts.length; i++) { + exactAmounts[i] = boundTokenDeposit(useStablePool, exactAmounts[i], i); + } + IBasePool pool = getPool(useStablePool); + uint256 rateBefore = getBptRate(pool, balances, bptTotalSupply); + + (uint256 amountOut,) = BasePoolMath.computeAddLiquidityUnbalanced( + balances, + exactAmounts, + bptTotalSupply, + swapFeePercentage, + pool + ); + + bptAmountOut = amountOut; + uint256[] memory balancesAfter = sumBalances(balances, exactAmounts); + uint256 rateAfter = getBptRate(pool, balancesAfter, bptTotalSupply + amountOut); + updateState(useStablePool, balancesAfter, 0, amountOut); + rateDecrease = rateBefore - rateAfter; + if (ASSERT_MODE) { + assert(rateDecrease <= 0); + } + } + + function computeAddLiquiditySingleTokenExactOut( + uint256 tokenInIndex, + uint256 exactBptAmountOut, + uint256 swapFeePercentage, + bool useStablePool + ) public returns (uint256 tokenAmountIn) { + assumeValidTradeAmount(exactBptAmountOut); + tokenInIndex = boundTokenIndex(useStablePool, tokenInIndex); + exactBptAmountOut = boundBptMint(useStablePool, exactBptAmountOut); + (uint256[] memory balances, uint256 bptTotalSupply) = loadState(useStablePool); + boundBalanceLength(balances, useStablePool); + uint256 rateBefore = getBptRate(getPool(useStablePool), balances, bptTotalSupply); + + (uint256 amountInMinusFee, uint256[] memory fees) = BasePoolMath.computeAddLiquiditySingleTokenExactOut( + balances, + tokenInIndex, + exactBptAmountOut, + bptTotalSupply, + swapFeePercentage, + getPool(useStablePool) + ); + + tokenAmountIn = amountInMinusFee; + balances[tokenInIndex] += (amountInMinusFee + fees[tokenInIndex]); + uint256 rateAfter = getBptRate(getPool(useStablePool), balances, bptTotalSupply + exactBptAmountOut); + updateState(useStablePool, balances, 0, exactBptAmountOut); + rateDecrease = rateBefore - rateAfter; + if (ASSERT_MODE) { + assert(rateDecrease <= 0); + } + } + + function computeRemoveLiquiditySingleTokenExactOut( + uint256 tokenOutIndex, + uint256 exactAmountOut, + uint256 swapFeePercentage, + bool useStablePool + ) public returns (uint256 bptAmountIn) { + assumeValidTradeAmount(exactAmountOut); + tokenOutIndex = boundTokenIndex(useStablePool, tokenOutIndex); + exactAmountOut = boundTokenWithdraw(useStablePool, exactAmountOut, tokenOutIndex); + (uint256[] memory balances, uint256 bptTotalSupply) = loadState(useStablePool); + uint256 rateBefore = getBptRate(getPool(useStablePool), balances, bptTotalSupply); + + (uint256 _bptAmountIn, uint256[] memory fees) = BasePoolMath.computeRemoveLiquiditySingleTokenExactOut( + balances, + tokenOutIndex, + exactAmountOut, + bptTotalSupply, + swapFeePercentage, + getPool(useStablePool) + ); + bptAmountIn = _bptAmountIn; + + balances[tokenOutIndex] -= (exactAmountOut + fees[tokenOutIndex]); + uint256 rateAfter = getBptRate(getPool(useStablePool), balances, bptTotalSupply - bptAmountIn); + updateState(useStablePool, balances, bptAmountIn, 0); + rateDecrease = rateBefore - rateAfter; + if (ASSERT_MODE) { + assert(rateDecrease <= 0); + } + } + + function computeRemoveLiquiditySingleTokenExactIn( + uint256 tokenOutIndex, + uint256 exactBptAmountIn, + uint256 swapFeePercentage, + bool useStablePool + ) public returns (uint256 bptAmountOut) { + assumeValidTradeAmount(exactBptAmountIn); + tokenOutIndex = boundTokenIndex(useStablePool, tokenOutIndex); + exactBptAmountIn = boundBptBurn(useStablePool, exactBptAmountIn); + (uint256[] memory balances, uint256 bptTotalSupply) = loadState(useStablePool); + uint256 rateBefore = getBptRate(getPool(useStablePool), balances, bptTotalSupply); + + (uint256 amountOutMinusFee,) = BasePoolMath.computeRemoveLiquiditySingleTokenExactIn( + balances, + tokenOutIndex, + exactBptAmountIn, + bptTotalSupply, + swapFeePercentage, + getPool(useStablePool) + ); + + bptAmountOut = amountOutMinusFee; + balances[tokenOutIndex] -= amountOutMinusFee; // fees already accounted for + uint256 rateAfter = getBptRate(getPool(useStablePool), balances, bptTotalSupply - exactBptAmountIn); + updateState(useStablePool, balances, exactBptAmountIn, 0); + rateDecrease = rateBefore - rateAfter; + if (ASSERT_MODE) { + assert(rateDecrease <= 0); + } + } + + //////////////////////////////////////// + // Helpers + + function mockInitialize(IBasePool pool, uint256[] memory balances) private view returns (uint256) { + uint256 invariant = pool.computeInvariant(balances, Rounding.ROUND_DOWN); + if (invariant < 1e6) revert(); + return invariant; + } + + function loadState(bool useStablePool) private returns (uint256[] memory balances, uint256 bptTotalSupply) { + balances = useStablePool ? stableBalanceLive : weightedBalanceLive; + bptTotalSupply = useStablePool ? stableBPTSupply : weightedBPTSupply; + } + + function updateState( + bool useStablePool, + uint256[] memory balances, + uint256 bptAmountIn, + uint256 bptAmountOut + ) private { + if (useStablePool) { + stableBalanceLive = balances; + stableBPTSupply -= bptAmountIn; + stableBPTSupply += bptAmountOut; + require(stableBPTSupply >= _POOL_MINIMUM_TOTAL_SUPPLY); + } else { + weightedBalanceLive = balances; + weightedBPTSupply -= bptAmountIn; + weightedBPTSupply += bptAmountOut; + require(weightedBPTSupply >= _POOL_MINIMUM_TOTAL_SUPPLY); + } + } + + function getBptRate( + IBasePool pool, + uint256[] memory balances, + uint256 bptTotalSupply + ) private returns (uint256) { + uint256 invariant = pool.computeInvariant(balances, Rounding.ROUND_DOWN); + return invariant.divDown(bptTotalSupply); + } + + function getPool(bool useStablePool) private view returns (IBasePool) { + return useStablePool ? stablePool : weightedPool; + } + + function getBalancesLength(bool useStablePool) private view returns(uint256 length) { + length = useStablePool ? stableBalanceLive.length : weightedBalanceLive.length; + } + + function boundTokenIndex(bool useStablePool, uint256 tokenIndex) private view returns(uint256 boundedIndex) { + uint256 len = getBalancesLength(useStablePool); + boundedIndex = bound(tokenIndex, 0, len - 1); + } + + function boundTokenDeposit(bool useStablePool, uint256 tokenAmt, uint256 tokenIndex) private view returns(uint256 boundedAmt) { + uint256[] memory balances = useStablePool ? stableBalanceLive : weightedBalanceLive; + boundedAmt = bound(tokenAmt, 0, _MAX_BALANCE - balances[tokenIndex]); + } + + function boundTokenWithdraw(bool useStablePool, uint256 tokenAmt, uint256 tokenIndex) private view returns(uint256 boundedAmt) { + uint256[] memory balances = useStablePool ? stableBalanceLive : weightedBalanceLive; + boundedAmt = bound(tokenAmt, 0, balances[tokenIndex]); + } + + function boundBptMint(bool useStablePool, uint256 bptAmt) private view returns(uint256 boundedAmt) { + uint256 totalSupply = useStablePool ? stableBPTSupply : weightedBPTSupply; + boundedAmt = bound(bptAmt, 0, _MAX_BALANCE - totalSupply); + } + + function boundBptBurn(bool useStablePool, uint256 bptAmt) private view returns(uint256 boundedAmt) { + uint256 totalSupply = useStablePool ? stableBPTSupply : weightedBPTSupply; + boundedAmt = bound(bptAmt, 0, totalSupply); + } + + function boundBalanceLength(uint256[] memory balances, bool isStablePool) private pure returns (uint256[] memory) { + if (!isStablePool) { + if (balances.length < 3) revert(); + assembly { + mstore(balances, 3) + } + return balances; + } else { + if (balances.length < 2) revert(); + uint256 numTokens = bound(balances.length, 2, 8); + assembly { + mstore(balances, numTokens) + } + return balances; + } + } + + function sumBalances(uint256[] memory balances, uint256[] memory amounts) private pure returns (uint256[] memory) { + require(amounts.length == balances.length); + uint256[] memory newBalances = new uint256[](balances.length); + for (uint256 i = 0; i < balances.length; i++) { + newBalances[i] = balances[i] + amounts[i]; + } + return newBalances; + } + + function subBalances(uint256[] memory balances, uint256[] memory amounts) private pure returns (uint256[] memory) { + require(amounts.length == balances.length); + uint256[] memory newBalances = new uint256[](balances.length); + for (uint256 i = 0; i < balances.length; i++) { + newBalances[i] = balances[i] - amounts[i]; + } + return newBalances; + } + + function assumeValidBalanceLength(uint256[] memory balances) private pure { + if (balances.length < 2 || balances.length > 8) revert(); + } + + function assumeValidTradeAmount(uint256 tradeAmount) private pure { + if (tradeAmount != 0 && tradeAmount < _MINIMUM_TRADE_AMOUNT) { + revert(); + } + } +} diff --git a/pkg/vault/test/foundry/utils/VaultMockDeployer.sol b/pkg/vault/test/foundry/utils/VaultMockDeployer.sol new file mode 100644 index 000000000..2df91ae06 --- /dev/null +++ b/pkg/vault/test/foundry/utils/VaultMockDeployer.sol @@ -0,0 +1,42 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +pragma solidity ^0.8.24; + +import { IAuthorizer } from "@balancer-labs/v3-interfaces/contracts/vault/IAuthorizer.sol"; +import { CREATE3 } from "@balancer-labs/v3-solidity-utils/contracts/solmate/CREATE3.sol"; +import { IVault } from "@balancer-labs/v3-interfaces/contracts/vault/IVault.sol"; + +import { ProtocolFeeControllerMock } from "../../../contracts/test/ProtocolFeeControllerMock.sol"; +import { BasicAuthorizerMock } from "../../../contracts/test/BasicAuthorizerMock.sol"; +import { ProtocolFeeController } from "../../../contracts/ProtocolFeeController.sol"; +import { VaultExtensionMock } from "../../../contracts/test/VaultExtensionMock.sol"; +import { VaultAdminMock } from "../../../contracts/test/VaultAdminMock.sol"; +import { VaultMock } from "../../../contracts/test/VaultMock.sol"; + +library VaultMockDeployer { + function deploy() internal returns (VaultMock vault) { + return deploy(0, 0); + } + + function deploy(uint256 minTradeAmount, uint256 minWrapAmount) internal returns (VaultMock vault) { + IAuthorizer authorizer = new BasicAuthorizerMock(); + bytes32 salt = bytes32(0); + vault = VaultMock(payable(CREATE3.getDeployed(salt))); + VaultAdminMock vaultAdmin = new VaultAdminMock( + IVault(address(vault)), + 90 days, + 30 days, + minTradeAmount, + minWrapAmount + ); + VaultExtensionMock vaultExtension = new VaultExtensionMock(IVault(address(vault)), vaultAdmin); + ProtocolFeeController protocolFeeController = new ProtocolFeeControllerMock(IVault(address(vault))); + + _create(abi.encode(vaultExtension, authorizer, protocolFeeController), salt); + return vault; + } + + function _create(bytes memory constructorArgs, bytes32 salt) internal returns (address) { + return CREATE3.deploy(salt, abi.encodePacked(type(VaultMock).creationCode, constructorArgs), 0); + } +} From 91003e00ff1b10442bdcc8109bce174d6edb7a2b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Bruno?= Date: Thu, 3 Oct 2024 18:02:19 -0300 Subject: [PATCH 02/29] Medusa Base Test --- .../contracts/test/IStdMedusaCheats.sol | 89 +++ .../contracts/test/WETHTestToken.sol | 4 + pkg/vault/medusa.json | 4 +- pkg/vault/test/foundry/fuzz/FuzzAddRemove.sol | 547 ++++++++++++++++++ .../test/foundry/utils/BaseMedusaTest.sol | 124 ++++ 5 files changed, 766 insertions(+), 2 deletions(-) create mode 100644 pkg/interfaces/contracts/test/IStdMedusaCheats.sol create mode 100644 pkg/vault/test/foundry/fuzz/FuzzAddRemove.sol create mode 100644 pkg/vault/test/foundry/utils/BaseMedusaTest.sol diff --git a/pkg/interfaces/contracts/test/IStdMedusaCheats.sol b/pkg/interfaces/contracts/test/IStdMedusaCheats.sol new file mode 100644 index 000000000..6ecc05520 --- /dev/null +++ b/pkg/interfaces/contracts/test/IStdMedusaCheats.sol @@ -0,0 +1,89 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +pragma solidity ^0.8.24; + +interface IStdMedusaCheats { + // Set block.timestamp + function warp(uint256) external; + + // Set block.number + function roll(uint256) external; + + // Set block.basefee + function fee(uint256) external; + + // Set block.difficulty and block.prevrandao + function difficulty(uint256) external; + + // Set block.chainid + function chainId(uint256) external; + + // Sets the block.coinbase + function coinbase(address) external; + + // Loads a storage slot from an address + function load(address account, bytes32 slot) external returns (bytes32); + + // Stores a value to an address' storage slot + function store(address account, bytes32 slot, bytes32 value) external; + + // Sets the *next* call's msg.sender to be the input address + function prank(address) external; + + // Set msg.sender to the input address until the current call exits + function prankHere(address) external; + + // Sets an address' balance + function deal(address who, uint256 newBalance) external; + + // Sets an address' code + function etch(address who, bytes calldata code) external; + + // Signs data + function sign(uint256 privateKey, bytes32 digest) external returns (uint8 v, bytes32 r, bytes32 s); + + // Computes address for a given private key + function addr(uint256 privateKey) external returns (address); + + // Gets the nonce of an account + function getNonce(address account) external returns (uint64); + + // Sets the nonce of an account + // The new nonce must be higher than the current nonce of the account + function setNonce(address account, uint64 nonce) external; + + // Performs a foreign function call via terminal + function ffi(string[] calldata) external returns (bytes memory); + + // Take a snapshot of the current state of the EVM + function snapshot() external returns (uint256); + + // Revert state back to a snapshot + function revertTo(uint256) external returns (bool); + + // Convert Solidity types to strings + function toString(address) external returns (string memory); + + function toString(bytes calldata) external returns (string memory); + + function toString(bytes32) external returns (string memory); + + function toString(bool) external returns (string memory); + + function toString(uint256) external returns (string memory); + + function toString(int256) external returns (string memory); + + // Convert strings into Solidity types + function parseBytes(string memory) external returns (bytes memory); + + function parseBytes32(string memory) external returns (bytes32); + + function parseAddress(string memory) external returns (address); + + function parseUint(string memory) external returns (uint256); + + function parseInt(string memory) external returns (int256); + + function parseBool(string memory) external returns (bool); +} diff --git a/pkg/solidity-utils/contracts/test/WETHTestToken.sol b/pkg/solidity-utils/contracts/test/WETHTestToken.sol index 631b3dc37..f92bdc79e 100644 --- a/pkg/solidity-utils/contracts/test/WETHTestToken.sol +++ b/pkg/solidity-utils/contracts/test/WETHTestToken.sol @@ -27,4 +27,8 @@ contract WETHTestToken is IWETH, ERC20 { payable(msg.sender).transfer(wad); emit Withdrawal(msg.sender, wad); } + + function mint(address to, uint256 value) public { + _mint(to, value); + } } diff --git a/pkg/vault/medusa.json b/pkg/vault/medusa.json index 55fcffb84..0ff7d1e62 100644 --- a/pkg/vault/medusa.json +++ b/pkg/vault/medusa.json @@ -3,12 +3,12 @@ "workers": 10, "workerResetLimit": 50, "timeout": 0, - "testLimit": 0, + "testLimit": 100000, "shrinkLimit": 5000, "callSequenceLength": 100, "corpusDirectory": "medusa-corpus", "coverageEnabled": true, - "targetContracts": ["FuzzHarness"], + "targetContracts": ["FuzzAddRemove"], "predeployedContracts": {}, "targetContractsBalances": [], "constructorArgs": {}, diff --git a/pkg/vault/test/foundry/fuzz/FuzzAddRemove.sol b/pkg/vault/test/foundry/fuzz/FuzzAddRemove.sol new file mode 100644 index 000000000..ebe7a07bd --- /dev/null +++ b/pkg/vault/test/foundry/fuzz/FuzzAddRemove.sol @@ -0,0 +1,547 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity ^0.8.24; + +import "forge-std/Test.sol"; + +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + +import { IBasePool } from "@balancer-labs/v3-interfaces/contracts/vault/IBasePool.sol"; +import { Rounding } from "@balancer-labs/v3-interfaces/contracts/vault/VaultTypes.sol"; + +import { FixedPoint } from "@balancer-labs/v3-solidity-utils/contracts/math/FixedPoint.sol"; +import { InputHelpers } from "@balancer-labs/v3-solidity-utils/contracts/helpers/InputHelpers.sol"; + +import { StablePool } from "@balancer-labs/v3-pool-stable/contracts/StablePool.sol"; + +import { WeightedPool } from "@balancer-labs/v3-pool-weighted/contracts/WeightedPool.sol"; +import { WeightedPoolFactory } from "@balancer-labs/v3-pool-weighted/contracts/WeightedPoolFactory.sol"; + +import { BasePoolMath } from "../../../contracts/BasePoolMath.sol"; + +import "../utils/BaseMedusaTest.sol"; + +contract FuzzHarness is Test, BaseMedusaTest { + using FixedPoint for uint256; + + event Debug(string, uint256); + + // If this flag is false, assertion tests will never fail! But optimizations will work. + // Changing to false will disable optimizations but allow the assertion tests to run + bool private constant ASSERT_MODE = false; + + uint256 internal constant DEFAULT_SWAP_FEE = 1e16; // 1% + + uint256 private constant _MAX_BALANCE = 2 ** (128) - 1; // from PackedTokenBalance.sol + uint256 private constant _MIN_WEIGHT = 1e16; // 1% (from WeightedPool.sol) + uint256 private constant _MINIMUM_TRADE_AMOUNT = 1e6; + uint256 private constant _POOL_MINIMUM_TOTAL_SUPPLY = 1e6; + + uint256 internal poolCreationNonce; + + IBasePool private stablePool; + IBasePool private weightedPool; + + // State management + uint256[] weightedBalanceLive; + uint256[] stableBalanceLive; + uint256 weightedBPTSupply; + uint256 stableBPTSupply; + + // State vars for optimization mode + uint256 rateDecrease = 0; + uint256 bptProfit = 0; + + constructor() BaseMedusaTest() { + uint256[] memory initialBalances = new uint256[](3); + initialBalances[0] = 10e18; + initialBalances[1] = 20e18; + initialBalances[2] = 30e18; + createNewWeightedPool(33e16, 33e16, initialBalances); + createNewStablePool(1000, initialBalances); + } + + //////////////////////////////////////// + // Pool Creation + + function createNewWeightedPool(uint256 weight1, uint256 weight2, uint256[] memory initialBalances) private { + uint256[] memory weights = new uint256[](3); + weights[0] = bound(weight1, _MIN_WEIGHT, 98e16); // any weight between min & 100%-2(min) + uint256 remainingWeight = 99e16 - weights[0]; // weights 0 + 1 must <= 100%-min so there's >=1% left for weight 2 + weights[1] = bound(weight2, _MIN_WEIGHT, remainingWeight); + remainingWeight = 100e16 - (weights[0] + weights[1]); + weights[2] = remainingWeight; + + IERC20[] memory tokens = new IERC20[](3); + tokens[0] = dai; + tokens[1] = usdc; + tokens[2] = weth; + tokens = InputHelpers.sortTokens(tokens); + + WeightedPoolFactory factory = new WeightedPoolFactory( + IVault(address(vault)), + 365 days, + "Factory v1", + "Pool v1" + ); + PoolRoleAccounts memory roleAccounts; + + WeightedPool newPool = WeightedPool( + factory.create( + "Weighted Pool", + "WP", + vault.buildTokenConfig(tokens), + weights, + roleAccounts, + DEFAULT_SWAP_FEE, // 1% swap fee + address(0), // No hooks + false, // Do not enable donations + false, // Do not disable unbalanced add/remove liquidity + // NOTE: sends a unique salt. + bytes32(poolCreationNonce++) + ) + ); + + // Cannot set the pool creator directly on a standard Balancer weighted pool factory. + vault.manualSetPoolCreator(address(newPool), lp); + + feeController.manualSetPoolCreator(address(newPool), lp); + + // Initialize liquidity for this new weighted pool + medusa.prank(lp); + router.initialize(address(newPool), tokens, initialBalances, 0, false, bytes("")); + + weightedPool = IBasePool(address(newPool)); + } + + function createNewStablePool(uint256 amplificationParameter, uint256[] memory initialBalances) private { + StablePool.NewPoolParams memory params = StablePool.NewPoolParams({ + name: "My Custom Pool", + symbol: "MCP", + amplificationParameter: bound(amplificationParameter, 1, 5000), + version: "1.0.0" + }); + stablePool = IBasePool(new StablePool(params, vault)); + // Initialize liquidity for this new stable pool + initialBalances = boundBalanceLength(initialBalances, true); + for (uint256 i; i < initialBalances.length; i++) { + if (initialBalances[i] < 1 ether) initialBalances[i] += 1 ether; + stableBalanceLive.push(initialBalances[i]); + } + stableBPTSupply += mockInitialize(stablePool, initialBalances); + } + + //////////////////////////////////////// + // Optimizations + + function optimize_rateDecrease() public view returns (int256) { + return int256(rateDecrease); + } + + function optimize_bptProfit() public view returns (int256) { + return int256(bptProfit); + } + + //////////////////////////////////////// + // Symmetrical Add/Remove Liquidity + + function computeAddAndRemoveLiquiditySingleToken( + uint256 tokenIndex, + uint256 bptMintAmt, + uint256 swapFeePercentage, + bool useStablePool + ) public { + tokenIndex = boundTokenIndex(useStablePool, tokenIndex); + bptMintAmt = boundBptMint(useStablePool, bptMintAmt); + + // deposit tokenAmt to mint exactly bptMintAmt + uint256 tokenAmt = computeAddLiquiditySingleTokenExactOut( + tokenIndex, + bptMintAmt, + swapFeePercentage, + useStablePool + ); + + // withdraw exactly tokenAmt to burn bptBurnAmt + uint256 bptBurnAmt = computeRemoveLiquiditySingleTokenExactIn( + tokenIndex, + tokenAmt, + swapFeePercentage, + useStablePool + ); + + emit Debug("BPT minted while adding liq:", bptMintAmt); + emit Debug("BPT burned while removing the same liq:", bptBurnAmt); + bptProfit = bptMintAmt - bptBurnAmt; + if (ASSERT_MODE) { + assert(bptProfit <= 0); + } + } + + function computeRemoveAndAddLiquiditySingleToken( + uint256 tokenIndex, + uint256 tokenAmt, + uint256 swapFeePercentage, + bool useStablePool + ) public { + tokenIndex = boundTokenIndex(useStablePool, tokenIndex); + tokenAmt = boundTokenWithdraw(useStablePool, tokenAmt, tokenIndex); + + // withdraw exactly tokenAmt to burn bptBurnAmt + uint256 bptBurnAmt = computeRemoveLiquiditySingleTokenExactOut( + tokenIndex, + tokenAmt, + swapFeePercentage, + useStablePool + ); + + // deposit exactly tokenAmt to mint bptMintAmt + uint256[] memory exactAmounts = new uint256[](getBalancesLength(useStablePool)); + exactAmounts[tokenIndex] = tokenAmt; + uint256 bptMintAmt = computeAddLiquidityUnbalanced(exactAmounts, swapFeePercentage, useStablePool); + + emit Debug("BPT burned while removing liq:", bptBurnAmt); + emit Debug("BPT minted while adding the same liq:", bptMintAmt); + bptProfit = bptMintAmt - bptBurnAmt; + if (ASSERT_MODE) { + assert(bptProfit <= 0); + } + } + + function computeAddAndRemoveAddLiquidityMultiToken( + uint256 bptMintAmt, + uint256 swapFeePercentage, + bool useStablePool + ) public { + bptMintAmt = boundBptMint(useStablePool, bptMintAmt); + + // mint exactly bptMintAmt to deposit tokenAmts + uint256[] memory tokenAmts = computeProportionalAmountsIn(bptMintAmt, useStablePool); + + // withdraw exactly tokenAmts to burn bptBurnAmt + // No computeRemoveLiquidityUnbalanced fn available, need to go one at a time to accomplish this + uint256 bptBurnAmt = 0; + for (uint256 i = 0; i < tokenAmts.length; i++) { + bptBurnAmt += computeRemoveLiquiditySingleTokenExactOut(i, tokenAmts[i], swapFeePercentage, useStablePool); + } + + emit Debug("BPT minted while adding liquidity:", bptMintAmt); + emit Debug("BPT burned while removing same liquidity:", bptBurnAmt); + bptProfit = bptMintAmt - bptBurnAmt; + if (ASSERT_MODE) { + assert(bptProfit <= 0); + } + } + + function computeRemoveAndAddLiquidityMultiToken( + uint256 bptBurnAmt, + uint256 swapFeePercentage, + bool useStablePool + ) public { + bptBurnAmt = boundBptBurn(useStablePool, bptBurnAmt); + + // burn exactly bptBurnAmt to withdraw tokenAmts + uint256[] memory tokenAmts = computeProportionalAmountsOut(bptBurnAmt, useStablePool); + + // deposit exactly tokenAmts to mint bptMintAmt + uint256 bptMintAmt = computeAddLiquidityUnbalanced(tokenAmts, swapFeePercentage, useStablePool); + + emit Debug("BPT burned while removing liquidity:", bptBurnAmt); + emit Debug("BPT minted while adding the same liquidity:", bptMintAmt); + bptProfit = bptMintAmt - bptBurnAmt; + if (ASSERT_MODE) { + assert(bptProfit <= 0); + } + } + + //////////////////////////////////////// + // Rate Invariants + + function computeProportionalAmountsIn( + uint256 bptAmountOut, + bool useStablePool + ) public returns (uint256[] memory amountsIn) { + assumeValidTradeAmount(bptAmountOut); + bptAmountOut = boundBptMint(useStablePool, bptAmountOut); + (uint256[] memory balances, uint256 bptTotalSupply) = loadState(useStablePool); + uint256 rateBefore = getBptRate(getPool(useStablePool), balances, bptTotalSupply); + + amountsIn = BasePoolMath.computeProportionalAmountsIn(balances, bptTotalSupply, bptAmountOut); + + uint256[] memory balancesAfter = sumBalances(balances, amountsIn); + uint256 rateAfter = getBptRate(getPool(useStablePool), balancesAfter, bptTotalSupply + bptAmountOut); + updateState(useStablePool, balancesAfter, 0, bptAmountOut); + rateDecrease = rateBefore - rateAfter; + if (ASSERT_MODE) { + assert(rateDecrease <= 0); + } + } + + function computeProportionalAmountsOut( + uint256 bptAmountIn, + bool useStablePool + ) public returns (uint256[] memory amountsOut) { + assumeValidTradeAmount(bptAmountIn); + bptAmountIn = boundBptBurn(useStablePool, bptAmountIn); + (uint256[] memory balances, uint256 bptTotalSupply) = loadState(useStablePool); + uint256 rateBefore = getBptRate(getPool(useStablePool), balances, bptTotalSupply); + + amountsOut = BasePoolMath.computeProportionalAmountsOut(balances, bptTotalSupply, bptAmountIn); + + uint256[] memory balancesAfter = subBalances(balances, amountsOut); + uint256 rateAfter = getBptRate(getPool(useStablePool), balancesAfter, bptTotalSupply - bptAmountIn); + updateState(useStablePool, balancesAfter, bptAmountIn, 0); + rateDecrease = rateBefore - rateAfter; + if (ASSERT_MODE) { + assert(rateDecrease <= 0); + } + } + + function computeAddLiquidityUnbalanced( + uint256[] memory exactAmounts, + uint256 swapFeePercentage, + bool useStablePool + ) public returns (uint256 bptAmountOut) { + exactAmounts = boundBalanceLength(exactAmounts, useStablePool); + (uint256[] memory balances, uint256 bptTotalSupply) = loadState(useStablePool); + for (uint256 i = 0; i < exactAmounts.length; i++) { + exactAmounts[i] = boundTokenDeposit(useStablePool, exactAmounts[i], i); + } + IBasePool pool = getPool(useStablePool); + uint256 rateBefore = getBptRate(pool, balances, bptTotalSupply); + + (uint256 amountOut, ) = BasePoolMath.computeAddLiquidityUnbalanced( + balances, + exactAmounts, + bptTotalSupply, + swapFeePercentage, + pool + ); + + bptAmountOut = amountOut; + uint256[] memory balancesAfter = sumBalances(balances, exactAmounts); + uint256 rateAfter = getBptRate(pool, balancesAfter, bptTotalSupply + amountOut); + updateState(useStablePool, balancesAfter, 0, amountOut); + rateDecrease = rateBefore - rateAfter; + if (ASSERT_MODE) { + assert(rateDecrease <= 0); + } + } + + function computeAddLiquiditySingleTokenExactOut( + uint256 tokenInIndex, + uint256 exactBptAmountOut, + uint256 swapFeePercentage, + bool useStablePool + ) public returns (uint256 tokenAmountIn) { + assumeValidTradeAmount(exactBptAmountOut); + tokenInIndex = boundTokenIndex(useStablePool, tokenInIndex); + exactBptAmountOut = boundBptMint(useStablePool, exactBptAmountOut); + (uint256[] memory balances, uint256 bptTotalSupply) = loadState(useStablePool); + boundBalanceLength(balances, useStablePool); + uint256 rateBefore = getBptRate(getPool(useStablePool), balances, bptTotalSupply); + + (uint256 amountInMinusFee, uint256[] memory fees) = BasePoolMath.computeAddLiquiditySingleTokenExactOut( + balances, + tokenInIndex, + exactBptAmountOut, + bptTotalSupply, + swapFeePercentage, + getPool(useStablePool) + ); + + tokenAmountIn = amountInMinusFee; + balances[tokenInIndex] += (amountInMinusFee + fees[tokenInIndex]); + uint256 rateAfter = getBptRate(getPool(useStablePool), balances, bptTotalSupply + exactBptAmountOut); + updateState(useStablePool, balances, 0, exactBptAmountOut); + rateDecrease = rateBefore - rateAfter; + if (ASSERT_MODE) { + assert(rateDecrease <= 0); + } + } + + function computeRemoveLiquiditySingleTokenExactOut( + uint256 tokenOutIndex, + uint256 exactAmountOut, + uint256 swapFeePercentage, + bool useStablePool + ) public returns (uint256 bptAmountIn) { + assumeValidTradeAmount(exactAmountOut); + tokenOutIndex = boundTokenIndex(useStablePool, tokenOutIndex); + exactAmountOut = boundTokenWithdraw(useStablePool, exactAmountOut, tokenOutIndex); + (uint256[] memory balances, uint256 bptTotalSupply) = loadState(useStablePool); + uint256 rateBefore = getBptRate(getPool(useStablePool), balances, bptTotalSupply); + + (uint256 _bptAmountIn, uint256[] memory fees) = BasePoolMath.computeRemoveLiquiditySingleTokenExactOut( + balances, + tokenOutIndex, + exactAmountOut, + bptTotalSupply, + swapFeePercentage, + getPool(useStablePool) + ); + bptAmountIn = _bptAmountIn; + + balances[tokenOutIndex] -= (exactAmountOut + fees[tokenOutIndex]); + uint256 rateAfter = getBptRate(getPool(useStablePool), balances, bptTotalSupply - bptAmountIn); + updateState(useStablePool, balances, bptAmountIn, 0); + rateDecrease = rateBefore - rateAfter; + if (ASSERT_MODE) { + assert(rateDecrease <= 0); + } + } + + function computeRemoveLiquiditySingleTokenExactIn( + uint256 tokenOutIndex, + uint256 exactBptAmountIn, + uint256 swapFeePercentage, + bool useStablePool + ) public returns (uint256 bptAmountOut) { + assumeValidTradeAmount(exactBptAmountIn); + tokenOutIndex = boundTokenIndex(useStablePool, tokenOutIndex); + exactBptAmountIn = boundBptBurn(useStablePool, exactBptAmountIn); + (uint256[] memory balances, uint256 bptTotalSupply) = loadState(useStablePool); + uint256 rateBefore = getBptRate(getPool(useStablePool), balances, bptTotalSupply); + + (uint256 amountOutMinusFee, ) = BasePoolMath.computeRemoveLiquiditySingleTokenExactIn( + balances, + tokenOutIndex, + exactBptAmountIn, + bptTotalSupply, + swapFeePercentage, + getPool(useStablePool) + ); + + bptAmountOut = amountOutMinusFee; + balances[tokenOutIndex] -= amountOutMinusFee; // fees already accounted for + uint256 rateAfter = getBptRate(getPool(useStablePool), balances, bptTotalSupply - exactBptAmountIn); + updateState(useStablePool, balances, exactBptAmountIn, 0); + rateDecrease = rateBefore - rateAfter; + if (ASSERT_MODE) { + assert(rateDecrease <= 0); + } + } + + //////////////////////////////////////// + // Helpers + + function mockInitialize(IBasePool pool, uint256[] memory balances) private view returns (uint256) { + uint256 invariant = pool.computeInvariant(balances, Rounding.ROUND_DOWN); + if (invariant < 1e6) revert(); + return invariant; + } + + function loadState(bool useStablePool) private returns (uint256[] memory balances, uint256 bptTotalSupply) { + balances = useStablePool ? stableBalanceLive : weightedBalanceLive; + bptTotalSupply = useStablePool ? stableBPTSupply : weightedBPTSupply; + } + + function updateState( + bool useStablePool, + uint256[] memory balances, + uint256 bptAmountIn, + uint256 bptAmountOut + ) private { + if (useStablePool) { + stableBalanceLive = balances; + stableBPTSupply -= bptAmountIn; + stableBPTSupply += bptAmountOut; + require(stableBPTSupply >= _POOL_MINIMUM_TOTAL_SUPPLY); + } else { + weightedBalanceLive = balances; + weightedBPTSupply -= bptAmountIn; + weightedBPTSupply += bptAmountOut; + require(weightedBPTSupply >= _POOL_MINIMUM_TOTAL_SUPPLY); + } + } + + function getBptRate(IBasePool pool, uint256[] memory balances, uint256 bptTotalSupply) private returns (uint256) { + uint256 invariant = pool.computeInvariant(balances, Rounding.ROUND_DOWN); + return invariant.divDown(bptTotalSupply); + } + + function getPool(bool useStablePool) private view returns (IBasePool) { + return useStablePool ? stablePool : weightedPool; + } + + function getBalancesLength(bool useStablePool) private view returns (uint256 length) { + length = useStablePool ? stableBalanceLive.length : weightedBalanceLive.length; + } + + function boundTokenIndex(bool useStablePool, uint256 tokenIndex) private view returns (uint256 boundedIndex) { + uint256 len = getBalancesLength(useStablePool); + boundedIndex = bound(tokenIndex, 0, len - 1); + } + + function boundTokenDeposit( + bool useStablePool, + uint256 tokenAmt, + uint256 tokenIndex + ) private view returns (uint256 boundedAmt) { + uint256[] memory balances = useStablePool ? stableBalanceLive : weightedBalanceLive; + boundedAmt = bound(tokenAmt, 0, _MAX_BALANCE - balances[tokenIndex]); + } + + function boundTokenWithdraw( + bool useStablePool, + uint256 tokenAmt, + uint256 tokenIndex + ) private view returns (uint256 boundedAmt) { + uint256[] memory balances = useStablePool ? stableBalanceLive : weightedBalanceLive; + boundedAmt = bound(tokenAmt, 0, balances[tokenIndex]); + } + + function boundBptMint(bool useStablePool, uint256 bptAmt) private view returns (uint256 boundedAmt) { + uint256 totalSupply = useStablePool ? stableBPTSupply : weightedBPTSupply; + boundedAmt = bound(bptAmt, 0, _MAX_BALANCE - totalSupply); + } + + function boundBptBurn(bool useStablePool, uint256 bptAmt) private view returns (uint256 boundedAmt) { + uint256 totalSupply = useStablePool ? stableBPTSupply : weightedBPTSupply; + boundedAmt = bound(bptAmt, 0, totalSupply); + } + + function boundBalanceLength(uint256[] memory balances, bool isStablePool) private pure returns (uint256[] memory) { + if (!isStablePool) { + if (balances.length < 3) revert(); + assembly { + mstore(balances, 3) + } + return balances; + } else { + if (balances.length < 2) revert(); + uint256 numTokens = bound(balances.length, 2, 8); + assembly { + mstore(balances, numTokens) + } + return balances; + } + } + + function sumBalances(uint256[] memory balances, uint256[] memory amounts) private pure returns (uint256[] memory) { + require(amounts.length == balances.length); + uint256[] memory newBalances = new uint256[](balances.length); + for (uint256 i = 0; i < balances.length; i++) { + newBalances[i] = balances[i] + amounts[i]; + } + return newBalances; + } + + function subBalances(uint256[] memory balances, uint256[] memory amounts) private pure returns (uint256[] memory) { + require(amounts.length == balances.length); + uint256[] memory newBalances = new uint256[](balances.length); + for (uint256 i = 0; i < balances.length; i++) { + newBalances[i] = balances[i] - amounts[i]; + } + return newBalances; + } + + function assumeValidBalanceLength(uint256[] memory balances) private pure { + if (balances.length < 2 || balances.length > 8) revert(); + } + + function assumeValidTradeAmount(uint256 tradeAmount) private pure { + if (tradeAmount != 0 && tradeAmount < _MINIMUM_TRADE_AMOUNT) { + revert(); + } + } +} diff --git a/pkg/vault/test/foundry/utils/BaseMedusaTest.sol b/pkg/vault/test/foundry/utils/BaseMedusaTest.sol new file mode 100644 index 000000000..ae31bf298 --- /dev/null +++ b/pkg/vault/test/foundry/utils/BaseMedusaTest.sol @@ -0,0 +1,124 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +pragma solidity ^0.8.24; + +import { IPermit2 } from "permit2/src/interfaces/IPermit2.sol"; +import { DeployPermit2 } from "permit2/test/utils/DeployPermit2.sol"; + +import { IProtocolFeeController } from "@balancer-labs/v3-interfaces/contracts/vault/IProtocolFeeController.sol"; +import { IVaultExtension } from "@balancer-labs/v3-interfaces/contracts/vault/IVaultExtension.sol"; +import { IVaultAdmin } from "@balancer-labs/v3-interfaces/contracts/vault/IVaultAdmin.sol"; +import { IVaultMock } from "@balancer-labs/v3-interfaces/contracts/test/IVaultMock.sol"; +import { IBasePool } from "@balancer-labs/v3-interfaces/contracts/vault/IBasePool.sol"; +import { IVault } from "@balancer-labs/v3-interfaces/contracts/vault/IVault.sol"; +import { IStdMedusaCheats } from "@balancer-labs/v3-interfaces/contracts/test/IStdMedusaCheats.sol"; +import "@balancer-labs/v3-interfaces/contracts/vault/VaultTypes.sol"; + +import { CREATE3 } from "@balancer-labs/v3-solidity-utils/contracts/solmate/CREATE3.sol"; +import { ERC20TestToken } from "@balancer-labs/v3-solidity-utils/contracts/test/ERC20TestToken.sol"; +import { WETHTestToken } from "@balancer-labs/v3-solidity-utils/contracts/test/WETHTestToken.sol"; + +import { VaultExtensionMock } from "../../../contracts/test/VaultExtensionMock.sol"; +import { VaultAdminMock } from "../../../contracts/test/VaultAdminMock.sol"; +import { VaultMock } from "../../../contracts/test/VaultMock.sol"; +import { ProtocolFeeController } from "../../../contracts/ProtocolFeeController.sol"; +import { BasicAuthorizerMock } from "../../../contracts/test/BasicAuthorizerMock.sol"; +import { RouterMock } from "../../../contracts/test/RouterMock.sol"; +import { BatchRouterMock } from "../../../contracts/test/BatchRouterMock.sol"; +import { CompositeLiquidityRouterMock } from "../../../contracts/test/CompositeLiquidityRouterMock.sol"; +import { ProtocolFeeControllerMock } from "../../../contracts/test/ProtocolFeeControllerMock.sol"; +import { PoolFactoryMock } from "../../../contracts/test/PoolFactoryMock.sol"; + +contract BaseMedusaTest { + IStdMedusaCheats internal medusa = IStdMedusaCheats(0x7109709ECfa91a80626fF3989D68f67F5b1DD12D); + + IPermit2 internal permit2; + + // Main contract mocks. + IVaultMock internal vault; + IVaultExtension internal vaultExtension; + IVaultAdmin internal vaultAdmin; + RouterMock internal router; + BatchRouterMock internal batchRouter; + CompositeLiquidityRouterMock internal compositeLiquidityRouter; + BasicAuthorizerMock internal authorizer; + ProtocolFeeControllerMock internal feeController; + PoolFactoryMock internal factoryMock; + + uint256 internal constant DEFAULT_USER_BALANCE = 1e9 * 1e18; + + // Set alice,bob and lp to addresses of medusa.json "senderAddresses" property + address internal alice = address(0x10000); + address internal bob = address(0x20000); + address internal lp = address(0x30000); + + ERC20TestToken internal dai; + ERC20TestToken internal usdc; + WETHTestToken internal weth; + + // Create Permit2 + // Create Vault/Routers/etc + // Set permissions for users + + constructor() { + dai = _createERC20TestToken("DAI", "DAI", 18); + usdc = _createERC20TestToken("USDC", "USDC", 18); + weth = new WETHTestToken(); + + // The only function used by _mintTokenToUsers is mint, which has the same signature as ERC20TestToken. So, + // cast weth as ERC20TestToken to use the same funtion. + _mintTokenToUsers(ERC20TestToken(address(weth))); + + DeployPermit2 _deployPermit2 = new DeployPermit2(); + permit2 = IPermit2(_deployPermit2.deployPermit2()); + + _deployVaultMock(0, 0); + + router = new RouterMock(IVault(address(vault)), weth, permit2); + batchRouter = new BatchRouterMock(IVault(address(vault)), weth, permit2); + compositeLiquidityRouter = new CompositeLiquidityRouterMock(IVault(address(vault)), weth, permit2); + } + + function _createERC20TestToken( + string memory name, + string memory symbol, + uint8 decimals + ) private returns (ERC20TestToken token) { + token = new ERC20TestToken(name, symbol, decimals); + _mintTokenToUsers(token); + } + + function _mintTokenToUsers(ERC20TestToken token) private { + token.mint(alice, DEFAULT_USER_BALANCE); + token.mint(bob, DEFAULT_USER_BALANCE); + token.mint(lp, DEFAULT_USER_BALANCE); + } + + function _deployVaultMock(uint256 minTradeAmount, uint256 minWrapAmount) private { + authorizer = new BasicAuthorizerMock(); + bytes32 salt = bytes32(0); + VaultMock newVault = VaultMock(payable(CREATE3.getDeployed(salt))); + + bytes memory vaultMockBytecode = type(VaultMock).creationCode; + vaultAdmin = new VaultAdminMock( + IVault(payable(address(newVault))), + 90 days, + 30 days, + minTradeAmount, + minWrapAmount + ); + vaultExtension = new VaultExtensionMock(IVault(payable(address(newVault))), vaultAdmin); + feeController = new ProtocolFeeControllerMock(IVault(payable(address(newVault)))); + + _create3(abi.encode(vaultExtension, authorizer, feeController), vaultMockBytecode, salt); + + address poolFactoryMock = newVault.getPoolFactoryMock(); + factoryMock = PoolFactoryMock(poolFactoryMock); + + vault = IVaultMock(address(newVault)); + } + + function _create3(bytes memory constructorArgs, bytes memory bytecode, bytes32 salt) private returns (address) { + return CREATE3.deploy(salt, abi.encodePacked(bytecode, constructorArgs), 0); + } +} From c892b5499fa5013d2edf08c80fc37fe870b46340 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Bruno?= Date: Tue, 8 Oct 2024 18:36:03 -0300 Subject: [PATCH 03/29] Implement Stateful tests with Medusa --- .gitignore | 2 +- pkg/pool-stable/medusa.json | 84 +++ pkg/pool-stable/package.json | 1 + .../AddAndRemoveLiquidityStable.medusa.sol | 57 ++ pkg/pool-weighted/medusa.json | 84 +++ pkg/pool-weighted/package.json | 1 + .../AddAndRemoveLiquidityWeighted.medusa.sol | 76 +++ pkg/vault/medusa.json | 4 +- .../fuzz/AddAndRemoveLiquidity.medusa.sol | 372 ++++++++++++ pkg/vault/test/foundry/fuzz/FuzzAddRemove.sol | 547 ------------------ .../test/foundry/utils/BaseMedusaTest.sol | 109 +++- 11 files changed, 783 insertions(+), 554 deletions(-) create mode 100644 pkg/pool-stable/medusa.json create mode 100644 pkg/pool-stable/test/foundry/fuzz/AddAndRemoveLiquidityStable.medusa.sol create mode 100644 pkg/pool-weighted/medusa.json create mode 100644 pkg/pool-weighted/test/foundry/fuzz/AddAndRemoveLiquidityWeighted.medusa.sol create mode 100644 pkg/vault/test/foundry/fuzz/AddAndRemoveLiquidity.medusa.sol delete mode 100644 pkg/vault/test/foundry/fuzz/FuzzAddRemove.sol diff --git a/.gitignore b/.gitignore index 27fc8f05d..3bbb9a26e 100644 --- a/.gitignore +++ b/.gitignore @@ -53,4 +53,4 @@ slither/ # Fuzz tests crytic-export/ -pkg/vault/medusa-corpus/ +medusa-corpus/ diff --git a/pkg/pool-stable/medusa.json b/pkg/pool-stable/medusa.json new file mode 100644 index 000000000..582fc3554 --- /dev/null +++ b/pkg/pool-stable/medusa.json @@ -0,0 +1,84 @@ +{ + "fuzzing": { + "workers": 10, + "workerResetLimit": 50, + "timeout": 0, + "testLimit": 10000, + "shrinkLimit": 5000, + "callSequenceLength": 100, + "corpusDirectory": "medusa-corpus", + "coverageEnabled": true, + "targetContracts": ["AddAndRemoveLiquidityStableMedusaTest"], + "predeployedContracts": {}, + "targetContractsBalances": [], + "constructorArgs": {}, + "deployerAddress": "0x30000", + "senderAddresses": [ + "0x10000", + "0x20000", + "0x30000" + ], + "blockNumberDelayMax": 60480, + "blockTimestampDelayMax": 604800, + "blockGasLimit": 1250000000, + "transactionGasLimit": 125000000, + "testing": { + "stopOnFailedTest": false, + "stopOnFailedContractMatching": false, + "stopOnNoTests": true, + "testAllContracts": false, + "traceAll": false, + "assertionTesting": { + "enabled": true, + "testViewMethods": false, + "panicCodeConfig": { + "failOnCompilerInsertedPanic": false, + "failOnAssertion": true, + "failOnArithmeticUnderflow": false, + "failOnDivideByZero": false, + "failOnEnumTypeConversionOutOfBounds": false, + "failOnIncorrectStorageAccess": false, + "failOnPopEmptyArray": false, + "failOnOutOfBoundsArrayAccess": false, + "failOnAllocateTooMuchMemory": false, + "failOnCallUninitializedVariable": false + } + }, + "propertyTesting": { + "enabled": true, + "testPrefixes": [ + "property_" + ] + }, + "optimizationTesting": { + "enabled": true, + "testPrefixes": [ + "optimize_" + ] + }, + "targetFunctionSignatures": [], + "excludeFunctionSignatures": [] + }, + "chainConfig": { + "codeSizeCheckDisabled": true, + "cheatCodes": { + "cheatCodesEnabled": true, + "enableFFI": true + } + } + }, + "compilation": { + "platform": "crytic-compile", + "platformConfig": { + "target": ".", + "solcVersion": "", + "exportDirectory": "", + "args": ["--foundry-out-directory=forge-artifacts", "--foundry-compile-all"] + } + }, + "logging": { + "level": "info", + "logDirectory": "", + "noColor": false + } +} diff --git a/pkg/pool-stable/package.json b/pkg/pool-stable/package.json index b6b8a3a9a..6bd280280 100644 --- a/pkg/pool-stable/package.json +++ b/pkg/pool-stable/package.json @@ -28,6 +28,7 @@ "test:hardhat": "hardhat test", "test:forge": "yarn build && REUSING_HARDHAT_ARTIFACTS=true forge test --ffi -vvv", "test:stress": "FOUNDRY_PROFILE=intense forge test --ffi -vvv", + "fuzz:medusa": "medusa fuzz --config medusa.json", "coverage": "./coverage.sh forge", "coverage:hardhat": "./coverage.sh hardhat", "coverage:all": "./coverage.sh all", diff --git a/pkg/pool-stable/test/foundry/fuzz/AddAndRemoveLiquidityStable.medusa.sol b/pkg/pool-stable/test/foundry/fuzz/AddAndRemoveLiquidityStable.medusa.sol new file mode 100644 index 000000000..a50260f17 --- /dev/null +++ b/pkg/pool-stable/test/foundry/fuzz/AddAndRemoveLiquidityStable.medusa.sol @@ -0,0 +1,57 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +pragma solidity ^0.8.24; + +import "forge-std/Test.sol"; + +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + +import { IVault } from "@balancer-labs/v3-interfaces/contracts/vault/IVault.sol"; +import "@balancer-labs/v3-interfaces/contracts/vault/VaultTypes.sol"; + +import { InputHelpers } from "@balancer-labs/v3-solidity-utils/contracts/helpers/InputHelpers.sol"; + +import { + AddAndRemoveLiquidityMedusaTest +} from "@balancer-labs/v3-vault/test/foundry/fuzz/AddAndRemoveLiquidity.medusa.sol"; + +import { StablePoolFactory } from "../../../contracts/StablePoolFactory.sol"; +import { StablePool } from "../../../contracts/StablePool.sol"; + +contract AddAndRemoveLiquidityStableMedusaTest is AddAndRemoveLiquidityMedusaTest { + uint256 internal constant _AMPLIFICATION_PARAMETER = 1000; + + constructor() AddAndRemoveLiquidityMedusaTest() { + maxRateTolerance = 0; + } + + function createPool( + IERC20[] memory tokens, + uint256[] memory initialBalances + ) internal override returns (address newPool) { + StablePoolFactory factory = new StablePoolFactory(IVault(address(vault)), 365 days, "Factory v1", "Pool v1"); + PoolRoleAccounts memory roleAccounts; + + StablePool newPool = StablePool( + factory.create( + "Stable Pool", + "STABLE", + vault.buildTokenConfig(tokens), + _AMPLIFICATION_PARAMETER, + roleAccounts, + DEFAULT_SWAP_FEE, // 1% swap fee, but test will override it + address(0), // No hooks + false, // Do not enable donations + false, // Do not disable unbalanced add/remove liquidity + // NOTE: sends a unique salt. + bytes32(poolCreationNonce++) + ) + ); + + // Initialize liquidity of stable pool. + medusa.prank(lp); + router.initialize(address(newPool), tokens, initialBalances, 0, false, bytes("")); + + return address(newPool); + } +} diff --git a/pkg/pool-weighted/medusa.json b/pkg/pool-weighted/medusa.json new file mode 100644 index 000000000..b9c045da5 --- /dev/null +++ b/pkg/pool-weighted/medusa.json @@ -0,0 +1,84 @@ +{ + "fuzzing": { + "workers": 10, + "workerResetLimit": 50, + "timeout": 0, + "testLimit": 10000, + "shrinkLimit": 5000, + "callSequenceLength": 100, + "corpusDirectory": "medusa-corpus", + "coverageEnabled": true, + "targetContracts": ["AddAndRemoveLiquidityWeightedMedusaTest"], + "predeployedContracts": {}, + "targetContractsBalances": [], + "constructorArgs": {}, + "deployerAddress": "0x30000", + "senderAddresses": [ + "0x10000", + "0x20000", + "0x30000" + ], + "blockNumberDelayMax": 60480, + "blockTimestampDelayMax": 604800, + "blockGasLimit": 1250000000, + "transactionGasLimit": 125000000, + "testing": { + "stopOnFailedTest": false, + "stopOnFailedContractMatching": false, + "stopOnNoTests": true, + "testAllContracts": false, + "traceAll": false, + "assertionTesting": { + "enabled": true, + "testViewMethods": false, + "panicCodeConfig": { + "failOnCompilerInsertedPanic": false, + "failOnAssertion": true, + "failOnArithmeticUnderflow": false, + "failOnDivideByZero": false, + "failOnEnumTypeConversionOutOfBounds": false, + "failOnIncorrectStorageAccess": false, + "failOnPopEmptyArray": false, + "failOnOutOfBoundsArrayAccess": false, + "failOnAllocateTooMuchMemory": false, + "failOnCallUninitializedVariable": false + } + }, + "propertyTesting": { + "enabled": true, + "testPrefixes": [ + "property_" + ] + }, + "optimizationTesting": { + "enabled": true, + "testPrefixes": [ + "optimize_" + ] + }, + "targetFunctionSignatures": [], + "excludeFunctionSignatures": [] + }, + "chainConfig": { + "codeSizeCheckDisabled": true, + "cheatCodes": { + "cheatCodesEnabled": true, + "enableFFI": true + } + } + }, + "compilation": { + "platform": "crytic-compile", + "platformConfig": { + "target": ".", + "solcVersion": "", + "exportDirectory": "", + "args": ["--foundry-out-directory=forge-artifacts", "--foundry-compile-all"] + } + }, + "logging": { + "level": "info", + "logDirectory": "", + "noColor": false + } +} diff --git a/pkg/pool-weighted/package.json b/pkg/pool-weighted/package.json index b3699dfd9..c11e1579d 100644 --- a/pkg/pool-weighted/package.json +++ b/pkg/pool-weighted/package.json @@ -28,6 +28,7 @@ "test:hardhat": "hardhat test", "test:forge": "yarn build && REUSING_HARDHAT_ARTIFACTS=true forge test --ffi -vvv", "test:stress": "FOUNDRY_PROFILE=intense forge test --ffi -vvv", + "fuzz:medusa": "medusa fuzz --config medusa.json", "coverage": "./coverage.sh forge", "coverage:hardhat": "./coverage.sh hardhat", "coverage:all": "./coverage.sh all", diff --git a/pkg/pool-weighted/test/foundry/fuzz/AddAndRemoveLiquidityWeighted.medusa.sol b/pkg/pool-weighted/test/foundry/fuzz/AddAndRemoveLiquidityWeighted.medusa.sol new file mode 100644 index 000000000..b1de1a0ba --- /dev/null +++ b/pkg/pool-weighted/test/foundry/fuzz/AddAndRemoveLiquidityWeighted.medusa.sol @@ -0,0 +1,76 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +pragma solidity ^0.8.24; + +import "forge-std/Test.sol"; + +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + +import { IVault } from "@balancer-labs/v3-interfaces/contracts/vault/IVault.sol"; +import "@balancer-labs/v3-interfaces/contracts/vault/VaultTypes.sol"; + +import { InputHelpers } from "@balancer-labs/v3-solidity-utils/contracts/helpers/InputHelpers.sol"; + +import { + AddAndRemoveLiquidityMedusaTest +} from "@balancer-labs/v3-vault/test/foundry/fuzz/AddAndRemoveLiquidity.medusa.sol"; + +import { WeightedPoolFactory } from "../../../contracts/WeightedPoolFactory.sol"; +import { WeightedPool } from "../../../contracts/WeightedPool.sol"; + +contract AddAndRemoveLiquidityWeightedMedusaTest is AddAndRemoveLiquidityMedusaTest { + uint256 private constant _MIN_WEIGHT = 1e16; // 1% (from WeightedPool.sol) + uint256 private constant _WEIGHT1 = 33e16; + uint256 private constant _WEIGHT2 = 33e16; + + constructor() AddAndRemoveLiquidityMedusaTest() { + maxRateTolerance = 500; + } + + function createPool( + IERC20[] memory tokens, + uint256[] memory initialBalances + ) internal override returns (address newPool) { + uint256[] memory weights = new uint256[](3); + weights[0] = bound(_WEIGHT1, _MIN_WEIGHT, 98e16); // any weight between min & 100%-2(min) + uint256 remainingWeight = 99e16 - weights[0]; // weights 0 + 1 must <= 100%-min so there's >=1% left for weight 2 + weights[1] = bound(_WEIGHT2, _MIN_WEIGHT, remainingWeight); + remainingWeight = 100e16 - (weights[0] + weights[1]); + weights[2] = remainingWeight; + + WeightedPoolFactory factory = new WeightedPoolFactory( + IVault(address(vault)), + 365 days, + "Factory v1", + "Pool v1" + ); + PoolRoleAccounts memory roleAccounts; + + WeightedPool newPool = WeightedPool( + factory.create( + "Weighted Pool", + "WP", + vault.buildTokenConfig(tokens), + weights, + roleAccounts, + DEFAULT_SWAP_FEE, // 1% swap fee + address(0), // No hooks + false, // Do not enable donations + false, // Do not disable unbalanced add/remove liquidity + // NOTE: sends a unique salt. + bytes32(poolCreationNonce++) + ) + ); + + // Cannot set the pool creator directly on a standard Balancer weighted pool factory. + vault.manualSetPoolCreator(address(newPool), lp); + + feeController.manualSetPoolCreator(address(newPool), lp); + + // Initialize liquidity of weighted pool. + medusa.prank(lp); + router.initialize(address(newPool), tokens, initialBalances, 0, false, bytes("")); + + return address(newPool); + } +} diff --git a/pkg/vault/medusa.json b/pkg/vault/medusa.json index 0ff7d1e62..48b0149f2 100644 --- a/pkg/vault/medusa.json +++ b/pkg/vault/medusa.json @@ -3,12 +3,12 @@ "workers": 10, "workerResetLimit": 50, "timeout": 0, - "testLimit": 100000, + "testLimit": 10000, "shrinkLimit": 5000, "callSequenceLength": 100, "corpusDirectory": "medusa-corpus", "coverageEnabled": true, - "targetContracts": ["FuzzAddRemove"], + "targetContracts": ["AddAndRemoveLiquidityMedusaTest"], "predeployedContracts": {}, "targetContractsBalances": [], "constructorArgs": {}, diff --git a/pkg/vault/test/foundry/fuzz/AddAndRemoveLiquidity.medusa.sol b/pkg/vault/test/foundry/fuzz/AddAndRemoveLiquidity.medusa.sol new file mode 100644 index 000000000..d18f6ba59 --- /dev/null +++ b/pkg/vault/test/foundry/fuzz/AddAndRemoveLiquidity.medusa.sol @@ -0,0 +1,372 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +pragma solidity ^0.8.24; + +import "forge-std/Test.sol"; + +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + +import { IBasePool } from "@balancer-labs/v3-interfaces/contracts/vault/IBasePool.sol"; +import { Rounding } from "@balancer-labs/v3-interfaces/contracts/vault/VaultTypes.sol"; + +import { FixedPoint } from "@balancer-labs/v3-solidity-utils/contracts/math/FixedPoint.sol"; +import { InputHelpers } from "@balancer-labs/v3-solidity-utils/contracts/helpers/InputHelpers.sol"; + +import { StablePoolFactory } from "@balancer-labs/v3-pool-stable/contracts/StablePoolFactory.sol"; +import { StablePool } from "@balancer-labs/v3-pool-stable/contracts/StablePool.sol"; + +import { WeightedPool } from "@balancer-labs/v3-pool-weighted/contracts/WeightedPool.sol"; +import { WeightedPoolFactory } from "@balancer-labs/v3-pool-weighted/contracts/WeightedPoolFactory.sol"; + +import { BasePoolMath } from "../../../contracts/BasePoolMath.sol"; +import { BalancerPoolToken } from "../../../contracts/BalancerPoolToken.sol"; + +import "../utils/BaseMedusaTest.sol"; + +contract AddAndRemoveLiquidityMedusaTest is BaseMedusaTest { + using FixedPoint for uint256; + + uint256 internal constant DEFAULT_SWAP_FEE = 1e16; // 1% + + uint256 private constant _MAX_BALANCE = 2 ** (128) - 1; // from PackedTokenBalance.sol + uint256 private constant _MINIMUM_TRADE_AMOUNT = 1e6; + uint256 private constant _POOL_MINIMUM_TOTAL_SUPPLY = 1e6; + + uint256 internal maxRateTolerance = 0; + + uint256 internal initialRate; + + // State var for optimization mode + int256 internal rateDecrease = 0; + + constructor() BaseMedusaTest() { + initialRate = getBptRate(); + } + + //////////////////////////////////////// + // Optimizations + + function optimize_rateDecrease() public view returns (int256) { + return rateDecrease; + } + + // function optimize_bptProfit() public view returns (int256) { + // return int256(bptProfit); + // } + + //////////////////////////////////////// + // Symmetrical Add/Remove Liquidity + + // function computeAddAndRemoveLiquiditySingleToken( + // uint256 tokenIndex, + // uint256 bptMintAmt, + // uint256 swapFeePercentage + // ) public { + // tokenIndex = boundTokenIndex(tokenIndex); + // bptMintAmt = boundBptMint(bptMintAmt); + // + // // deposit tokenAmt to mint exactly bptMintAmt + // uint256 tokenAmt = computeAddLiquiditySingleTokenExactOut( + // tokenIndex, + // bptMintAmt, + // swapFeePercentage + // ); + // + // // withdraw exactly tokenAmt to burn bptBurnAmt + // uint256 bptBurnAmt = computeRemoveLiquiditySingleTokenExactIn( + // tokenIndex, + // tokenAmt, + // swapFeePercentage + // ); + // + // emit Debug("BPT minted while adding liq:", bptMintAmt); + // emit Debug("BPT burned while removing the same liq:", bptBurnAmt); + // bptProfit = bptMintAmt - bptBurnAmt; + // if (ASSERT_MODE) { + // assert(bptProfit <= 0); + // } + // } + + // function computeRemoveAndAddLiquiditySingleToken( + // uint256 tokenIndex, + // uint256 tokenAmt, + // uint256 swapFeePercentage + // ) public { + // tokenIndex = boundTokenIndex(tokenIndex); + // tokenAmt = boundTokenWithdraw(tokenAmt, tokenIndex); + // + // // withdraw exactly tokenAmt to burn bptBurnAmt + // uint256 bptBurnAmt = computeRemoveLiquiditySingleTokenExactOut( + // tokenIndex, + // tokenAmt, + // swapFeePercentage + // ); + // + // // deposit exactly tokenAmt to mint bptMintAmt + // uint256[] memory exactAmounts = new uint256[](getBalancesLength()); + // exactAmounts[tokenIndex] = tokenAmt; + // uint256 bptMintAmt = computeAddLiquidityUnbalanced(exactAmounts, swapFeePercentage); + // + // emit Debug("BPT burned while removing liq:", bptBurnAmt); + // emit Debug("BPT minted while adding the same liq:", bptMintAmt); + // bptProfit = bptMintAmt - bptBurnAmt; + // if (ASSERT_MODE) { + // assert(bptProfit <= 0); + // } + // } + // + // function computeAddAndRemoveAddLiquidityMultiToken( + // uint256 bptMintAmt, + // uint256 swapFeePercentage + // ) public { + // bptMintAmt = boundBptMint(bptMintAmt); + // + // // mint exactly bptMintAmt to deposit tokenAmts + // uint256[] memory tokenAmts = computeProportionalAmountsIn(bptMintAmt); + // + // // withdraw exactly tokenAmts to burn bptBurnAmt + // // No computeRemoveLiquidityUnbalanced fn available, need to go one at a time to accomplish this + // uint256 bptBurnAmt = 0; + // for (uint256 i = 0; i < tokenAmts.length; i++) { + // bptBurnAmt += computeRemoveLiquiditySingleTokenExactOut(i, tokenAmts[i], swapFeePercentage); + // } + // + // emit Debug("BPT minted while adding liquidity:", bptMintAmt); + // emit Debug("BPT burned while removing same liquidity:", bptBurnAmt); + // bptProfit = bptMintAmt - bptBurnAmt; + // if (ASSERT_MODE) { + // assert(bptProfit <= 0); + // } + // } + // + // function computeRemoveAndAddLiquidityMultiToken( + // uint256 bptBurnAmt, + // uint256 swapFeePercentage + // ) public { + // bptBurnAmt = boundBptBurn(bptBurnAmt); + // + // // burn exactly bptBurnAmt to withdraw tokenAmts + // uint256[] memory tokenAmts = computeProportionalAmountsOut(bptBurnAmt); + // + // // deposit exactly tokenAmts to mint bptMintAmt + // uint256 bptMintAmt = computeAddLiquidityUnbalanced(tokenAmts, swapFeePercentage); + // + // emit Debug("BPT burned while removing liquidity:", bptBurnAmt); + // emit Debug("BPT minted while adding the same liquidity:", bptMintAmt); + // bptProfit = bptMintAmt - bptBurnAmt; + // if (ASSERT_MODE) { + // assert(bptProfit <= 0); + // } + // } + // + //////////////////////////////////////// + // Rate Invariants + + function computeProportionalAmountsIn(uint256 bptAmountOut) public returns (uint256[] memory amountsIn) { + assumeValidTradeAmount(bptAmountOut); + bptAmountOut = boundBptMint(bptAmountOut); + + uint256[] memory maxAmountsIn = getMaxAmountsIn(); + medusa.prank(lp); + amountsIn = router.addLiquidityProportional(address(pool), maxAmountsIn, bptAmountOut, false, bytes("")); + + updateRateDecrease(); + } + + function computeProportionalAmountsOut(uint256 bptAmountIn) public returns (uint256[] memory amountsOut) { + assumeValidTradeAmount(bptAmountIn); + bptAmountIn = boundBptBurn(bptAmountIn); + + uint256[] memory minAmountsOut = getMinAmountsOut(); + medusa.prank(lp); + amountsOut = router.removeLiquidityProportional(address(pool), bptAmountIn, minAmountsOut, false, bytes("")); + + updateRateDecrease(); + } + + function computeAddLiquidityUnbalanced( + uint256[] memory exactAmountsIn, + uint256 swapFeePercentage + ) public returns (uint256 bptAmountOut) { + exactAmountsIn = boundBalanceLength(exactAmountsIn); + for (uint256 i = 0; i < exactAmountsIn.length; i++) { + exactAmountsIn[i] = boundTokenDeposit(exactAmountsIn[i], i); + } + + medusa.prank(lp); + bptAmountOut = router.addLiquidityUnbalanced(address(pool), exactAmountsIn, 0, false, bytes("")); + + updateRateDecrease(); + } + + function computeAddLiquiditySingleTokenExactOut( + uint256 tokenInIndex, + uint256 exactBptAmountOut, + uint256 swapFeePercentage + ) public returns (uint256 amountIn) { + assumeValidTradeAmount(exactBptAmountOut); + tokenInIndex = boundTokenIndex(tokenInIndex); + exactBptAmountOut = boundBptMint(exactBptAmountOut); + + (IERC20[] memory tokens, , , ) = vault.getPoolTokenInfo(address(pool)); + + medusa.prank(lp); + amountIn = router.addLiquiditySingleTokenExactOut( + address(pool), + tokens[tokenInIndex], + type(uint128).max, + exactBptAmountOut, + false, + bytes("") + ); + + updateRateDecrease(); + } + + function computeRemoveLiquiditySingleTokenExactOut( + uint256 tokenOutIndex, + uint256 exactAmountOut, + uint256 swapFeePercentage + ) public returns (uint256 bptAmountIn) { + assumeValidTradeAmount(exactAmountOut); + tokenOutIndex = boundTokenIndex(tokenOutIndex); + exactAmountOut = boundTokenAmountOut(exactAmountOut, tokenOutIndex); + + (IERC20[] memory tokens, , , ) = vault.getPoolTokenInfo(address(pool)); + + medusa.prank(lp); + bptAmountIn = router.removeLiquiditySingleTokenExactOut( + address(pool), + type(uint128).max, + tokens[tokenOutIndex], + exactAmountOut, + false, + bytes("") + ); + + updateRateDecrease(); + } + + function computeRemoveLiquiditySingleTokenExactIn( + uint256 tokenOutIndex, + uint256 exactBptAmountIn, + uint256 swapFeePercentage + ) public returns (uint256 amountOut) { + assumeValidTradeAmount(exactBptAmountIn); + tokenOutIndex = boundTokenIndex(tokenOutIndex); + exactBptAmountIn = boundBptBurn(exactBptAmountIn); + + (IERC20[] memory tokens, , , ) = vault.getPoolTokenInfo(address(pool)); + + medusa.prank(lp); + amountOut = router.removeLiquiditySingleTokenExactIn( + address(pool), + exactBptAmountIn, + tokens[tokenOutIndex], + 0, + false, + bytes("") + ); + + updateRateDecrease(); + } + + function property_rate_never_decreases() public returns (bool) { + return assertRate(pool); + } + + //////////////////////////////////////// + // Helpers (private functions, so they're not fuzzed) + + function assertRate(IBasePool pool) internal returns (bool) { + updateRateDecrease(); + return rateDecrease <= int256(maxRateTolerance); + } + + function updateRateDecrease() internal { + uint256 rateAfter = getBptRate(); + rateDecrease = int256(initialRate) - int256(rateAfter); + + emit Debug("initial rate", initialRate); + emit Debug("rate after", rateAfter); + } + + function getBptRate() internal returns (uint256) { + (, , , uint256[] memory lastBalancesLiveScaled18) = vault.getPoolTokenInfo(address(pool)); + uint256 bptTotalSupply = BalancerPoolToken(address(pool)).totalSupply(); + + uint256 invariant = pool.computeInvariant(lastBalancesLiveScaled18, Rounding.ROUND_DOWN); + return invariant.divDown(bptTotalSupply); + } + + function getBalancesLength() internal view returns (uint256 length) { + (, , uint256[] memory balancesRaw, ) = vault.getPoolTokenInfo(address(pool)); + length = balancesRaw.length; + } + + function boundTokenIndex(uint256 tokenIndex) internal view returns (uint256 boundedIndex) { + uint256 len = getBalancesLength(); + boundedIndex = bound(tokenIndex, 0, len - 1); + } + + function boundTokenDeposit( + uint256 tokenAmountIn, + uint256 tokenIndex + ) internal view returns (uint256 boundedAmountIn) { + (, , uint256[] memory balancesRaw, ) = vault.getPoolTokenInfo(address(pool)); + boundedAmountIn = bound(tokenAmountIn, 0, _MAX_BALANCE - balancesRaw[tokenIndex]); + } + + function boundTokenAmountOut( + uint256 tokenAmountOut, + uint256 tokenIndex + ) internal view returns (uint256 boundedAmountOut) { + (, , uint256[] memory balancesRaw, ) = vault.getPoolTokenInfo(address(pool)); + boundedAmountOut = bound(tokenAmountOut, 0, balancesRaw[tokenIndex]); + } + + function boundBptMint(uint256 bptAmount) internal view returns (uint256 boundedAmt) { + uint256 totalSupply = BalancerPoolToken(address(pool)).totalSupply(); + boundedAmt = bound(bptAmount, 0, _MAX_BALANCE - totalSupply); + } + + function boundBptBurn(uint256 bptAmt) internal view returns (uint256 boundedAmt) { + uint256 totalSupply = BalancerPoolToken(address(pool)).totalSupply(); + boundedAmt = bound(bptAmt, 0, totalSupply); + } + + function boundBalanceLength(uint256[] memory balances) internal view returns (uint256[] memory) { + (IERC20[] memory tokens, , , ) = vault.getPoolTokenInfo(address(pool)); + uint256 length = tokens.length; + assembly { + mstore(balances, length) + } + return balances; + } + + function assumeValidTradeAmount(uint256 tradeAmount) internal pure { + if (tradeAmount != 0 && tradeAmount < _MINIMUM_TRADE_AMOUNT) { + revert(); + } + } + + function getMinAmountsOut() internal view returns (uint256[] memory minAmountsOut) { + (, , uint256[] memory balances, ) = vault.getPoolTokenInfo(address(pool)); + + minAmountsOut = new uint256[](balances.length); + + for (uint256 i = 0; i < balances.length; i++) { + minAmountsOut[i] = 0; + } + } + + function getMaxAmountsIn() internal view returns (uint256[] memory maxAmountsIn) { + (, , uint256[] memory balances, ) = vault.getPoolTokenInfo(address(pool)); + + maxAmountsIn = new uint256[](balances.length); + + for (uint256 i = 0; i < balances.length; i++) { + maxAmountsIn[i] = type(uint128).max; + } + } +} diff --git a/pkg/vault/test/foundry/fuzz/FuzzAddRemove.sol b/pkg/vault/test/foundry/fuzz/FuzzAddRemove.sol deleted file mode 100644 index ebe7a07bd..000000000 --- a/pkg/vault/test/foundry/fuzz/FuzzAddRemove.sol +++ /dev/null @@ -1,547 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity ^0.8.24; - -import "forge-std/Test.sol"; - -import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; - -import { IBasePool } from "@balancer-labs/v3-interfaces/contracts/vault/IBasePool.sol"; -import { Rounding } from "@balancer-labs/v3-interfaces/contracts/vault/VaultTypes.sol"; - -import { FixedPoint } from "@balancer-labs/v3-solidity-utils/contracts/math/FixedPoint.sol"; -import { InputHelpers } from "@balancer-labs/v3-solidity-utils/contracts/helpers/InputHelpers.sol"; - -import { StablePool } from "@balancer-labs/v3-pool-stable/contracts/StablePool.sol"; - -import { WeightedPool } from "@balancer-labs/v3-pool-weighted/contracts/WeightedPool.sol"; -import { WeightedPoolFactory } from "@balancer-labs/v3-pool-weighted/contracts/WeightedPoolFactory.sol"; - -import { BasePoolMath } from "../../../contracts/BasePoolMath.sol"; - -import "../utils/BaseMedusaTest.sol"; - -contract FuzzHarness is Test, BaseMedusaTest { - using FixedPoint for uint256; - - event Debug(string, uint256); - - // If this flag is false, assertion tests will never fail! But optimizations will work. - // Changing to false will disable optimizations but allow the assertion tests to run - bool private constant ASSERT_MODE = false; - - uint256 internal constant DEFAULT_SWAP_FEE = 1e16; // 1% - - uint256 private constant _MAX_BALANCE = 2 ** (128) - 1; // from PackedTokenBalance.sol - uint256 private constant _MIN_WEIGHT = 1e16; // 1% (from WeightedPool.sol) - uint256 private constant _MINIMUM_TRADE_AMOUNT = 1e6; - uint256 private constant _POOL_MINIMUM_TOTAL_SUPPLY = 1e6; - - uint256 internal poolCreationNonce; - - IBasePool private stablePool; - IBasePool private weightedPool; - - // State management - uint256[] weightedBalanceLive; - uint256[] stableBalanceLive; - uint256 weightedBPTSupply; - uint256 stableBPTSupply; - - // State vars for optimization mode - uint256 rateDecrease = 0; - uint256 bptProfit = 0; - - constructor() BaseMedusaTest() { - uint256[] memory initialBalances = new uint256[](3); - initialBalances[0] = 10e18; - initialBalances[1] = 20e18; - initialBalances[2] = 30e18; - createNewWeightedPool(33e16, 33e16, initialBalances); - createNewStablePool(1000, initialBalances); - } - - //////////////////////////////////////// - // Pool Creation - - function createNewWeightedPool(uint256 weight1, uint256 weight2, uint256[] memory initialBalances) private { - uint256[] memory weights = new uint256[](3); - weights[0] = bound(weight1, _MIN_WEIGHT, 98e16); // any weight between min & 100%-2(min) - uint256 remainingWeight = 99e16 - weights[0]; // weights 0 + 1 must <= 100%-min so there's >=1% left for weight 2 - weights[1] = bound(weight2, _MIN_WEIGHT, remainingWeight); - remainingWeight = 100e16 - (weights[0] + weights[1]); - weights[2] = remainingWeight; - - IERC20[] memory tokens = new IERC20[](3); - tokens[0] = dai; - tokens[1] = usdc; - tokens[2] = weth; - tokens = InputHelpers.sortTokens(tokens); - - WeightedPoolFactory factory = new WeightedPoolFactory( - IVault(address(vault)), - 365 days, - "Factory v1", - "Pool v1" - ); - PoolRoleAccounts memory roleAccounts; - - WeightedPool newPool = WeightedPool( - factory.create( - "Weighted Pool", - "WP", - vault.buildTokenConfig(tokens), - weights, - roleAccounts, - DEFAULT_SWAP_FEE, // 1% swap fee - address(0), // No hooks - false, // Do not enable donations - false, // Do not disable unbalanced add/remove liquidity - // NOTE: sends a unique salt. - bytes32(poolCreationNonce++) - ) - ); - - // Cannot set the pool creator directly on a standard Balancer weighted pool factory. - vault.manualSetPoolCreator(address(newPool), lp); - - feeController.manualSetPoolCreator(address(newPool), lp); - - // Initialize liquidity for this new weighted pool - medusa.prank(lp); - router.initialize(address(newPool), tokens, initialBalances, 0, false, bytes("")); - - weightedPool = IBasePool(address(newPool)); - } - - function createNewStablePool(uint256 amplificationParameter, uint256[] memory initialBalances) private { - StablePool.NewPoolParams memory params = StablePool.NewPoolParams({ - name: "My Custom Pool", - symbol: "MCP", - amplificationParameter: bound(amplificationParameter, 1, 5000), - version: "1.0.0" - }); - stablePool = IBasePool(new StablePool(params, vault)); - // Initialize liquidity for this new stable pool - initialBalances = boundBalanceLength(initialBalances, true); - for (uint256 i; i < initialBalances.length; i++) { - if (initialBalances[i] < 1 ether) initialBalances[i] += 1 ether; - stableBalanceLive.push(initialBalances[i]); - } - stableBPTSupply += mockInitialize(stablePool, initialBalances); - } - - //////////////////////////////////////// - // Optimizations - - function optimize_rateDecrease() public view returns (int256) { - return int256(rateDecrease); - } - - function optimize_bptProfit() public view returns (int256) { - return int256(bptProfit); - } - - //////////////////////////////////////// - // Symmetrical Add/Remove Liquidity - - function computeAddAndRemoveLiquiditySingleToken( - uint256 tokenIndex, - uint256 bptMintAmt, - uint256 swapFeePercentage, - bool useStablePool - ) public { - tokenIndex = boundTokenIndex(useStablePool, tokenIndex); - bptMintAmt = boundBptMint(useStablePool, bptMintAmt); - - // deposit tokenAmt to mint exactly bptMintAmt - uint256 tokenAmt = computeAddLiquiditySingleTokenExactOut( - tokenIndex, - bptMintAmt, - swapFeePercentage, - useStablePool - ); - - // withdraw exactly tokenAmt to burn bptBurnAmt - uint256 bptBurnAmt = computeRemoveLiquiditySingleTokenExactIn( - tokenIndex, - tokenAmt, - swapFeePercentage, - useStablePool - ); - - emit Debug("BPT minted while adding liq:", bptMintAmt); - emit Debug("BPT burned while removing the same liq:", bptBurnAmt); - bptProfit = bptMintAmt - bptBurnAmt; - if (ASSERT_MODE) { - assert(bptProfit <= 0); - } - } - - function computeRemoveAndAddLiquiditySingleToken( - uint256 tokenIndex, - uint256 tokenAmt, - uint256 swapFeePercentage, - bool useStablePool - ) public { - tokenIndex = boundTokenIndex(useStablePool, tokenIndex); - tokenAmt = boundTokenWithdraw(useStablePool, tokenAmt, tokenIndex); - - // withdraw exactly tokenAmt to burn bptBurnAmt - uint256 bptBurnAmt = computeRemoveLiquiditySingleTokenExactOut( - tokenIndex, - tokenAmt, - swapFeePercentage, - useStablePool - ); - - // deposit exactly tokenAmt to mint bptMintAmt - uint256[] memory exactAmounts = new uint256[](getBalancesLength(useStablePool)); - exactAmounts[tokenIndex] = tokenAmt; - uint256 bptMintAmt = computeAddLiquidityUnbalanced(exactAmounts, swapFeePercentage, useStablePool); - - emit Debug("BPT burned while removing liq:", bptBurnAmt); - emit Debug("BPT minted while adding the same liq:", bptMintAmt); - bptProfit = bptMintAmt - bptBurnAmt; - if (ASSERT_MODE) { - assert(bptProfit <= 0); - } - } - - function computeAddAndRemoveAddLiquidityMultiToken( - uint256 bptMintAmt, - uint256 swapFeePercentage, - bool useStablePool - ) public { - bptMintAmt = boundBptMint(useStablePool, bptMintAmt); - - // mint exactly bptMintAmt to deposit tokenAmts - uint256[] memory tokenAmts = computeProportionalAmountsIn(bptMintAmt, useStablePool); - - // withdraw exactly tokenAmts to burn bptBurnAmt - // No computeRemoveLiquidityUnbalanced fn available, need to go one at a time to accomplish this - uint256 bptBurnAmt = 0; - for (uint256 i = 0; i < tokenAmts.length; i++) { - bptBurnAmt += computeRemoveLiquiditySingleTokenExactOut(i, tokenAmts[i], swapFeePercentage, useStablePool); - } - - emit Debug("BPT minted while adding liquidity:", bptMintAmt); - emit Debug("BPT burned while removing same liquidity:", bptBurnAmt); - bptProfit = bptMintAmt - bptBurnAmt; - if (ASSERT_MODE) { - assert(bptProfit <= 0); - } - } - - function computeRemoveAndAddLiquidityMultiToken( - uint256 bptBurnAmt, - uint256 swapFeePercentage, - bool useStablePool - ) public { - bptBurnAmt = boundBptBurn(useStablePool, bptBurnAmt); - - // burn exactly bptBurnAmt to withdraw tokenAmts - uint256[] memory tokenAmts = computeProportionalAmountsOut(bptBurnAmt, useStablePool); - - // deposit exactly tokenAmts to mint bptMintAmt - uint256 bptMintAmt = computeAddLiquidityUnbalanced(tokenAmts, swapFeePercentage, useStablePool); - - emit Debug("BPT burned while removing liquidity:", bptBurnAmt); - emit Debug("BPT minted while adding the same liquidity:", bptMintAmt); - bptProfit = bptMintAmt - bptBurnAmt; - if (ASSERT_MODE) { - assert(bptProfit <= 0); - } - } - - //////////////////////////////////////// - // Rate Invariants - - function computeProportionalAmountsIn( - uint256 bptAmountOut, - bool useStablePool - ) public returns (uint256[] memory amountsIn) { - assumeValidTradeAmount(bptAmountOut); - bptAmountOut = boundBptMint(useStablePool, bptAmountOut); - (uint256[] memory balances, uint256 bptTotalSupply) = loadState(useStablePool); - uint256 rateBefore = getBptRate(getPool(useStablePool), balances, bptTotalSupply); - - amountsIn = BasePoolMath.computeProportionalAmountsIn(balances, bptTotalSupply, bptAmountOut); - - uint256[] memory balancesAfter = sumBalances(balances, amountsIn); - uint256 rateAfter = getBptRate(getPool(useStablePool), balancesAfter, bptTotalSupply + bptAmountOut); - updateState(useStablePool, balancesAfter, 0, bptAmountOut); - rateDecrease = rateBefore - rateAfter; - if (ASSERT_MODE) { - assert(rateDecrease <= 0); - } - } - - function computeProportionalAmountsOut( - uint256 bptAmountIn, - bool useStablePool - ) public returns (uint256[] memory amountsOut) { - assumeValidTradeAmount(bptAmountIn); - bptAmountIn = boundBptBurn(useStablePool, bptAmountIn); - (uint256[] memory balances, uint256 bptTotalSupply) = loadState(useStablePool); - uint256 rateBefore = getBptRate(getPool(useStablePool), balances, bptTotalSupply); - - amountsOut = BasePoolMath.computeProportionalAmountsOut(balances, bptTotalSupply, bptAmountIn); - - uint256[] memory balancesAfter = subBalances(balances, amountsOut); - uint256 rateAfter = getBptRate(getPool(useStablePool), balancesAfter, bptTotalSupply - bptAmountIn); - updateState(useStablePool, balancesAfter, bptAmountIn, 0); - rateDecrease = rateBefore - rateAfter; - if (ASSERT_MODE) { - assert(rateDecrease <= 0); - } - } - - function computeAddLiquidityUnbalanced( - uint256[] memory exactAmounts, - uint256 swapFeePercentage, - bool useStablePool - ) public returns (uint256 bptAmountOut) { - exactAmounts = boundBalanceLength(exactAmounts, useStablePool); - (uint256[] memory balances, uint256 bptTotalSupply) = loadState(useStablePool); - for (uint256 i = 0; i < exactAmounts.length; i++) { - exactAmounts[i] = boundTokenDeposit(useStablePool, exactAmounts[i], i); - } - IBasePool pool = getPool(useStablePool); - uint256 rateBefore = getBptRate(pool, balances, bptTotalSupply); - - (uint256 amountOut, ) = BasePoolMath.computeAddLiquidityUnbalanced( - balances, - exactAmounts, - bptTotalSupply, - swapFeePercentage, - pool - ); - - bptAmountOut = amountOut; - uint256[] memory balancesAfter = sumBalances(balances, exactAmounts); - uint256 rateAfter = getBptRate(pool, balancesAfter, bptTotalSupply + amountOut); - updateState(useStablePool, balancesAfter, 0, amountOut); - rateDecrease = rateBefore - rateAfter; - if (ASSERT_MODE) { - assert(rateDecrease <= 0); - } - } - - function computeAddLiquiditySingleTokenExactOut( - uint256 tokenInIndex, - uint256 exactBptAmountOut, - uint256 swapFeePercentage, - bool useStablePool - ) public returns (uint256 tokenAmountIn) { - assumeValidTradeAmount(exactBptAmountOut); - tokenInIndex = boundTokenIndex(useStablePool, tokenInIndex); - exactBptAmountOut = boundBptMint(useStablePool, exactBptAmountOut); - (uint256[] memory balances, uint256 bptTotalSupply) = loadState(useStablePool); - boundBalanceLength(balances, useStablePool); - uint256 rateBefore = getBptRate(getPool(useStablePool), balances, bptTotalSupply); - - (uint256 amountInMinusFee, uint256[] memory fees) = BasePoolMath.computeAddLiquiditySingleTokenExactOut( - balances, - tokenInIndex, - exactBptAmountOut, - bptTotalSupply, - swapFeePercentage, - getPool(useStablePool) - ); - - tokenAmountIn = amountInMinusFee; - balances[tokenInIndex] += (amountInMinusFee + fees[tokenInIndex]); - uint256 rateAfter = getBptRate(getPool(useStablePool), balances, bptTotalSupply + exactBptAmountOut); - updateState(useStablePool, balances, 0, exactBptAmountOut); - rateDecrease = rateBefore - rateAfter; - if (ASSERT_MODE) { - assert(rateDecrease <= 0); - } - } - - function computeRemoveLiquiditySingleTokenExactOut( - uint256 tokenOutIndex, - uint256 exactAmountOut, - uint256 swapFeePercentage, - bool useStablePool - ) public returns (uint256 bptAmountIn) { - assumeValidTradeAmount(exactAmountOut); - tokenOutIndex = boundTokenIndex(useStablePool, tokenOutIndex); - exactAmountOut = boundTokenWithdraw(useStablePool, exactAmountOut, tokenOutIndex); - (uint256[] memory balances, uint256 bptTotalSupply) = loadState(useStablePool); - uint256 rateBefore = getBptRate(getPool(useStablePool), balances, bptTotalSupply); - - (uint256 _bptAmountIn, uint256[] memory fees) = BasePoolMath.computeRemoveLiquiditySingleTokenExactOut( - balances, - tokenOutIndex, - exactAmountOut, - bptTotalSupply, - swapFeePercentage, - getPool(useStablePool) - ); - bptAmountIn = _bptAmountIn; - - balances[tokenOutIndex] -= (exactAmountOut + fees[tokenOutIndex]); - uint256 rateAfter = getBptRate(getPool(useStablePool), balances, bptTotalSupply - bptAmountIn); - updateState(useStablePool, balances, bptAmountIn, 0); - rateDecrease = rateBefore - rateAfter; - if (ASSERT_MODE) { - assert(rateDecrease <= 0); - } - } - - function computeRemoveLiquiditySingleTokenExactIn( - uint256 tokenOutIndex, - uint256 exactBptAmountIn, - uint256 swapFeePercentage, - bool useStablePool - ) public returns (uint256 bptAmountOut) { - assumeValidTradeAmount(exactBptAmountIn); - tokenOutIndex = boundTokenIndex(useStablePool, tokenOutIndex); - exactBptAmountIn = boundBptBurn(useStablePool, exactBptAmountIn); - (uint256[] memory balances, uint256 bptTotalSupply) = loadState(useStablePool); - uint256 rateBefore = getBptRate(getPool(useStablePool), balances, bptTotalSupply); - - (uint256 amountOutMinusFee, ) = BasePoolMath.computeRemoveLiquiditySingleTokenExactIn( - balances, - tokenOutIndex, - exactBptAmountIn, - bptTotalSupply, - swapFeePercentage, - getPool(useStablePool) - ); - - bptAmountOut = amountOutMinusFee; - balances[tokenOutIndex] -= amountOutMinusFee; // fees already accounted for - uint256 rateAfter = getBptRate(getPool(useStablePool), balances, bptTotalSupply - exactBptAmountIn); - updateState(useStablePool, balances, exactBptAmountIn, 0); - rateDecrease = rateBefore - rateAfter; - if (ASSERT_MODE) { - assert(rateDecrease <= 0); - } - } - - //////////////////////////////////////// - // Helpers - - function mockInitialize(IBasePool pool, uint256[] memory balances) private view returns (uint256) { - uint256 invariant = pool.computeInvariant(balances, Rounding.ROUND_DOWN); - if (invariant < 1e6) revert(); - return invariant; - } - - function loadState(bool useStablePool) private returns (uint256[] memory balances, uint256 bptTotalSupply) { - balances = useStablePool ? stableBalanceLive : weightedBalanceLive; - bptTotalSupply = useStablePool ? stableBPTSupply : weightedBPTSupply; - } - - function updateState( - bool useStablePool, - uint256[] memory balances, - uint256 bptAmountIn, - uint256 bptAmountOut - ) private { - if (useStablePool) { - stableBalanceLive = balances; - stableBPTSupply -= bptAmountIn; - stableBPTSupply += bptAmountOut; - require(stableBPTSupply >= _POOL_MINIMUM_TOTAL_SUPPLY); - } else { - weightedBalanceLive = balances; - weightedBPTSupply -= bptAmountIn; - weightedBPTSupply += bptAmountOut; - require(weightedBPTSupply >= _POOL_MINIMUM_TOTAL_SUPPLY); - } - } - - function getBptRate(IBasePool pool, uint256[] memory balances, uint256 bptTotalSupply) private returns (uint256) { - uint256 invariant = pool.computeInvariant(balances, Rounding.ROUND_DOWN); - return invariant.divDown(bptTotalSupply); - } - - function getPool(bool useStablePool) private view returns (IBasePool) { - return useStablePool ? stablePool : weightedPool; - } - - function getBalancesLength(bool useStablePool) private view returns (uint256 length) { - length = useStablePool ? stableBalanceLive.length : weightedBalanceLive.length; - } - - function boundTokenIndex(bool useStablePool, uint256 tokenIndex) private view returns (uint256 boundedIndex) { - uint256 len = getBalancesLength(useStablePool); - boundedIndex = bound(tokenIndex, 0, len - 1); - } - - function boundTokenDeposit( - bool useStablePool, - uint256 tokenAmt, - uint256 tokenIndex - ) private view returns (uint256 boundedAmt) { - uint256[] memory balances = useStablePool ? stableBalanceLive : weightedBalanceLive; - boundedAmt = bound(tokenAmt, 0, _MAX_BALANCE - balances[tokenIndex]); - } - - function boundTokenWithdraw( - bool useStablePool, - uint256 tokenAmt, - uint256 tokenIndex - ) private view returns (uint256 boundedAmt) { - uint256[] memory balances = useStablePool ? stableBalanceLive : weightedBalanceLive; - boundedAmt = bound(tokenAmt, 0, balances[tokenIndex]); - } - - function boundBptMint(bool useStablePool, uint256 bptAmt) private view returns (uint256 boundedAmt) { - uint256 totalSupply = useStablePool ? stableBPTSupply : weightedBPTSupply; - boundedAmt = bound(bptAmt, 0, _MAX_BALANCE - totalSupply); - } - - function boundBptBurn(bool useStablePool, uint256 bptAmt) private view returns (uint256 boundedAmt) { - uint256 totalSupply = useStablePool ? stableBPTSupply : weightedBPTSupply; - boundedAmt = bound(bptAmt, 0, totalSupply); - } - - function boundBalanceLength(uint256[] memory balances, bool isStablePool) private pure returns (uint256[] memory) { - if (!isStablePool) { - if (balances.length < 3) revert(); - assembly { - mstore(balances, 3) - } - return balances; - } else { - if (balances.length < 2) revert(); - uint256 numTokens = bound(balances.length, 2, 8); - assembly { - mstore(balances, numTokens) - } - return balances; - } - } - - function sumBalances(uint256[] memory balances, uint256[] memory amounts) private pure returns (uint256[] memory) { - require(amounts.length == balances.length); - uint256[] memory newBalances = new uint256[](balances.length); - for (uint256 i = 0; i < balances.length; i++) { - newBalances[i] = balances[i] + amounts[i]; - } - return newBalances; - } - - function subBalances(uint256[] memory balances, uint256[] memory amounts) private pure returns (uint256[] memory) { - require(amounts.length == balances.length); - uint256[] memory newBalances = new uint256[](balances.length); - for (uint256 i = 0; i < balances.length; i++) { - newBalances[i] = balances[i] - amounts[i]; - } - return newBalances; - } - - function assumeValidBalanceLength(uint256[] memory balances) private pure { - if (balances.length < 2 || balances.length > 8) revert(); - } - - function assumeValidTradeAmount(uint256 tradeAmount) private pure { - if (tradeAmount != 0 && tradeAmount < _MINIMUM_TRADE_AMOUNT) { - revert(); - } - } -} diff --git a/pkg/vault/test/foundry/utils/BaseMedusaTest.sol b/pkg/vault/test/foundry/utils/BaseMedusaTest.sol index ae31bf298..d0884290f 100644 --- a/pkg/vault/test/foundry/utils/BaseMedusaTest.sol +++ b/pkg/vault/test/foundry/utils/BaseMedusaTest.sol @@ -2,6 +2,8 @@ pragma solidity ^0.8.24; +import "forge-std/Test.sol"; + import { IPermit2 } from "permit2/src/interfaces/IPermit2.sol"; import { DeployPermit2 } from "permit2/test/utils/DeployPermit2.sol"; @@ -14,6 +16,7 @@ import { IVault } from "@balancer-labs/v3-interfaces/contracts/vault/IVault.sol" import { IStdMedusaCheats } from "@balancer-labs/v3-interfaces/contracts/test/IStdMedusaCheats.sol"; import "@balancer-labs/v3-interfaces/contracts/vault/VaultTypes.sol"; +import { InputHelpers } from "@balancer-labs/v3-solidity-utils/contracts/helpers/InputHelpers.sol"; import { CREATE3 } from "@balancer-labs/v3-solidity-utils/contracts/solmate/CREATE3.sol"; import { ERC20TestToken } from "@balancer-labs/v3-solidity-utils/contracts/test/ERC20TestToken.sol"; import { WETHTestToken } from "@balancer-labs/v3-solidity-utils/contracts/test/WETHTestToken.sol"; @@ -29,9 +32,11 @@ import { CompositeLiquidityRouterMock } from "../../../contracts/test/CompositeL import { ProtocolFeeControllerMock } from "../../../contracts/test/ProtocolFeeControllerMock.sol"; import { PoolFactoryMock } from "../../../contracts/test/PoolFactoryMock.sol"; -contract BaseMedusaTest { +contract BaseMedusaTest is Test { IStdMedusaCheats internal medusa = IStdMedusaCheats(0x7109709ECfa91a80626fF3989D68f67F5b1DD12D); + event Debug(string, uint256); + IPermit2 internal permit2; // Main contract mocks. @@ -45,7 +50,10 @@ contract BaseMedusaTest { ProtocolFeeControllerMock internal feeController; PoolFactoryMock internal factoryMock; - uint256 internal constant DEFAULT_USER_BALANCE = 1e9 * 1e18; + IBasePool internal pool; + uint256 internal poolCreationNonce; + + uint256 internal constant DEFAULT_USER_BALANCE = 1e18 * 1e18; // Set alice,bob and lp to addresses of medusa.json "senderAddresses" property address internal alice = address(0x10000); @@ -56,8 +64,6 @@ contract BaseMedusaTest { ERC20TestToken internal usdc; WETHTestToken internal weth; - // Create Permit2 - // Create Vault/Routers/etc // Set permissions for users constructor() { @@ -77,6 +83,43 @@ contract BaseMedusaTest { router = new RouterMock(IVault(address(vault)), weth, permit2); batchRouter = new BatchRouterMock(IVault(address(vault)), weth, permit2); compositeLiquidityRouter = new CompositeLiquidityRouterMock(IVault(address(vault)), weth, permit2); + + _setPermissionsForUsersAndTokens(); + + (IERC20[] memory tokens, uint256[] memory initialBalances) = getTokensAndInitialBalances(); + pool = IBasePool(createPool(tokens, initialBalances)); + + _allowBptTransfers(); + } + + function createPool(IERC20[] memory tokens, uint256[] memory initialBalances) internal virtual returns (address) { + address newPool = factoryMock.createPool("ERC20 Pool", "ERC20POOL"); + + // No hooks contract. + factoryMock.registerTestPool(newPool, vault.buildTokenConfig(tokens), address(0), lp); + + // Initialize liquidity of new pool. + medusa.prank(lp); + router.initialize(address(newPool), tokens, initialBalances, 0, false, bytes("")); + + return newPool; + } + + function getTokensAndInitialBalances() + internal + virtual + returns (IERC20[] memory tokens, uint256[] memory initialBalances) + { + tokens = new IERC20[](3); + tokens[0] = dai; + tokens[1] = usdc; + tokens[2] = weth; + tokens = InputHelpers.sortTokens(tokens); + + initialBalances = new uint256[](3); + initialBalances[0] = 10e18; + initialBalances[1] = 20e18; + initialBalances[2] = 30e18; } function _createERC20TestToken( @@ -118,6 +161,64 @@ contract BaseMedusaTest { vault = IVaultMock(address(newVault)); } + function _allowBptTransfers() private { + address[] memory users = new address[](3); + users[0] = alice; + users[1] = bob; + users[2] = lp; + + for (uint256 j = 0; j < users.length; j++) { + _approveBptTokenForUser(IERC20(address(pool)), users[j]); + } + } + + function _approveBptTokenForUser(IERC20 bptToken, address user) private { + medusa.prank(user); + bptToken.approve(address(router), type(uint256).max); + medusa.prank(user); + bptToken.approve(address(batchRouter), type(uint256).max); + medusa.prank(user); + bptToken.approve(address(compositeLiquidityRouter), type(uint256).max); + + medusa.prank(user); + bptToken.approve(address(permit2), type(uint256).max); + medusa.prank(user); + permit2.approve(address(bptToken), address(router), type(uint160).max, type(uint48).max); + medusa.prank(user); + permit2.approve(address(bptToken), address(batchRouter), type(uint160).max, type(uint48).max); + medusa.prank(user); + permit2.approve(address(bptToken), address(compositeLiquidityRouter), type(uint160).max, type(uint48).max); + } + + function _setPermissionsForUsersAndTokens() private { + address[] memory tokens = new address[](3); + tokens[0] = address(dai); + tokens[1] = address(usdc); + tokens[2] = address(weth); + + address[] memory users = new address[](3); + users[0] = alice; + users[1] = bob; + users[2] = lp; + + for (uint256 i = 0; i < tokens.length; i++) { + for (uint256 j = 0; j < users.length; j++) { + _approveTokenForUser(tokens[i], users[j]); + } + } + } + + function _approveTokenForUser(address token, address user) private { + medusa.prank(user); + IERC20(token).approve(address(permit2), type(uint256).max); + medusa.prank(user); + permit2.approve(token, address(router), type(uint160).max, type(uint48).max); + medusa.prank(user); + permit2.approve(token, address(batchRouter), type(uint160).max, type(uint48).max); + medusa.prank(user); + permit2.approve(token, address(compositeLiquidityRouter), type(uint160).max, type(uint48).max); + } + function _create3(bytes memory constructorArgs, bytes memory bytecode, bytes32 salt) private returns (address) { return CREATE3.deploy(salt, abi.encodePacked(bytecode, constructorArgs), 0); } From 44e71fda6669acf8a70695558f15417ac8784902 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Bruno?= Date: Wed, 9 Oct 2024 10:46:56 -0300 Subject: [PATCH 04/29] Lint --- pkg/vault/test/foundry/fuzz/FuzzHarness.sol | 75 ++++++++++----------- 1 file changed, 37 insertions(+), 38 deletions(-) diff --git a/pkg/vault/test/foundry/fuzz/FuzzHarness.sol b/pkg/vault/test/foundry/fuzz/FuzzHarness.sol index e308d7922..6ea13991c 100644 --- a/pkg/vault/test/foundry/fuzz/FuzzHarness.sol +++ b/pkg/vault/test/foundry/fuzz/FuzzHarness.sol @@ -125,12 +125,18 @@ contract FuzzHarness is Test { // deposit tokenAmt to mint exactly bptMintAmt uint256 tokenAmt = computeAddLiquiditySingleTokenExactOut( - tokenIndex, bptMintAmt, swapFeePercentage, useStablePool + tokenIndex, + bptMintAmt, + swapFeePercentage, + useStablePool ); // withdraw exactly tokenAmt to burn bptBurnAmt uint256 bptBurnAmt = computeRemoveLiquiditySingleTokenExactIn( - tokenIndex, tokenAmt, swapFeePercentage, useStablePool + tokenIndex, + tokenAmt, + swapFeePercentage, + useStablePool ); emit Debug("BPT minted while adding liq:", bptMintAmt); @@ -152,15 +158,16 @@ contract FuzzHarness is Test { // withdraw exactly tokenAmt to burn bptBurnAmt uint256 bptBurnAmt = computeRemoveLiquiditySingleTokenExactOut( - tokenIndex, tokenAmt, swapFeePercentage, useStablePool + tokenIndex, + tokenAmt, + swapFeePercentage, + useStablePool ); // deposit exactly tokenAmt to mint bptMintAmt uint256[] memory exactAmounts = new uint256[](getBalancesLength(useStablePool)); exactAmounts[tokenIndex] = tokenAmt; - uint256 bptMintAmt = computeAddLiquidityUnbalanced( - exactAmounts, swapFeePercentage, useStablePool - ); + uint256 bptMintAmt = computeAddLiquidityUnbalanced(exactAmounts, swapFeePercentage, useStablePool); emit Debug("BPT burned while removing liq:", bptBurnAmt); emit Debug("BPT minted while adding the same liq:", bptMintAmt); @@ -178,17 +185,13 @@ contract FuzzHarness is Test { bptMintAmt = boundBptMint(useStablePool, bptMintAmt); // mint exactly bptMintAmt to deposit tokenAmts - uint256[] memory tokenAmts = computeProportionalAmountsIn( - bptMintAmt, useStablePool - ); + uint256[] memory tokenAmts = computeProportionalAmountsIn(bptMintAmt, useStablePool); // withdraw exactly tokenAmts to burn bptBurnAmt // No computeRemoveLiquidityUnbalanced fn available, need to go one at a time to accomplish this uint256 bptBurnAmt = 0; for (uint256 i = 0; i < tokenAmts.length; i++) { - bptBurnAmt += computeRemoveLiquiditySingleTokenExactOut( - i, tokenAmts[i], swapFeePercentage, useStablePool - ); + bptBurnAmt += computeRemoveLiquiditySingleTokenExactOut(i, tokenAmts[i], swapFeePercentage, useStablePool); } emit Debug("BPT minted while adding liquidity:", bptMintAmt); @@ -226,7 +229,7 @@ contract FuzzHarness is Test { function computeProportionalAmountsIn( uint256 bptAmountOut, bool useStablePool - ) public returns(uint256[] memory amountsIn) { + ) public returns (uint256[] memory amountsIn) { assumeValidTradeAmount(bptAmountOut); bptAmountOut = boundBptMint(useStablePool, bptAmountOut); (uint256[] memory balances, uint256 bptTotalSupply) = loadState(useStablePool); @@ -235,11 +238,7 @@ contract FuzzHarness is Test { amountsIn = BasePoolMath.computeProportionalAmountsIn(balances, bptTotalSupply, bptAmountOut); uint256[] memory balancesAfter = sumBalances(balances, amountsIn); - uint256 rateAfter = getBptRate( - getPool(useStablePool), - balancesAfter, - bptTotalSupply + bptAmountOut - ); + uint256 rateAfter = getBptRate(getPool(useStablePool), balancesAfter, bptTotalSupply + bptAmountOut); updateState(useStablePool, balancesAfter, 0, bptAmountOut); rateDecrease = rateBefore - rateAfter; if (ASSERT_MODE) { @@ -250,7 +249,7 @@ contract FuzzHarness is Test { function computeProportionalAmountsOut( uint256 bptAmountIn, bool useStablePool - ) public returns(uint256[] memory amountsOut) { + ) public returns (uint256[] memory amountsOut) { assumeValidTradeAmount(bptAmountIn); bptAmountIn = boundBptBurn(useStablePool, bptAmountIn); (uint256[] memory balances, uint256 bptTotalSupply) = loadState(useStablePool); @@ -259,11 +258,7 @@ contract FuzzHarness is Test { amountsOut = BasePoolMath.computeProportionalAmountsOut(balances, bptTotalSupply, bptAmountIn); uint256[] memory balancesAfter = subBalances(balances, amountsOut); - uint256 rateAfter = getBptRate( - getPool(useStablePool), - balancesAfter, - bptTotalSupply - bptAmountIn - ); + uint256 rateAfter = getBptRate(getPool(useStablePool), balancesAfter, bptTotalSupply - bptAmountIn); updateState(useStablePool, balancesAfter, bptAmountIn, 0); rateDecrease = rateBefore - rateAfter; if (ASSERT_MODE) { @@ -275,7 +270,7 @@ contract FuzzHarness is Test { uint256[] memory exactAmounts, uint256 swapFeePercentage, bool useStablePool - ) public returns(uint256 bptAmountOut) { + ) public returns (uint256 bptAmountOut) { exactAmounts = boundBalanceLength(exactAmounts, useStablePool); (uint256[] memory balances, uint256 bptTotalSupply) = loadState(useStablePool); for (uint256 i = 0; i < exactAmounts.length; i++) { @@ -284,7 +279,7 @@ contract FuzzHarness is Test { IBasePool pool = getPool(useStablePool); uint256 rateBefore = getBptRate(pool, balances, bptTotalSupply); - (uint256 amountOut,) = BasePoolMath.computeAddLiquidityUnbalanced( + (uint256 amountOut, ) = BasePoolMath.computeAddLiquidityUnbalanced( balances, exactAmounts, bptTotalSupply, @@ -377,7 +372,7 @@ contract FuzzHarness is Test { (uint256[] memory balances, uint256 bptTotalSupply) = loadState(useStablePool); uint256 rateBefore = getBptRate(getPool(useStablePool), balances, bptTotalSupply); - (uint256 amountOutMinusFee,) = BasePoolMath.computeRemoveLiquiditySingleTokenExactIn( + (uint256 amountOutMinusFee, ) = BasePoolMath.computeRemoveLiquiditySingleTokenExactIn( balances, tokenOutIndex, exactBptAmountIn, @@ -429,11 +424,7 @@ contract FuzzHarness is Test { } } - function getBptRate( - IBasePool pool, - uint256[] memory balances, - uint256 bptTotalSupply - ) private returns (uint256) { + function getBptRate(IBasePool pool, uint256[] memory balances, uint256 bptTotalSupply) private returns (uint256) { uint256 invariant = pool.computeInvariant(balances, Rounding.ROUND_DOWN); return invariant.divDown(bptTotalSupply); } @@ -442,31 +433,39 @@ contract FuzzHarness is Test { return useStablePool ? stablePool : weightedPool; } - function getBalancesLength(bool useStablePool) private view returns(uint256 length) { + function getBalancesLength(bool useStablePool) private view returns (uint256 length) { length = useStablePool ? stableBalanceLive.length : weightedBalanceLive.length; } - function boundTokenIndex(bool useStablePool, uint256 tokenIndex) private view returns(uint256 boundedIndex) { + function boundTokenIndex(bool useStablePool, uint256 tokenIndex) private view returns (uint256 boundedIndex) { uint256 len = getBalancesLength(useStablePool); boundedIndex = bound(tokenIndex, 0, len - 1); } - function boundTokenDeposit(bool useStablePool, uint256 tokenAmt, uint256 tokenIndex) private view returns(uint256 boundedAmt) { + function boundTokenDeposit( + bool useStablePool, + uint256 tokenAmt, + uint256 tokenIndex + ) private view returns (uint256 boundedAmt) { uint256[] memory balances = useStablePool ? stableBalanceLive : weightedBalanceLive; boundedAmt = bound(tokenAmt, 0, _MAX_BALANCE - balances[tokenIndex]); } - function boundTokenWithdraw(bool useStablePool, uint256 tokenAmt, uint256 tokenIndex) private view returns(uint256 boundedAmt) { + function boundTokenWithdraw( + bool useStablePool, + uint256 tokenAmt, + uint256 tokenIndex + ) private view returns (uint256 boundedAmt) { uint256[] memory balances = useStablePool ? stableBalanceLive : weightedBalanceLive; boundedAmt = bound(tokenAmt, 0, balances[tokenIndex]); } - function boundBptMint(bool useStablePool, uint256 bptAmt) private view returns(uint256 boundedAmt) { + function boundBptMint(bool useStablePool, uint256 bptAmt) private view returns (uint256 boundedAmt) { uint256 totalSupply = useStablePool ? stableBPTSupply : weightedBPTSupply; boundedAmt = bound(bptAmt, 0, _MAX_BALANCE - totalSupply); } - function boundBptBurn(bool useStablePool, uint256 bptAmt) private view returns(uint256 boundedAmt) { + function boundBptBurn(bool useStablePool, uint256 bptAmt) private view returns (uint256 boundedAmt) { uint256 totalSupply = useStablePool ? stableBPTSupply : weightedBPTSupply; boundedAmt = bound(bptAmt, 0, totalSupply); } From b21e28b3a22f3e00abfad03b3e8008b052d9d3e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Bruno?= Date: Wed, 9 Oct 2024 14:32:57 -0300 Subject: [PATCH 05/29] Fix Medusa tests --- pkg/pool-stable/medusa.json | 4 +- pkg/pool-weighted/medusa.json | 4 +- pkg/vault/medusa.json | 4 +- .../fuzz/AddAndRemoveLiquidity.medusa.sol | 288 ++++++---- pkg/vault/test/foundry/fuzz/FuzzHarness.sol | 517 ------------------ 5 files changed, 182 insertions(+), 635 deletions(-) delete mode 100644 pkg/vault/test/foundry/fuzz/FuzzHarness.sol diff --git a/pkg/pool-stable/medusa.json b/pkg/pool-stable/medusa.json index 582fc3554..86f6ee730 100644 --- a/pkg/pool-stable/medusa.json +++ b/pkg/pool-stable/medusa.json @@ -3,9 +3,9 @@ "workers": 10, "workerResetLimit": 50, "timeout": 0, - "testLimit": 10000, + "testLimit": 100000, "shrinkLimit": 5000, - "callSequenceLength": 100, + "callSequenceLength": 10, "corpusDirectory": "medusa-corpus", "coverageEnabled": true, "targetContracts": ["AddAndRemoveLiquidityStableMedusaTest"], diff --git a/pkg/pool-weighted/medusa.json b/pkg/pool-weighted/medusa.json index b9c045da5..e94c90e11 100644 --- a/pkg/pool-weighted/medusa.json +++ b/pkg/pool-weighted/medusa.json @@ -3,9 +3,9 @@ "workers": 10, "workerResetLimit": 50, "timeout": 0, - "testLimit": 10000, + "testLimit": 100000, "shrinkLimit": 5000, - "callSequenceLength": 100, + "callSequenceLength": 10, "corpusDirectory": "medusa-corpus", "coverageEnabled": true, "targetContracts": ["AddAndRemoveLiquidityWeightedMedusaTest"], diff --git a/pkg/vault/medusa.json b/pkg/vault/medusa.json index 48b0149f2..83e939589 100644 --- a/pkg/vault/medusa.json +++ b/pkg/vault/medusa.json @@ -3,9 +3,9 @@ "workers": 10, "workerResetLimit": 50, "timeout": 0, - "testLimit": 10000, + "testLimit": 100000, "shrinkLimit": 5000, - "callSequenceLength": 100, + "callSequenceLength": 10, "corpusDirectory": "medusa-corpus", "coverageEnabled": true, "targetContracts": ["AddAndRemoveLiquidityMedusaTest"], diff --git a/pkg/vault/test/foundry/fuzz/AddAndRemoveLiquidity.medusa.sol b/pkg/vault/test/foundry/fuzz/AddAndRemoveLiquidity.medusa.sol index d18f6ba59..aa2ab60a8 100644 --- a/pkg/vault/test/foundry/fuzz/AddAndRemoveLiquidity.medusa.sol +++ b/pkg/vault/test/foundry/fuzz/AddAndRemoveLiquidity.medusa.sol @@ -12,12 +12,6 @@ import { Rounding } from "@balancer-labs/v3-interfaces/contracts/vault/VaultType import { FixedPoint } from "@balancer-labs/v3-solidity-utils/contracts/math/FixedPoint.sol"; import { InputHelpers } from "@balancer-labs/v3-solidity-utils/contracts/helpers/InputHelpers.sol"; -import { StablePoolFactory } from "@balancer-labs/v3-pool-stable/contracts/StablePoolFactory.sol"; -import { StablePool } from "@balancer-labs/v3-pool-stable/contracts/StablePool.sol"; - -import { WeightedPool } from "@balancer-labs/v3-pool-weighted/contracts/WeightedPool.sol"; -import { WeightedPoolFactory } from "@balancer-labs/v3-pool-weighted/contracts/WeightedPoolFactory.sol"; - import { BasePoolMath } from "../../../contracts/BasePoolMath.sol"; import { BalancerPoolToken } from "../../../contracts/BalancerPoolToken.sol"; @@ -38,6 +32,7 @@ contract AddAndRemoveLiquidityMedusaTest is BaseMedusaTest { // State var for optimization mode int256 internal rateDecrease = 0; + int256 internal bptProfit = 0; constructor() BaseMedusaTest() { initialRate = getBptRate(); @@ -50,117 +45,166 @@ contract AddAndRemoveLiquidityMedusaTest is BaseMedusaTest { return rateDecrease; } - // function optimize_bptProfit() public view returns (int256) { - // return int256(bptProfit); - // } + function optimize_bptProfit() public view returns (int256) { + return int256(bptProfit); + } //////////////////////////////////////// // Symmetrical Add/Remove Liquidity - // function computeAddAndRemoveLiquiditySingleToken( - // uint256 tokenIndex, - // uint256 bptMintAmt, - // uint256 swapFeePercentage - // ) public { - // tokenIndex = boundTokenIndex(tokenIndex); - // bptMintAmt = boundBptMint(bptMintAmt); - // - // // deposit tokenAmt to mint exactly bptMintAmt - // uint256 tokenAmt = computeAddLiquiditySingleTokenExactOut( - // tokenIndex, - // bptMintAmt, - // swapFeePercentage - // ); - // - // // withdraw exactly tokenAmt to burn bptBurnAmt - // uint256 bptBurnAmt = computeRemoveLiquiditySingleTokenExactIn( - // tokenIndex, - // tokenAmt, - // swapFeePercentage - // ); - // - // emit Debug("BPT minted while adding liq:", bptMintAmt); - // emit Debug("BPT burned while removing the same liq:", bptBurnAmt); - // bptProfit = bptMintAmt - bptBurnAmt; - // if (ASSERT_MODE) { - // assert(bptProfit <= 0); - // } - // } - - // function computeRemoveAndAddLiquiditySingleToken( - // uint256 tokenIndex, - // uint256 tokenAmt, - // uint256 swapFeePercentage - // ) public { - // tokenIndex = boundTokenIndex(tokenIndex); - // tokenAmt = boundTokenWithdraw(tokenAmt, tokenIndex); - // - // // withdraw exactly tokenAmt to burn bptBurnAmt - // uint256 bptBurnAmt = computeRemoveLiquiditySingleTokenExactOut( - // tokenIndex, - // tokenAmt, - // swapFeePercentage - // ); - // - // // deposit exactly tokenAmt to mint bptMintAmt - // uint256[] memory exactAmounts = new uint256[](getBalancesLength()); - // exactAmounts[tokenIndex] = tokenAmt; - // uint256 bptMintAmt = computeAddLiquidityUnbalanced(exactAmounts, swapFeePercentage); - // - // emit Debug("BPT burned while removing liq:", bptBurnAmt); - // emit Debug("BPT minted while adding the same liq:", bptMintAmt); - // bptProfit = bptMintAmt - bptBurnAmt; - // if (ASSERT_MODE) { - // assert(bptProfit <= 0); - // } - // } - // - // function computeAddAndRemoveAddLiquidityMultiToken( - // uint256 bptMintAmt, - // uint256 swapFeePercentage - // ) public { - // bptMintAmt = boundBptMint(bptMintAmt); - // - // // mint exactly bptMintAmt to deposit tokenAmts - // uint256[] memory tokenAmts = computeProportionalAmountsIn(bptMintAmt); - // - // // withdraw exactly tokenAmts to burn bptBurnAmt - // // No computeRemoveLiquidityUnbalanced fn available, need to go one at a time to accomplish this - // uint256 bptBurnAmt = 0; - // for (uint256 i = 0; i < tokenAmts.length; i++) { - // bptBurnAmt += computeRemoveLiquiditySingleTokenExactOut(i, tokenAmts[i], swapFeePercentage); - // } - // - // emit Debug("BPT minted while adding liquidity:", bptMintAmt); - // emit Debug("BPT burned while removing same liquidity:", bptBurnAmt); - // bptProfit = bptMintAmt - bptBurnAmt; - // if (ASSERT_MODE) { - // assert(bptProfit <= 0); - // } - // } - // - // function computeRemoveAndAddLiquidityMultiToken( - // uint256 bptBurnAmt, - // uint256 swapFeePercentage - // ) public { - // bptBurnAmt = boundBptBurn(bptBurnAmt); - // - // // burn exactly bptBurnAmt to withdraw tokenAmts - // uint256[] memory tokenAmts = computeProportionalAmountsOut(bptBurnAmt); - // - // // deposit exactly tokenAmts to mint bptMintAmt - // uint256 bptMintAmt = computeAddLiquidityUnbalanced(tokenAmts, swapFeePercentage); - // - // emit Debug("BPT burned while removing liquidity:", bptBurnAmt); - // emit Debug("BPT minted while adding the same liquidity:", bptMintAmt); - // bptProfit = bptMintAmt - bptBurnAmt; - // if (ASSERT_MODE) { - // assert(bptProfit <= 0); - // } - // } - // + function computeAddAndRemoveLiquiditySingleToken( + uint256 tokenIndex, + uint256 exactBptAmountOut, + uint256 swapFeePercentage + ) public { + // Fee % between 0% and 100% + swapFeePercentage = bound(swapFeePercentage, 0, 1e18); + vault.manualSetStaticSwapFeePercentage(address(pool), swapFeePercentage); + + tokenIndex = boundTokenIndex(tokenIndex); + exactBptAmountOut = boundBptMint(exactBptAmountOut); + + (IERC20[] memory tokens, , , ) = vault.getPoolTokenInfo(address(pool)); + + // deposit tokenAmt to mint exactly bptMintAmt + medusa.prank(lp); + uint256 tokenAmountIn = router.addLiquiditySingleTokenExactOut( + address(pool), + tokens[tokenIndex], + type(uint128).max, + exactBptAmountOut, + false, + bytes("") + ); + + // withdraw exactly tokenAmountIn to burn bptAmountIn + medusa.prank(lp); + uint256 bptAmountIn = router.removeLiquiditySingleTokenExactOut( + address(pool), + type(uint128).max, + tokens[tokenIndex], + tokenAmountIn, + false, + bytes("") + ); + + emit Debug("BPT minted while adding liquidity:", exactBptAmountOut); + emit Debug("BPT burned while removing the same liquidity:", bptAmountIn); + bptProfit += int256(exactBptAmountOut) - int256(bptAmountIn); + } + + function computeRemoveAndAddLiquiditySingleToken( + uint256 tokenIndex, + uint256 tokenAmountOut, + uint256 swapFeePercentage + ) public { + // Fee % between 0% and 100% + swapFeePercentage = bound(swapFeePercentage, 0, 1e18); + vault.manualSetStaticSwapFeePercentage(address(pool), swapFeePercentage); + + tokenIndex = boundTokenIndex(tokenIndex); + tokenAmountOut = boundTokenAmountOut(tokenAmountOut, tokenIndex); + + (IERC20[] memory tokens, , , ) = vault.getPoolTokenInfo(address(pool)); + + // withdraw exactly tokenAmountOut to burn bptBurnAmt + medusa.prank(lp); + uint256 bptAmountIn = router.removeLiquiditySingleTokenExactOut( + address(pool), + type(uint128).max, + tokens[tokenIndex], + tokenAmountOut, + false, + bytes("") + ); + + // deposit exactly tokenAmountOut to mint bptMintAmt + uint256[] memory exactAmountsIn = new uint256[](getBalancesLength()); + exactAmountsIn[tokenIndex] = tokenAmountOut; + + medusa.prank(lp); + uint256 bptAmountOut = router.addLiquidityUnbalanced(address(pool), exactAmountsIn, 0, false, bytes("")); + + emit Debug("BPT burned while removing liquidity:", bptAmountIn); + emit Debug("BPT minted while adding the same liquidity:", bptAmountOut); + bptProfit += int256(bptAmountOut) - int256(bptAmountIn); + } + + function computeAddAndRemoveLiquidityMultiToken(uint256 exactBptAmountOut, uint256 swapFeePercentage) public { + // Fee % between 0% and 100% + swapFeePercentage = bound(swapFeePercentage, 0, 1e18); + vault.manualSetStaticSwapFeePercentage(address(pool), swapFeePercentage); + + (IERC20[] memory tokens, , , ) = vault.getPoolTokenInfo(address(pool)); + + exactBptAmountOut = boundBptMint(exactBptAmountOut); + + uint256[] memory maxAmountsIn = getMaxAmountsIn(); + + // mint exactly bptAmountOut to deposit tokenAmountIn + medusa.prank(lp); + uint256[] memory tokenAmountsIn = router.addLiquidityProportional( + address(pool), + maxAmountsIn, + exactBptAmountOut, + false, + bytes("") + ); + + // Withdraw exactly tokenAmountsIn to burn bptAmountIn. The function `removeLiquidityUnbalanced` does not + // exist, so we need to go one token at a time to accomplish this. + uint256 bptAmountIn = 0; + for (uint256 i = 0; i < tokenAmountsIn.length; i++) { + medusa.prank(lp); + bptAmountIn += router.removeLiquiditySingleTokenExactOut( + address(pool), + type(uint128).max, + tokens[i], + tokenAmountsIn[i], + false, + bytes("") + ); + } + + emit Debug("BPT minted while adding liquidity:", exactBptAmountOut); + emit Debug("BPT burned while removing same liquidity:", bptAmountIn); + bptProfit += int256(exactBptAmountOut) - int256(bptAmountIn); + } + + function computeRemoveAndAddLiquidityMultiToken(uint256 exactBptAmountIn, uint256 swapFeePercentage) public { + // Fee % between 0% and 100% + swapFeePercentage = bound(swapFeePercentage, 0, 1e18); + vault.manualSetStaticSwapFeePercentage(address(pool), swapFeePercentage); + + exactBptAmountIn = boundBptBurn(exactBptAmountIn); + + uint256[] memory minAmountsOut = getMinAmountsOut(); + + // Burn exactly exactBptAmountIn to withdraw tokenAmountsOut. + medusa.prank(lp); + uint256[] memory tokenAmountsOut = router.removeLiquidityProportional( + address(pool), + exactBptAmountIn, + minAmountsOut, + false, + bytes("") + ); + + // Deposit exactly tokenAmountsOut to mint bptAmountOut. + medusa.prank(lp); + uint256 bptAmountOut = router.addLiquidityUnbalanced(address(pool), tokenAmountsOut, 0, false, bytes("")); + + emit Debug("BPT burned while removing liquidity:", exactBptAmountIn); + emit Debug("BPT minted while adding the same liquidity:", bptAmountOut); + bptProfit += int256(bptAmountOut) - int256(exactBptAmountIn); + } + + function property_no_bpt_profit() public returns (bool) { + return assertBptProfit(pool); + } + //////////////////////////////////////// - // Rate Invariants + // Simple operations function computeProportionalAmountsIn(uint256 bptAmountOut) public returns (uint256[] memory amountsIn) { assumeValidTradeAmount(bptAmountOut); @@ -188,6 +232,10 @@ contract AddAndRemoveLiquidityMedusaTest is BaseMedusaTest { uint256[] memory exactAmountsIn, uint256 swapFeePercentage ) public returns (uint256 bptAmountOut) { + // Fee % between 0% and 100% + swapFeePercentage = bound(swapFeePercentage, 0, 1e18); + vault.manualSetStaticSwapFeePercentage(address(pool), swapFeePercentage); + exactAmountsIn = boundBalanceLength(exactAmountsIn); for (uint256 i = 0; i < exactAmountsIn.length; i++) { exactAmountsIn[i] = boundTokenDeposit(exactAmountsIn[i], i); @@ -204,6 +252,10 @@ contract AddAndRemoveLiquidityMedusaTest is BaseMedusaTest { uint256 exactBptAmountOut, uint256 swapFeePercentage ) public returns (uint256 amountIn) { + // Fee % between 0% and 100% + swapFeePercentage = bound(swapFeePercentage, 0, 1e18); + vault.manualSetStaticSwapFeePercentage(address(pool), swapFeePercentage); + assumeValidTradeAmount(exactBptAmountOut); tokenInIndex = boundTokenIndex(tokenInIndex); exactBptAmountOut = boundBptMint(exactBptAmountOut); @@ -228,6 +280,10 @@ contract AddAndRemoveLiquidityMedusaTest is BaseMedusaTest { uint256 exactAmountOut, uint256 swapFeePercentage ) public returns (uint256 bptAmountIn) { + // Fee % between 0% and 100% + swapFeePercentage = bound(swapFeePercentage, 0, 1e18); + vault.manualSetStaticSwapFeePercentage(address(pool), swapFeePercentage); + assumeValidTradeAmount(exactAmountOut); tokenOutIndex = boundTokenIndex(tokenOutIndex); exactAmountOut = boundTokenAmountOut(exactAmountOut, tokenOutIndex); @@ -252,6 +308,10 @@ contract AddAndRemoveLiquidityMedusaTest is BaseMedusaTest { uint256 exactBptAmountIn, uint256 swapFeePercentage ) public returns (uint256 amountOut) { + // Fee % between 0% and 100% + swapFeePercentage = bound(swapFeePercentage, 0, 1e18); + vault.manualSetStaticSwapFeePercentage(address(pool), swapFeePercentage); + assumeValidTradeAmount(exactBptAmountIn); tokenOutIndex = boundTokenIndex(tokenOutIndex); exactBptAmountIn = boundBptBurn(exactBptAmountIn); @@ -278,6 +338,10 @@ contract AddAndRemoveLiquidityMedusaTest is BaseMedusaTest { //////////////////////////////////////// // Helpers (private functions, so they're not fuzzed) + function assertBptProfit(IBasePool pool) internal returns (bool) { + return bptProfit <= 0; + } + function assertRate(IBasePool pool) internal returns (bool) { updateRateDecrease(); return rateDecrease <= int256(maxRateTolerance); diff --git a/pkg/vault/test/foundry/fuzz/FuzzHarness.sol b/pkg/vault/test/foundry/fuzz/FuzzHarness.sol deleted file mode 100644 index 6ea13991c..000000000 --- a/pkg/vault/test/foundry/fuzz/FuzzHarness.sol +++ /dev/null @@ -1,517 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity ^0.8.24; - -import "forge-std/Test.sol"; - -import { FixedPoint } from "@balancer-labs/v3-solidity-utils/contracts/math/FixedPoint.sol"; -import { IBasePool } from "@balancer-labs/v3-interfaces/contracts/vault/IBasePool.sol"; -import { IVaultMock } from "@balancer-labs/v3-interfaces/contracts/test/IVaultMock.sol"; -import { Rounding } from "@balancer-labs/v3-interfaces/contracts/vault/VaultTypes.sol"; -import { StablePool } from "@balancer-labs/v3-pool-stable/contracts/StablePool.sol"; -import { WeightedPool } from "@balancer-labs/v3-pool-weighted/contracts/WeightedPool.sol"; - -import { VaultMockDeployer } from "../utils/VaultMockDeployer.sol"; -import { BasePoolMath } from "../../../contracts/BasePoolMath.sol"; - -contract FuzzHarness is Test { - using FixedPoint for uint256; - - event Debug(string, uint256); - - // If this flag is false, assertion tests will never fail! But optimizations will work. - // Changing to false will disable optimizations but allow the assertion tests to run - bool private constant ASSERT_MODE = false; - - uint256 private constant _MAX_BALANCE = 2 ** (128) - 1; // from PackedTokenBalance.sol - uint256 private constant _MIN_WEIGHT = 1e16; // 1% (from WeightedPool.sol) - uint256 private constant _MINIMUM_TRADE_AMOUNT = 1e6; - uint256 private constant _POOL_MINIMUM_TOTAL_SUPPLY = 1e6; - - IVaultMock private vault; - IBasePool private stablePool; - IBasePool private weightedPool; - - // State management - uint256[] weightedBalanceLive; - uint256[] stableBalanceLive; - uint256 weightedBPTSupply; - uint256 stableBPTSupply; - - // State vars for optimization mode - uint256 rateDecrease = 0; - uint256 bptProfit = 0; - - constructor() { - if (address(vault) == address(0)) { - uint256 vaultMockMinTradeAmount = 0; - uint256 vaultMockMinWrapAmount = 0; - vault = IVaultMock(address(VaultMockDeployer.deploy(vaultMockMinTradeAmount, vaultMockMinWrapAmount))); - } - uint256[] memory initialBalances = new uint256[](3); - initialBalances[0] = 10e18; - initialBalances[1] = 20e18; - initialBalances[2] = 30e18; - createNewWeightedPool(33e16, 33e16, initialBalances); - createNewStablePool(1000, initialBalances); - } - - //////////////////////////////////////// - // Pool Creation - - function createNewWeightedPool(uint256 weight1, uint256 weight2, uint256[] memory initialBalances) private { - uint256[] memory weights = new uint256[](3); - weights[0] = bound(weight1, _MIN_WEIGHT, 98e16); // any weight between min & 100%-2(min) - uint256 remainingWeight = 99e16 - weights[0]; // weights 0 + 1 must <= 100%-min so there's >=1% left for weight 2 - weights[1] = bound(weight2, _MIN_WEIGHT, remainingWeight); - remainingWeight = 100e16 - (weights[0] + weights[1]); - weights[2] = remainingWeight; - WeightedPool.NewPoolParams memory params = WeightedPool.NewPoolParams({ - name: "My Custom Pool", - symbol: "MCP", - numTokens: 3, - normalizedWeights: weights, - version: "1.0.0" - }); - weightedPool = IBasePool(new WeightedPool(params, vault)); - // Initialize liquidity for this new weighted pool - initialBalances = boundBalanceLength(initialBalances, false); - for (uint256 i; i < initialBalances.length; i++) { - if (initialBalances[i] < 1 ether) initialBalances[i] += 1 ether; - weightedBalanceLive.push(initialBalances[0]); - initialBalances[i] = initialBalances[0]; - } - weightedBPTSupply += mockInitialize(weightedPool, initialBalances); - } - - function createNewStablePool(uint256 amplificationParameter, uint256[] memory initialBalances) private { - StablePool.NewPoolParams memory params = StablePool.NewPoolParams({ - name: "My Custom Pool", - symbol: "MCP", - amplificationParameter: bound(amplificationParameter, 1, 5000), - version: "1.0.0" - }); - stablePool = IBasePool(new StablePool(params, vault)); - // Initialize liquidity for this new stable pool - initialBalances = boundBalanceLength(initialBalances, true); - for (uint256 i; i < initialBalances.length; i++) { - if (initialBalances[i] < 1 ether) initialBalances[i] += 1 ether; - stableBalanceLive.push(initialBalances[i]); - } - stableBPTSupply += mockInitialize(stablePool, initialBalances); - } - - //////////////////////////////////////// - // Optimizations - - function optimize_rateDecrease() public view returns (int256) { - return int256(rateDecrease); - } - - function optimize_bptProfit() public view returns (int256) { - return int256(bptProfit); - } - - //////////////////////////////////////// - // Symmetrical Add/Remove Liquidity - - function computeAddAndRemoveLiquiditySingleToken( - uint256 tokenIndex, - uint256 bptMintAmt, - uint256 swapFeePercentage, - bool useStablePool - ) public { - tokenIndex = boundTokenIndex(useStablePool, tokenIndex); - bptMintAmt = boundBptMint(useStablePool, bptMintAmt); - - // deposit tokenAmt to mint exactly bptMintAmt - uint256 tokenAmt = computeAddLiquiditySingleTokenExactOut( - tokenIndex, - bptMintAmt, - swapFeePercentage, - useStablePool - ); - - // withdraw exactly tokenAmt to burn bptBurnAmt - uint256 bptBurnAmt = computeRemoveLiquiditySingleTokenExactIn( - tokenIndex, - tokenAmt, - swapFeePercentage, - useStablePool - ); - - emit Debug("BPT minted while adding liq:", bptMintAmt); - emit Debug("BPT burned while removing the same liq:", bptBurnAmt); - bptProfit = bptMintAmt - bptBurnAmt; - if (ASSERT_MODE) { - assert(bptProfit <= 0); - } - } - - function computeRemoveAndAddLiquiditySingleToken( - uint256 tokenIndex, - uint256 tokenAmt, - uint256 swapFeePercentage, - bool useStablePool - ) public { - tokenIndex = boundTokenIndex(useStablePool, tokenIndex); - tokenAmt = boundTokenWithdraw(useStablePool, tokenAmt, tokenIndex); - - // withdraw exactly tokenAmt to burn bptBurnAmt - uint256 bptBurnAmt = computeRemoveLiquiditySingleTokenExactOut( - tokenIndex, - tokenAmt, - swapFeePercentage, - useStablePool - ); - - // deposit exactly tokenAmt to mint bptMintAmt - uint256[] memory exactAmounts = new uint256[](getBalancesLength(useStablePool)); - exactAmounts[tokenIndex] = tokenAmt; - uint256 bptMintAmt = computeAddLiquidityUnbalanced(exactAmounts, swapFeePercentage, useStablePool); - - emit Debug("BPT burned while removing liq:", bptBurnAmt); - emit Debug("BPT minted while adding the same liq:", bptMintAmt); - bptProfit = bptMintAmt - bptBurnAmt; - if (ASSERT_MODE) { - assert(bptProfit <= 0); - } - } - - function computeAddAndRemoveAddLiquidityMultiToken( - uint256 bptMintAmt, - uint256 swapFeePercentage, - bool useStablePool - ) public { - bptMintAmt = boundBptMint(useStablePool, bptMintAmt); - - // mint exactly bptMintAmt to deposit tokenAmts - uint256[] memory tokenAmts = computeProportionalAmountsIn(bptMintAmt, useStablePool); - - // withdraw exactly tokenAmts to burn bptBurnAmt - // No computeRemoveLiquidityUnbalanced fn available, need to go one at a time to accomplish this - uint256 bptBurnAmt = 0; - for (uint256 i = 0; i < tokenAmts.length; i++) { - bptBurnAmt += computeRemoveLiquiditySingleTokenExactOut(i, tokenAmts[i], swapFeePercentage, useStablePool); - } - - emit Debug("BPT minted while adding liquidity:", bptMintAmt); - emit Debug("BPT burned while removing same liquidity:", bptBurnAmt); - bptProfit = bptMintAmt - bptBurnAmt; - if (ASSERT_MODE) { - assert(bptProfit <= 0); - } - } - - function computeRemoveAndAddLiquidityMultiToken( - uint256 bptBurnAmt, - uint256 swapFeePercentage, - bool useStablePool - ) public { - bptBurnAmt = boundBptBurn(useStablePool, bptBurnAmt); - - // burn exactly bptBurnAmt to withdraw tokenAmts - uint256[] memory tokenAmts = computeProportionalAmountsOut(bptBurnAmt, useStablePool); - - // deposit exactly tokenAmts to mint bptMintAmt - uint256 bptMintAmt = computeAddLiquidityUnbalanced(tokenAmts, swapFeePercentage, useStablePool); - - emit Debug("BPT burned while removing liquidity:", bptBurnAmt); - emit Debug("BPT minted while adding the same liquidity:", bptMintAmt); - bptProfit = bptMintAmt - bptBurnAmt; - if (ASSERT_MODE) { - assert(bptProfit <= 0); - } - } - - //////////////////////////////////////// - // Rate Invariants - - function computeProportionalAmountsIn( - uint256 bptAmountOut, - bool useStablePool - ) public returns (uint256[] memory amountsIn) { - assumeValidTradeAmount(bptAmountOut); - bptAmountOut = boundBptMint(useStablePool, bptAmountOut); - (uint256[] memory balances, uint256 bptTotalSupply) = loadState(useStablePool); - uint256 rateBefore = getBptRate(getPool(useStablePool), balances, bptTotalSupply); - - amountsIn = BasePoolMath.computeProportionalAmountsIn(balances, bptTotalSupply, bptAmountOut); - - uint256[] memory balancesAfter = sumBalances(balances, amountsIn); - uint256 rateAfter = getBptRate(getPool(useStablePool), balancesAfter, bptTotalSupply + bptAmountOut); - updateState(useStablePool, balancesAfter, 0, bptAmountOut); - rateDecrease = rateBefore - rateAfter; - if (ASSERT_MODE) { - assert(rateDecrease <= 0); - } - } - - function computeProportionalAmountsOut( - uint256 bptAmountIn, - bool useStablePool - ) public returns (uint256[] memory amountsOut) { - assumeValidTradeAmount(bptAmountIn); - bptAmountIn = boundBptBurn(useStablePool, bptAmountIn); - (uint256[] memory balances, uint256 bptTotalSupply) = loadState(useStablePool); - uint256 rateBefore = getBptRate(getPool(useStablePool), balances, bptTotalSupply); - - amountsOut = BasePoolMath.computeProportionalAmountsOut(balances, bptTotalSupply, bptAmountIn); - - uint256[] memory balancesAfter = subBalances(balances, amountsOut); - uint256 rateAfter = getBptRate(getPool(useStablePool), balancesAfter, bptTotalSupply - bptAmountIn); - updateState(useStablePool, balancesAfter, bptAmountIn, 0); - rateDecrease = rateBefore - rateAfter; - if (ASSERT_MODE) { - assert(rateDecrease <= 0); - } - } - - function computeAddLiquidityUnbalanced( - uint256[] memory exactAmounts, - uint256 swapFeePercentage, - bool useStablePool - ) public returns (uint256 bptAmountOut) { - exactAmounts = boundBalanceLength(exactAmounts, useStablePool); - (uint256[] memory balances, uint256 bptTotalSupply) = loadState(useStablePool); - for (uint256 i = 0; i < exactAmounts.length; i++) { - exactAmounts[i] = boundTokenDeposit(useStablePool, exactAmounts[i], i); - } - IBasePool pool = getPool(useStablePool); - uint256 rateBefore = getBptRate(pool, balances, bptTotalSupply); - - (uint256 amountOut, ) = BasePoolMath.computeAddLiquidityUnbalanced( - balances, - exactAmounts, - bptTotalSupply, - swapFeePercentage, - pool - ); - - bptAmountOut = amountOut; - uint256[] memory balancesAfter = sumBalances(balances, exactAmounts); - uint256 rateAfter = getBptRate(pool, balancesAfter, bptTotalSupply + amountOut); - updateState(useStablePool, balancesAfter, 0, amountOut); - rateDecrease = rateBefore - rateAfter; - if (ASSERT_MODE) { - assert(rateDecrease <= 0); - } - } - - function computeAddLiquiditySingleTokenExactOut( - uint256 tokenInIndex, - uint256 exactBptAmountOut, - uint256 swapFeePercentage, - bool useStablePool - ) public returns (uint256 tokenAmountIn) { - assumeValidTradeAmount(exactBptAmountOut); - tokenInIndex = boundTokenIndex(useStablePool, tokenInIndex); - exactBptAmountOut = boundBptMint(useStablePool, exactBptAmountOut); - (uint256[] memory balances, uint256 bptTotalSupply) = loadState(useStablePool); - boundBalanceLength(balances, useStablePool); - uint256 rateBefore = getBptRate(getPool(useStablePool), balances, bptTotalSupply); - - (uint256 amountInMinusFee, uint256[] memory fees) = BasePoolMath.computeAddLiquiditySingleTokenExactOut( - balances, - tokenInIndex, - exactBptAmountOut, - bptTotalSupply, - swapFeePercentage, - getPool(useStablePool) - ); - - tokenAmountIn = amountInMinusFee; - balances[tokenInIndex] += (amountInMinusFee + fees[tokenInIndex]); - uint256 rateAfter = getBptRate(getPool(useStablePool), balances, bptTotalSupply + exactBptAmountOut); - updateState(useStablePool, balances, 0, exactBptAmountOut); - rateDecrease = rateBefore - rateAfter; - if (ASSERT_MODE) { - assert(rateDecrease <= 0); - } - } - - function computeRemoveLiquiditySingleTokenExactOut( - uint256 tokenOutIndex, - uint256 exactAmountOut, - uint256 swapFeePercentage, - bool useStablePool - ) public returns (uint256 bptAmountIn) { - assumeValidTradeAmount(exactAmountOut); - tokenOutIndex = boundTokenIndex(useStablePool, tokenOutIndex); - exactAmountOut = boundTokenWithdraw(useStablePool, exactAmountOut, tokenOutIndex); - (uint256[] memory balances, uint256 bptTotalSupply) = loadState(useStablePool); - uint256 rateBefore = getBptRate(getPool(useStablePool), balances, bptTotalSupply); - - (uint256 _bptAmountIn, uint256[] memory fees) = BasePoolMath.computeRemoveLiquiditySingleTokenExactOut( - balances, - tokenOutIndex, - exactAmountOut, - bptTotalSupply, - swapFeePercentage, - getPool(useStablePool) - ); - bptAmountIn = _bptAmountIn; - - balances[tokenOutIndex] -= (exactAmountOut + fees[tokenOutIndex]); - uint256 rateAfter = getBptRate(getPool(useStablePool), balances, bptTotalSupply - bptAmountIn); - updateState(useStablePool, balances, bptAmountIn, 0); - rateDecrease = rateBefore - rateAfter; - if (ASSERT_MODE) { - assert(rateDecrease <= 0); - } - } - - function computeRemoveLiquiditySingleTokenExactIn( - uint256 tokenOutIndex, - uint256 exactBptAmountIn, - uint256 swapFeePercentage, - bool useStablePool - ) public returns (uint256 bptAmountOut) { - assumeValidTradeAmount(exactBptAmountIn); - tokenOutIndex = boundTokenIndex(useStablePool, tokenOutIndex); - exactBptAmountIn = boundBptBurn(useStablePool, exactBptAmountIn); - (uint256[] memory balances, uint256 bptTotalSupply) = loadState(useStablePool); - uint256 rateBefore = getBptRate(getPool(useStablePool), balances, bptTotalSupply); - - (uint256 amountOutMinusFee, ) = BasePoolMath.computeRemoveLiquiditySingleTokenExactIn( - balances, - tokenOutIndex, - exactBptAmountIn, - bptTotalSupply, - swapFeePercentage, - getPool(useStablePool) - ); - - bptAmountOut = amountOutMinusFee; - balances[tokenOutIndex] -= amountOutMinusFee; // fees already accounted for - uint256 rateAfter = getBptRate(getPool(useStablePool), balances, bptTotalSupply - exactBptAmountIn); - updateState(useStablePool, balances, exactBptAmountIn, 0); - rateDecrease = rateBefore - rateAfter; - if (ASSERT_MODE) { - assert(rateDecrease <= 0); - } - } - - //////////////////////////////////////// - // Helpers - - function mockInitialize(IBasePool pool, uint256[] memory balances) private view returns (uint256) { - uint256 invariant = pool.computeInvariant(balances, Rounding.ROUND_DOWN); - if (invariant < 1e6) revert(); - return invariant; - } - - function loadState(bool useStablePool) private returns (uint256[] memory balances, uint256 bptTotalSupply) { - balances = useStablePool ? stableBalanceLive : weightedBalanceLive; - bptTotalSupply = useStablePool ? stableBPTSupply : weightedBPTSupply; - } - - function updateState( - bool useStablePool, - uint256[] memory balances, - uint256 bptAmountIn, - uint256 bptAmountOut - ) private { - if (useStablePool) { - stableBalanceLive = balances; - stableBPTSupply -= bptAmountIn; - stableBPTSupply += bptAmountOut; - require(stableBPTSupply >= _POOL_MINIMUM_TOTAL_SUPPLY); - } else { - weightedBalanceLive = balances; - weightedBPTSupply -= bptAmountIn; - weightedBPTSupply += bptAmountOut; - require(weightedBPTSupply >= _POOL_MINIMUM_TOTAL_SUPPLY); - } - } - - function getBptRate(IBasePool pool, uint256[] memory balances, uint256 bptTotalSupply) private returns (uint256) { - uint256 invariant = pool.computeInvariant(balances, Rounding.ROUND_DOWN); - return invariant.divDown(bptTotalSupply); - } - - function getPool(bool useStablePool) private view returns (IBasePool) { - return useStablePool ? stablePool : weightedPool; - } - - function getBalancesLength(bool useStablePool) private view returns (uint256 length) { - length = useStablePool ? stableBalanceLive.length : weightedBalanceLive.length; - } - - function boundTokenIndex(bool useStablePool, uint256 tokenIndex) private view returns (uint256 boundedIndex) { - uint256 len = getBalancesLength(useStablePool); - boundedIndex = bound(tokenIndex, 0, len - 1); - } - - function boundTokenDeposit( - bool useStablePool, - uint256 tokenAmt, - uint256 tokenIndex - ) private view returns (uint256 boundedAmt) { - uint256[] memory balances = useStablePool ? stableBalanceLive : weightedBalanceLive; - boundedAmt = bound(tokenAmt, 0, _MAX_BALANCE - balances[tokenIndex]); - } - - function boundTokenWithdraw( - bool useStablePool, - uint256 tokenAmt, - uint256 tokenIndex - ) private view returns (uint256 boundedAmt) { - uint256[] memory balances = useStablePool ? stableBalanceLive : weightedBalanceLive; - boundedAmt = bound(tokenAmt, 0, balances[tokenIndex]); - } - - function boundBptMint(bool useStablePool, uint256 bptAmt) private view returns (uint256 boundedAmt) { - uint256 totalSupply = useStablePool ? stableBPTSupply : weightedBPTSupply; - boundedAmt = bound(bptAmt, 0, _MAX_BALANCE - totalSupply); - } - - function boundBptBurn(bool useStablePool, uint256 bptAmt) private view returns (uint256 boundedAmt) { - uint256 totalSupply = useStablePool ? stableBPTSupply : weightedBPTSupply; - boundedAmt = bound(bptAmt, 0, totalSupply); - } - - function boundBalanceLength(uint256[] memory balances, bool isStablePool) private pure returns (uint256[] memory) { - if (!isStablePool) { - if (balances.length < 3) revert(); - assembly { - mstore(balances, 3) - } - return balances; - } else { - if (balances.length < 2) revert(); - uint256 numTokens = bound(balances.length, 2, 8); - assembly { - mstore(balances, numTokens) - } - return balances; - } - } - - function sumBalances(uint256[] memory balances, uint256[] memory amounts) private pure returns (uint256[] memory) { - require(amounts.length == balances.length); - uint256[] memory newBalances = new uint256[](balances.length); - for (uint256 i = 0; i < balances.length; i++) { - newBalances[i] = balances[i] + amounts[i]; - } - return newBalances; - } - - function subBalances(uint256[] memory balances, uint256[] memory amounts) private pure returns (uint256[] memory) { - require(amounts.length == balances.length); - uint256[] memory newBalances = new uint256[](balances.length); - for (uint256 i = 0; i < balances.length; i++) { - newBalances[i] = balances[i] - amounts[i]; - } - return newBalances; - } - - function assumeValidBalanceLength(uint256[] memory balances) private pure { - if (balances.length < 2 || balances.length > 8) revert(); - } - - function assumeValidTradeAmount(uint256 tradeAmount) private pure { - if (tradeAmount != 0 && tradeAmount < _MINIMUM_TRADE_AMOUNT) { - revert(); - } - } -} From 86d3f6f653a34c3422ac0079d2ffbcffa00112ea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Bruno?= Date: Wed, 9 Oct 2024 14:41:57 -0300 Subject: [PATCH 06/29] Remove unused test contract --- .../test/foundry/utils/VaultMockDeployer.sol | 42 ------------------- 1 file changed, 42 deletions(-) delete mode 100644 pkg/vault/test/foundry/utils/VaultMockDeployer.sol diff --git a/pkg/vault/test/foundry/utils/VaultMockDeployer.sol b/pkg/vault/test/foundry/utils/VaultMockDeployer.sol deleted file mode 100644 index 2df91ae06..000000000 --- a/pkg/vault/test/foundry/utils/VaultMockDeployer.sol +++ /dev/null @@ -1,42 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later - -pragma solidity ^0.8.24; - -import { IAuthorizer } from "@balancer-labs/v3-interfaces/contracts/vault/IAuthorizer.sol"; -import { CREATE3 } from "@balancer-labs/v3-solidity-utils/contracts/solmate/CREATE3.sol"; -import { IVault } from "@balancer-labs/v3-interfaces/contracts/vault/IVault.sol"; - -import { ProtocolFeeControllerMock } from "../../../contracts/test/ProtocolFeeControllerMock.sol"; -import { BasicAuthorizerMock } from "../../../contracts/test/BasicAuthorizerMock.sol"; -import { ProtocolFeeController } from "../../../contracts/ProtocolFeeController.sol"; -import { VaultExtensionMock } from "../../../contracts/test/VaultExtensionMock.sol"; -import { VaultAdminMock } from "../../../contracts/test/VaultAdminMock.sol"; -import { VaultMock } from "../../../contracts/test/VaultMock.sol"; - -library VaultMockDeployer { - function deploy() internal returns (VaultMock vault) { - return deploy(0, 0); - } - - function deploy(uint256 minTradeAmount, uint256 minWrapAmount) internal returns (VaultMock vault) { - IAuthorizer authorizer = new BasicAuthorizerMock(); - bytes32 salt = bytes32(0); - vault = VaultMock(payable(CREATE3.getDeployed(salt))); - VaultAdminMock vaultAdmin = new VaultAdminMock( - IVault(address(vault)), - 90 days, - 30 days, - minTradeAmount, - minWrapAmount - ); - VaultExtensionMock vaultExtension = new VaultExtensionMock(IVault(address(vault)), vaultAdmin); - ProtocolFeeController protocolFeeController = new ProtocolFeeControllerMock(IVault(address(vault))); - - _create(abi.encode(vaultExtension, authorizer, protocolFeeController), salt); - return vault; - } - - function _create(bytes memory constructorArgs, bytes32 salt) internal returns (address) { - return CREATE3.deploy(salt, abi.encodePacked(type(VaultMock).creationCode, constructorArgs), 0); - } -} From de2c8d7d31ae56d6d4398f2a0a7d7f807b151f01 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Bruno?= Date: Wed, 9 Oct 2024 15:01:10 -0300 Subject: [PATCH 07/29] Remove stale comment --- pkg/vault/test/foundry/utils/BaseMedusaTest.sol | 2 -- 1 file changed, 2 deletions(-) diff --git a/pkg/vault/test/foundry/utils/BaseMedusaTest.sol b/pkg/vault/test/foundry/utils/BaseMedusaTest.sol index d0884290f..2ea484835 100644 --- a/pkg/vault/test/foundry/utils/BaseMedusaTest.sol +++ b/pkg/vault/test/foundry/utils/BaseMedusaTest.sol @@ -64,8 +64,6 @@ contract BaseMedusaTest is Test { ERC20TestToken internal usdc; WETHTestToken internal weth; - // Set permissions for users - constructor() { dai = _createERC20TestToken("DAI", "DAI", 18); usdc = _createERC20TestToken("USDC", "USDC", 18); From 7a8b83cf082c672c5c58a12ddf935ffb6302ff3c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Bruno?= Date: Wed, 9 Oct 2024 15:13:45 -0300 Subject: [PATCH 08/29] Improve documentation --- README.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/README.md b/README.md index 84aa7cb25..e52eb1c02 100644 --- a/README.md +++ b/README.md @@ -98,6 +98,20 @@ $ yarn test:hardhat Hardhat tests will also update snapshots for bytecode size and gas usage if applicable for a given package. +### Medusa tests + +Medusa is a tool, developed by Trail of Bits, that allow us to execute stateful fuzz tests in contracts, in a way +smarter than Forge. That's because Medusa has an optimizer to create a path of transactions which is not completely +random. + +To run Medusa tests, we first need to install Echidna. To do so, install `echidna` using the +[release page](https://github.com/crytic/echidna/releases) or `brew install echidna` in Mac. + +Then, install Medusa using this [installation guide](https://github.com/crytic/medusa/blob/master/docs/src/getting_started/installation.md#building-from-source). + +Finally, run `yarn fuzz:medusa`. This command is available inside the packages `vault`, `pool-weighted` and +`pool-stable`. + ## Static analysis To run [Slither](https://github.com/crytic/slither) static analyzer, Python 3.8+ is a requirement. From 6502799beba1fe7db775525b790fc7064945929f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Bruno?= Date: Wed, 9 Oct 2024 15:16:33 -0300 Subject: [PATCH 09/29] Remove Echidna configuration --- README.md | 3 ++- pkg/vault/echidna.yml | 4 ---- pkg/vault/package.json | 1 - 3 files changed, 2 insertions(+), 6 deletions(-) delete mode 100644 pkg/vault/echidna.yml diff --git a/README.md b/README.md index e52eb1c02..2651bd55c 100644 --- a/README.md +++ b/README.md @@ -105,7 +105,8 @@ smarter than Forge. That's because Medusa has an optimizer to create a path of t random. To run Medusa tests, we first need to install Echidna. To do so, install `echidna` using the -[release page](https://github.com/crytic/echidna/releases) or `brew install echidna` in Mac. +[release page](https://github.com/crytic/echidna/releases) or `brew install echidna` in Mac. Notice that the Mac +installation will also install Crytic-compiller, so this step can be skipped when installing Medusa. Then, install Medusa using this [installation guide](https://github.com/crytic/medusa/blob/master/docs/src/getting_started/installation.md#building-from-source). diff --git a/pkg/vault/echidna.yml b/pkg/vault/echidna.yml deleted file mode 100644 index 6e54bd145..000000000 --- a/pkg/vault/echidna.yml +++ /dev/null @@ -1,4 +0,0 @@ -testMode: "assertion" -coverage: false -cryticArgs: ["--foundry-out-directory=forge-artifacts", "--foundry-compile-all"] -allowFFI: true diff --git a/pkg/vault/package.json b/pkg/vault/package.json index a7345ec60..2ca4f0aea 100644 --- a/pkg/vault/package.json +++ b/pkg/vault/package.json @@ -30,7 +30,6 @@ "test:forgeonly": "forge test -vvv --no-match-test Fork", "test:stress": "FOUNDRY_PROFILE=intense forge test -vvv", "fuzz:medusa": "medusa fuzz --config medusa.json", - "fuzz:echidna": "echidna . --contract FuzzHarness --config echidna.yml", "coverage": "./coverage.sh forge", "coverage:hardhat": "./coverage.sh hardhat", "coverage:all": "./coverage.sh all", From 37af832ec0284cceba2ddb85acbc73dad0e2ba92 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Bruno?= Date: Mon, 14 Oct 2024 09:46:10 -0300 Subject: [PATCH 10/29] Replace Tabs by spaces --- pkg/pool-stable/medusa.json | 164 +++++++++++++++++----------------- pkg/pool-weighted/medusa.json | 164 +++++++++++++++++----------------- pkg/vault/medusa.json | 164 +++++++++++++++++----------------- 3 files changed, 246 insertions(+), 246 deletions(-) diff --git a/pkg/pool-stable/medusa.json b/pkg/pool-stable/medusa.json index 86f6ee730..7cfb367fc 100644 --- a/pkg/pool-stable/medusa.json +++ b/pkg/pool-stable/medusa.json @@ -1,84 +1,84 @@ { - "fuzzing": { - "workers": 10, - "workerResetLimit": 50, - "timeout": 0, - "testLimit": 100000, - "shrinkLimit": 5000, - "callSequenceLength": 10, - "corpusDirectory": "medusa-corpus", - "coverageEnabled": true, - "targetContracts": ["AddAndRemoveLiquidityStableMedusaTest"], - "predeployedContracts": {}, - "targetContractsBalances": [], - "constructorArgs": {}, - "deployerAddress": "0x30000", - "senderAddresses": [ - "0x10000", - "0x20000", - "0x30000" - ], - "blockNumberDelayMax": 60480, - "blockTimestampDelayMax": 604800, - "blockGasLimit": 1250000000, - "transactionGasLimit": 125000000, - "testing": { - "stopOnFailedTest": false, - "stopOnFailedContractMatching": false, - "stopOnNoTests": true, - "testAllContracts": false, - "traceAll": false, - "assertionTesting": { - "enabled": true, - "testViewMethods": false, - "panicCodeConfig": { - "failOnCompilerInsertedPanic": false, - "failOnAssertion": true, - "failOnArithmeticUnderflow": false, - "failOnDivideByZero": false, - "failOnEnumTypeConversionOutOfBounds": false, - "failOnIncorrectStorageAccess": false, - "failOnPopEmptyArray": false, - "failOnOutOfBoundsArrayAccess": false, - "failOnAllocateTooMuchMemory": false, - "failOnCallUninitializedVariable": false - } - }, - "propertyTesting": { - "enabled": true, - "testPrefixes": [ - "property_" - ] - }, - "optimizationTesting": { - "enabled": true, - "testPrefixes": [ - "optimize_" - ] - }, - "targetFunctionSignatures": [], - "excludeFunctionSignatures": [] - }, - "chainConfig": { - "codeSizeCheckDisabled": true, - "cheatCodes": { - "cheatCodesEnabled": true, - "enableFFI": true - } - } - }, - "compilation": { - "platform": "crytic-compile", - "platformConfig": { - "target": ".", - "solcVersion": "", - "exportDirectory": "", - "args": ["--foundry-out-directory=forge-artifacts", "--foundry-compile-all"] - } - }, - "logging": { - "level": "info", - "logDirectory": "", - "noColor": false - } + "fuzzing": { + "workers": 10, + "workerResetLimit": 50, + "timeout": 0, + "testLimit": 100000, + "shrinkLimit": 5000, + "callSequenceLength": 10, + "corpusDirectory": "medusa-corpus", + "coverageEnabled": true, + "targetContracts": ["AddAndRemoveLiquidityStableMedusaTest"], + "predeployedContracts": {}, + "targetContractsBalances": [], + "constructorArgs": {}, + "deployerAddress": "0x30000", + "senderAddresses": [ + "0x10000", + "0x20000", + "0x30000" + ], + "blockNumberDelayMax": 60480, + "blockTimestampDelayMax": 604800, + "blockGasLimit": 1250000000, + "transactionGasLimit": 125000000, + "testing": { + "stopOnFailedTest": false, + "stopOnFailedContractMatching": false, + "stopOnNoTests": true, + "testAllContracts": false, + "traceAll": false, + "assertionTesting": { + "enabled": true, + "testViewMethods": false, + "panicCodeConfig": { + "failOnCompilerInsertedPanic": false, + "failOnAssertion": true, + "failOnArithmeticUnderflow": false, + "failOnDivideByZero": false, + "failOnEnumTypeConversionOutOfBounds": false, + "failOnIncorrectStorageAccess": false, + "failOnPopEmptyArray": false, + "failOnOutOfBoundsArrayAccess": false, + "failOnAllocateTooMuchMemory": false, + "failOnCallUninitializedVariable": false + } + }, + "propertyTesting": { + "enabled": true, + "testPrefixes": [ + "property_" + ] + }, + "optimizationTesting": { + "enabled": true, + "testPrefixes": [ + "optimize_" + ] + }, + "targetFunctionSignatures": [], + "excludeFunctionSignatures": [] + }, + "chainConfig": { + "codeSizeCheckDisabled": true, + "cheatCodes": { + "cheatCodesEnabled": true, + "enableFFI": true + } + } + }, + "compilation": { + "platform": "crytic-compile", + "platformConfig": { + "target": ".", + "solcVersion": "", + "exportDirectory": "", + "args": ["--foundry-out-directory=forge-artifacts", "--foundry-compile-all"] + } + }, + "logging": { + "level": "info", + "logDirectory": "", + "noColor": false + } } diff --git a/pkg/pool-weighted/medusa.json b/pkg/pool-weighted/medusa.json index e94c90e11..36312e1e5 100644 --- a/pkg/pool-weighted/medusa.json +++ b/pkg/pool-weighted/medusa.json @@ -1,84 +1,84 @@ { - "fuzzing": { - "workers": 10, - "workerResetLimit": 50, - "timeout": 0, - "testLimit": 100000, - "shrinkLimit": 5000, - "callSequenceLength": 10, - "corpusDirectory": "medusa-corpus", - "coverageEnabled": true, - "targetContracts": ["AddAndRemoveLiquidityWeightedMedusaTest"], - "predeployedContracts": {}, - "targetContractsBalances": [], - "constructorArgs": {}, - "deployerAddress": "0x30000", - "senderAddresses": [ - "0x10000", - "0x20000", - "0x30000" - ], - "blockNumberDelayMax": 60480, - "blockTimestampDelayMax": 604800, - "blockGasLimit": 1250000000, - "transactionGasLimit": 125000000, - "testing": { - "stopOnFailedTest": false, - "stopOnFailedContractMatching": false, - "stopOnNoTests": true, - "testAllContracts": false, - "traceAll": false, - "assertionTesting": { - "enabled": true, - "testViewMethods": false, - "panicCodeConfig": { - "failOnCompilerInsertedPanic": false, - "failOnAssertion": true, - "failOnArithmeticUnderflow": false, - "failOnDivideByZero": false, - "failOnEnumTypeConversionOutOfBounds": false, - "failOnIncorrectStorageAccess": false, - "failOnPopEmptyArray": false, - "failOnOutOfBoundsArrayAccess": false, - "failOnAllocateTooMuchMemory": false, - "failOnCallUninitializedVariable": false - } - }, - "propertyTesting": { - "enabled": true, - "testPrefixes": [ - "property_" - ] - }, - "optimizationTesting": { - "enabled": true, - "testPrefixes": [ - "optimize_" - ] - }, - "targetFunctionSignatures": [], - "excludeFunctionSignatures": [] - }, - "chainConfig": { - "codeSizeCheckDisabled": true, - "cheatCodes": { - "cheatCodesEnabled": true, - "enableFFI": true - } - } - }, - "compilation": { - "platform": "crytic-compile", - "platformConfig": { - "target": ".", - "solcVersion": "", - "exportDirectory": "", - "args": ["--foundry-out-directory=forge-artifacts", "--foundry-compile-all"] - } - }, - "logging": { - "level": "info", - "logDirectory": "", - "noColor": false - } + "fuzzing": { + "workers": 10, + "workerResetLimit": 50, + "timeout": 0, + "testLimit": 100000, + "shrinkLimit": 5000, + "callSequenceLength": 10, + "corpusDirectory": "medusa-corpus", + "coverageEnabled": true, + "targetContracts": ["AddAndRemoveLiquidityWeightedMedusaTest"], + "predeployedContracts": {}, + "targetContractsBalances": [], + "constructorArgs": {}, + "deployerAddress": "0x30000", + "senderAddresses": [ + "0x10000", + "0x20000", + "0x30000" + ], + "blockNumberDelayMax": 60480, + "blockTimestampDelayMax": 604800, + "blockGasLimit": 1250000000, + "transactionGasLimit": 125000000, + "testing": { + "stopOnFailedTest": false, + "stopOnFailedContractMatching": false, + "stopOnNoTests": true, + "testAllContracts": false, + "traceAll": false, + "assertionTesting": { + "enabled": true, + "testViewMethods": false, + "panicCodeConfig": { + "failOnCompilerInsertedPanic": false, + "failOnAssertion": true, + "failOnArithmeticUnderflow": false, + "failOnDivideByZero": false, + "failOnEnumTypeConversionOutOfBounds": false, + "failOnIncorrectStorageAccess": false, + "failOnPopEmptyArray": false, + "failOnOutOfBoundsArrayAccess": false, + "failOnAllocateTooMuchMemory": false, + "failOnCallUninitializedVariable": false + } + }, + "propertyTesting": { + "enabled": true, + "testPrefixes": [ + "property_" + ] + }, + "optimizationTesting": { + "enabled": true, + "testPrefixes": [ + "optimize_" + ] + }, + "targetFunctionSignatures": [], + "excludeFunctionSignatures": [] + }, + "chainConfig": { + "codeSizeCheckDisabled": true, + "cheatCodes": { + "cheatCodesEnabled": true, + "enableFFI": true + } + } + }, + "compilation": { + "platform": "crytic-compile", + "platformConfig": { + "target": ".", + "solcVersion": "", + "exportDirectory": "", + "args": ["--foundry-out-directory=forge-artifacts", "--foundry-compile-all"] + } + }, + "logging": { + "level": "info", + "logDirectory": "", + "noColor": false + } } diff --git a/pkg/vault/medusa.json b/pkg/vault/medusa.json index 83e939589..7f422cbf3 100644 --- a/pkg/vault/medusa.json +++ b/pkg/vault/medusa.json @@ -1,84 +1,84 @@ { - "fuzzing": { - "workers": 10, - "workerResetLimit": 50, - "timeout": 0, - "testLimit": 100000, - "shrinkLimit": 5000, - "callSequenceLength": 10, - "corpusDirectory": "medusa-corpus", - "coverageEnabled": true, - "targetContracts": ["AddAndRemoveLiquidityMedusaTest"], - "predeployedContracts": {}, - "targetContractsBalances": [], - "constructorArgs": {}, - "deployerAddress": "0x30000", - "senderAddresses": [ - "0x10000", - "0x20000", - "0x30000" - ], - "blockNumberDelayMax": 60480, - "blockTimestampDelayMax": 604800, - "blockGasLimit": 1250000000, - "transactionGasLimit": 125000000, - "testing": { - "stopOnFailedTest": false, - "stopOnFailedContractMatching": false, - "stopOnNoTests": true, - "testAllContracts": false, - "traceAll": false, - "assertionTesting": { - "enabled": true, - "testViewMethods": false, - "panicCodeConfig": { - "failOnCompilerInsertedPanic": false, - "failOnAssertion": true, - "failOnArithmeticUnderflow": false, - "failOnDivideByZero": false, - "failOnEnumTypeConversionOutOfBounds": false, - "failOnIncorrectStorageAccess": false, - "failOnPopEmptyArray": false, - "failOnOutOfBoundsArrayAccess": false, - "failOnAllocateTooMuchMemory": false, - "failOnCallUninitializedVariable": false - } - }, - "propertyTesting": { - "enabled": true, - "testPrefixes": [ - "property_" - ] - }, - "optimizationTesting": { - "enabled": true, - "testPrefixes": [ - "optimize_" - ] - }, - "targetFunctionSignatures": [], - "excludeFunctionSignatures": [] - }, - "chainConfig": { - "codeSizeCheckDisabled": true, - "cheatCodes": { - "cheatCodesEnabled": true, - "enableFFI": true - } - } - }, - "compilation": { - "platform": "crytic-compile", - "platformConfig": { - "target": ".", - "solcVersion": "", - "exportDirectory": "", - "args": ["--foundry-out-directory=forge-artifacts", "--foundry-compile-all"] - } - }, - "logging": { - "level": "info", - "logDirectory": "", - "noColor": false - } + "fuzzing": { + "workers": 10, + "workerResetLimit": 50, + "timeout": 0, + "testLimit": 100000, + "shrinkLimit": 5000, + "callSequenceLength": 10, + "corpusDirectory": "medusa-corpus", + "coverageEnabled": true, + "targetContracts": ["AddAndRemoveLiquidityMedusaTest"], + "predeployedContracts": {}, + "targetContractsBalances": [], + "constructorArgs": {}, + "deployerAddress": "0x30000", + "senderAddresses": [ + "0x10000", + "0x20000", + "0x30000" + ], + "blockNumberDelayMax": 60480, + "blockTimestampDelayMax": 604800, + "blockGasLimit": 1250000000, + "transactionGasLimit": 125000000, + "testing": { + "stopOnFailedTest": false, + "stopOnFailedContractMatching": false, + "stopOnNoTests": true, + "testAllContracts": false, + "traceAll": false, + "assertionTesting": { + "enabled": true, + "testViewMethods": false, + "panicCodeConfig": { + "failOnCompilerInsertedPanic": false, + "failOnAssertion": true, + "failOnArithmeticUnderflow": false, + "failOnDivideByZero": false, + "failOnEnumTypeConversionOutOfBounds": false, + "failOnIncorrectStorageAccess": false, + "failOnPopEmptyArray": false, + "failOnOutOfBoundsArrayAccess": false, + "failOnAllocateTooMuchMemory": false, + "failOnCallUninitializedVariable": false + } + }, + "propertyTesting": { + "enabled": true, + "testPrefixes": [ + "property_" + ] + }, + "optimizationTesting": { + "enabled": true, + "testPrefixes": [ + "optimize_" + ] + }, + "targetFunctionSignatures": [], + "excludeFunctionSignatures": [] + }, + "chainConfig": { + "codeSizeCheckDisabled": true, + "cheatCodes": { + "cheatCodesEnabled": true, + "enableFFI": true + } + } + }, + "compilation": { + "platform": "crytic-compile", + "platformConfig": { + "target": ".", + "solcVersion": "", + "exportDirectory": "", + "args": ["--foundry-out-directory=forge-artifacts", "--foundry-compile-all"] + } + }, + "logging": { + "level": "info", + "logDirectory": "", + "noColor": false + } } From dc7b881b5014889110054aad42d82315200eba46 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Bruno?= Date: Mon, 14 Oct 2024 09:47:58 -0300 Subject: [PATCH 11/29] Fix weights boundaries --- .../fuzz/AddAndRemoveLiquidityWeighted.medusa.sol | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/pkg/pool-weighted/test/foundry/fuzz/AddAndRemoveLiquidityWeighted.medusa.sol b/pkg/pool-weighted/test/foundry/fuzz/AddAndRemoveLiquidityWeighted.medusa.sol index b1de1a0ba..e3d915735 100644 --- a/pkg/pool-weighted/test/foundry/fuzz/AddAndRemoveLiquidityWeighted.medusa.sol +++ b/pkg/pool-weighted/test/foundry/fuzz/AddAndRemoveLiquidityWeighted.medusa.sol @@ -32,11 +32,10 @@ contract AddAndRemoveLiquidityWeightedMedusaTest is AddAndRemoveLiquidityMedusaT uint256[] memory initialBalances ) internal override returns (address newPool) { uint256[] memory weights = new uint256[](3); - weights[0] = bound(_WEIGHT1, _MIN_WEIGHT, 98e16); // any weight between min & 100%-2(min) - uint256 remainingWeight = 99e16 - weights[0]; // weights 0 + 1 must <= 100%-min so there's >=1% left for weight 2 - weights[1] = bound(_WEIGHT2, _MIN_WEIGHT, remainingWeight); - remainingWeight = 100e16 - (weights[0] + weights[1]); - weights[2] = remainingWeight; + weights[0] = _WEIGHT1; + weights[1] = _WEIGHT2; + // Sum of weights should equal 100%. + weights[2] = 100e16 - (weights[0] + weights[1]); WeightedPoolFactory factory = new WeightedPoolFactory( IVault(address(vault)), From d20d6a9b5442bec1deb3ba0655fe627083b0108a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Bruno?= Date: Mon, 14 Oct 2024 09:49:49 -0300 Subject: [PATCH 12/29] Fix comments --- .../fuzz/AddAndRemoveLiquidity.medusa.sol | 20 +++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/pkg/vault/test/foundry/fuzz/AddAndRemoveLiquidity.medusa.sol b/pkg/vault/test/foundry/fuzz/AddAndRemoveLiquidity.medusa.sol index aa2ab60a8..47fe4b159 100644 --- a/pkg/vault/test/foundry/fuzz/AddAndRemoveLiquidity.medusa.sol +++ b/pkg/vault/test/foundry/fuzz/AddAndRemoveLiquidity.medusa.sol @@ -38,8 +38,9 @@ contract AddAndRemoveLiquidityMedusaTest is BaseMedusaTest { initialRate = getBptRate(); } - //////////////////////////////////////// - // Optimizations + /******************************************************************************* + Optimizations + *******************************************************************************/ function optimize_rateDecrease() public view returns (int256) { return rateDecrease; @@ -49,8 +50,9 @@ contract AddAndRemoveLiquidityMedusaTest is BaseMedusaTest { return int256(bptProfit); } - //////////////////////////////////////// - // Symmetrical Add/Remove Liquidity + /******************************************************************************* + Symmetrical Add/Remove Liquidity + *******************************************************************************/ function computeAddAndRemoveLiquiditySingleToken( uint256 tokenIndex, @@ -203,8 +205,9 @@ contract AddAndRemoveLiquidityMedusaTest is BaseMedusaTest { return assertBptProfit(pool); } - //////////////////////////////////////// - // Simple operations + /******************************************************************************* + Simple Add/Remove operations + *******************************************************************************/ function computeProportionalAmountsIn(uint256 bptAmountOut) public returns (uint256[] memory amountsIn) { assumeValidTradeAmount(bptAmountOut); @@ -335,8 +338,9 @@ contract AddAndRemoveLiquidityMedusaTest is BaseMedusaTest { return assertRate(pool); } - //////////////////////////////////////// - // Helpers (private functions, so they're not fuzzed) + /******************************************************************************* + Helpers (private functions, so they're not fuzzed) + *******************************************************************************/ function assertBptProfit(IBasePool pool) internal returns (bool) { return bptProfit <= 0; From 453b82a5b8759ebe1788b53d85b3afe0e6af7fce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Bruno?= Date: Mon, 14 Oct 2024 11:23:07 -0300 Subject: [PATCH 13/29] Fix WETH token in Medusa --- .../contracts/test/WETHTestToken.sol | 4 ---- .../test/foundry/utils/BaseMedusaTest.sol | 20 +++++++++---------- 2 files changed, 10 insertions(+), 14 deletions(-) diff --git a/pkg/solidity-utils/contracts/test/WETHTestToken.sol b/pkg/solidity-utils/contracts/test/WETHTestToken.sol index f92bdc79e..631b3dc37 100644 --- a/pkg/solidity-utils/contracts/test/WETHTestToken.sol +++ b/pkg/solidity-utils/contracts/test/WETHTestToken.sol @@ -27,8 +27,4 @@ contract WETHTestToken is IWETH, ERC20 { payable(msg.sender).transfer(wad); emit Withdrawal(msg.sender, wad); } - - function mint(address to, uint256 value) public { - _mint(to, value); - } } diff --git a/pkg/vault/test/foundry/utils/BaseMedusaTest.sol b/pkg/vault/test/foundry/utils/BaseMedusaTest.sol index 2ea484835..771cc8415 100644 --- a/pkg/vault/test/foundry/utils/BaseMedusaTest.sol +++ b/pkg/vault/test/foundry/utils/BaseMedusaTest.sol @@ -13,13 +13,13 @@ import { IVaultAdmin } from "@balancer-labs/v3-interfaces/contracts/vault/IVault import { IVaultMock } from "@balancer-labs/v3-interfaces/contracts/test/IVaultMock.sol"; import { IBasePool } from "@balancer-labs/v3-interfaces/contracts/vault/IBasePool.sol"; import { IVault } from "@balancer-labs/v3-interfaces/contracts/vault/IVault.sol"; +import { IWETH } from "@balancer-labs/v3-interfaces/contracts/solidity-utils/misc/IWETH.sol"; import { IStdMedusaCheats } from "@balancer-labs/v3-interfaces/contracts/test/IStdMedusaCheats.sol"; import "@balancer-labs/v3-interfaces/contracts/vault/VaultTypes.sol"; import { InputHelpers } from "@balancer-labs/v3-solidity-utils/contracts/helpers/InputHelpers.sol"; import { CREATE3 } from "@balancer-labs/v3-solidity-utils/contracts/solmate/CREATE3.sol"; import { ERC20TestToken } from "@balancer-labs/v3-solidity-utils/contracts/test/ERC20TestToken.sol"; -import { WETHTestToken } from "@balancer-labs/v3-solidity-utils/contracts/test/WETHTestToken.sol"; import { VaultExtensionMock } from "../../../contracts/test/VaultExtensionMock.sol"; import { VaultAdminMock } from "../../../contracts/test/VaultAdminMock.sol"; @@ -62,25 +62,25 @@ contract BaseMedusaTest is Test { ERC20TestToken internal dai; ERC20TestToken internal usdc; - WETHTestToken internal weth; + ERC20TestToken internal weth; constructor() { dai = _createERC20TestToken("DAI", "DAI", 18); usdc = _createERC20TestToken("USDC", "USDC", 18); - weth = new WETHTestToken(); - - // The only function used by _mintTokenToUsers is mint, which has the same signature as ERC20TestToken. So, - // cast weth as ERC20TestToken to use the same funtion. - _mintTokenToUsers(ERC20TestToken(address(weth))); + weth = _createERC20TestToken("WETH", "WETH", 18); DeployPermit2 _deployPermit2 = new DeployPermit2(); permit2 = IPermit2(_deployPermit2.deployPermit2()); _deployVaultMock(0, 0); - router = new RouterMock(IVault(address(vault)), weth, permit2); - batchRouter = new BatchRouterMock(IVault(address(vault)), weth, permit2); - compositeLiquidityRouter = new CompositeLiquidityRouterMock(IVault(address(vault)), weth, permit2); + router = new RouterMock(IVault(address(vault)), IWETH(address(weth)), permit2); + batchRouter = new BatchRouterMock(IVault(address(vault)), IWETH(address(weth)), permit2); + compositeLiquidityRouter = new CompositeLiquidityRouterMock( + IVault(address(vault)), + IWETH(address(weth)), + permit2 + ); _setPermissionsForUsersAndTokens(); From 4a90c761b3cf8ba8e6a356ffb20095e60fd36abf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Bruno?= Date: Mon, 14 Oct 2024 11:25:19 -0300 Subject: [PATCH 14/29] Remove getBalancesLength function. --- .../foundry/fuzz/AddAndRemoveLiquidity.medusa.sol | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/pkg/vault/test/foundry/fuzz/AddAndRemoveLiquidity.medusa.sol b/pkg/vault/test/foundry/fuzz/AddAndRemoveLiquidity.medusa.sol index 47fe4b159..6b8c1c155 100644 --- a/pkg/vault/test/foundry/fuzz/AddAndRemoveLiquidity.medusa.sol +++ b/pkg/vault/test/foundry/fuzz/AddAndRemoveLiquidity.medusa.sol @@ -120,8 +120,8 @@ contract AddAndRemoveLiquidityMedusaTest is BaseMedusaTest { bytes("") ); - // deposit exactly tokenAmountOut to mint bptMintAmt - uint256[] memory exactAmountsIn = new uint256[](getBalancesLength()); + // deposit exactly tokenAmountOut to mint bptAmountOut. + uint256[] memory exactAmountsIn = new uint256[](vault.getPoolTokens(address(pool)).length); exactAmountsIn[tokenIndex] = tokenAmountOut; medusa.prank(lp); @@ -367,13 +367,8 @@ contract AddAndRemoveLiquidityMedusaTest is BaseMedusaTest { return invariant.divDown(bptTotalSupply); } - function getBalancesLength() internal view returns (uint256 length) { - (, , uint256[] memory balancesRaw, ) = vault.getPoolTokenInfo(address(pool)); - length = balancesRaw.length; - } - function boundTokenIndex(uint256 tokenIndex) internal view returns (uint256 boundedIndex) { - uint256 len = getBalancesLength(); + uint256 len = vault.getPoolTokens(address(pool)).length; boundedIndex = bound(tokenIndex, 0, len - 1); } From 97cf79d338b0f5df61b5445ac6d9d440dc0f1ce0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Bruno?= Date: Mon, 14 Oct 2024 11:30:22 -0300 Subject: [PATCH 15/29] Comments --- pkg/vault/test/foundry/utils/BaseMedusaTest.sol | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/pkg/vault/test/foundry/utils/BaseMedusaTest.sol b/pkg/vault/test/foundry/utils/BaseMedusaTest.sol index 771cc8415..01945846e 100644 --- a/pkg/vault/test/foundry/utils/BaseMedusaTest.sol +++ b/pkg/vault/test/foundry/utils/BaseMedusaTest.sol @@ -33,8 +33,14 @@ import { ProtocolFeeControllerMock } from "../../../contracts/test/ProtocolFeeCo import { PoolFactoryMock } from "../../../contracts/test/PoolFactoryMock.sol"; contract BaseMedusaTest is Test { + // Forge has vm commands, which allow us to prank callers, deal ETH and ERC20 tokens to users, etc. Medusa is not + // compatible with it and has its own StdCheats, in the address below. So, instead of calling `vm.prank`, we should + // use `medusa.prank`. The interface documents which functions are available. Notice that Medusa's StdCheats and + // Forge's StdCheats implement different methods. IStdMedusaCheats internal medusa = IStdMedusaCheats(0x7109709ECfa91a80626fF3989D68f67F5b1DD12D); + // In certain places, console.log will not print to stdout the intended message, so we use this event to print + // messages and values. event Debug(string, uint256); IPermit2 internal permit2; From 78c819f1a76199079372f1cb8d30a941f155dc86 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Bruno?= Date: Mon, 14 Oct 2024 11:34:10 -0300 Subject: [PATCH 16/29] Remove getBptRate implementation --- .../foundry/fuzz/AddAndRemoveLiquidity.medusa.sol | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/pkg/vault/test/foundry/fuzz/AddAndRemoveLiquidity.medusa.sol b/pkg/vault/test/foundry/fuzz/AddAndRemoveLiquidity.medusa.sol index 6b8c1c155..159cfe083 100644 --- a/pkg/vault/test/foundry/fuzz/AddAndRemoveLiquidity.medusa.sol +++ b/pkg/vault/test/foundry/fuzz/AddAndRemoveLiquidity.medusa.sol @@ -35,7 +35,7 @@ contract AddAndRemoveLiquidityMedusaTest is BaseMedusaTest { int256 internal bptProfit = 0; constructor() BaseMedusaTest() { - initialRate = getBptRate(); + initialRate = vault.getBptRate(address(pool)); } /******************************************************************************* @@ -352,21 +352,13 @@ contract AddAndRemoveLiquidityMedusaTest is BaseMedusaTest { } function updateRateDecrease() internal { - uint256 rateAfter = getBptRate(); + uint256 rateAfter = vault.getBptRate(address(pool)); rateDecrease = int256(initialRate) - int256(rateAfter); emit Debug("initial rate", initialRate); emit Debug("rate after", rateAfter); } - function getBptRate() internal returns (uint256) { - (, , , uint256[] memory lastBalancesLiveScaled18) = vault.getPoolTokenInfo(address(pool)); - uint256 bptTotalSupply = BalancerPoolToken(address(pool)).totalSupply(); - - uint256 invariant = pool.computeInvariant(lastBalancesLiveScaled18, Rounding.ROUND_DOWN); - return invariant.divDown(bptTotalSupply); - } - function boundTokenIndex(uint256 tokenIndex) internal view returns (uint256 boundedIndex) { uint256 len = vault.getPoolTokens(address(pool)).length; boundedIndex = bound(tokenIndex, 0, len - 1); From c46ac2eebb803ce2576c01fc646b93ca2244d43e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Bruno?= Date: Mon, 14 Oct 2024 11:50:51 -0300 Subject: [PATCH 17/29] Fix initial pool balances --- .../foundry/fuzz/AddAndRemoveLiquidityWeighted.medusa.sol | 1 - pkg/vault/test/foundry/utils/BaseMedusaTest.sol | 7 ++++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/pkg/pool-weighted/test/foundry/fuzz/AddAndRemoveLiquidityWeighted.medusa.sol b/pkg/pool-weighted/test/foundry/fuzz/AddAndRemoveLiquidityWeighted.medusa.sol index e3d915735..043c60150 100644 --- a/pkg/pool-weighted/test/foundry/fuzz/AddAndRemoveLiquidityWeighted.medusa.sol +++ b/pkg/pool-weighted/test/foundry/fuzz/AddAndRemoveLiquidityWeighted.medusa.sol @@ -19,7 +19,6 @@ import { WeightedPoolFactory } from "../../../contracts/WeightedPoolFactory.sol" import { WeightedPool } from "../../../contracts/WeightedPool.sol"; contract AddAndRemoveLiquidityWeightedMedusaTest is AddAndRemoveLiquidityMedusaTest { - uint256 private constant _MIN_WEIGHT = 1e16; // 1% (from WeightedPool.sol) uint256 private constant _WEIGHT1 = 33e16; uint256 private constant _WEIGHT2 = 33e16; diff --git a/pkg/vault/test/foundry/utils/BaseMedusaTest.sol b/pkg/vault/test/foundry/utils/BaseMedusaTest.sol index 01945846e..b976c2c76 100644 --- a/pkg/vault/test/foundry/utils/BaseMedusaTest.sol +++ b/pkg/vault/test/foundry/utils/BaseMedusaTest.sol @@ -60,6 +60,7 @@ contract BaseMedusaTest is Test { uint256 internal poolCreationNonce; uint256 internal constant DEFAULT_USER_BALANCE = 1e18 * 1e18; + uint256 internal constant DEFAULT_INITIAL_POOL_BALANCE = 1e6 * 1e18; // Set alice,bob and lp to addresses of medusa.json "senderAddresses" property address internal alice = address(0x10000); @@ -121,9 +122,9 @@ contract BaseMedusaTest is Test { tokens = InputHelpers.sortTokens(tokens); initialBalances = new uint256[](3); - initialBalances[0] = 10e18; - initialBalances[1] = 20e18; - initialBalances[2] = 30e18; + initialBalances[0] = DEFAULT_INITIAL_POOL_BALANCE; + initialBalances[1] = DEFAULT_INITIAL_POOL_BALANCE; + initialBalances[2] = DEFAULT_INITIAL_POOL_BALANCE; } function _createERC20TestToken( From 965170e0948e1727ac23dec0a8ee1b3823b8d7b6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Bruno?= Date: Mon, 14 Oct 2024 11:55:05 -0300 Subject: [PATCH 18/29] Comment --- .../test/foundry/fuzz/AddAndRemoveLiquidityWeighted.medusa.sol | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pkg/pool-weighted/test/foundry/fuzz/AddAndRemoveLiquidityWeighted.medusa.sol b/pkg/pool-weighted/test/foundry/fuzz/AddAndRemoveLiquidityWeighted.medusa.sol index 043c60150..4533fbd5b 100644 --- a/pkg/pool-weighted/test/foundry/fuzz/AddAndRemoveLiquidityWeighted.medusa.sol +++ b/pkg/pool-weighted/test/foundry/fuzz/AddAndRemoveLiquidityWeighted.medusa.sol @@ -23,6 +23,9 @@ contract AddAndRemoveLiquidityWeightedMedusaTest is AddAndRemoveLiquidityMedusaT uint256 private constant _WEIGHT2 = 33e16; constructor() AddAndRemoveLiquidityMedusaTest() { + // Weighted Pool rate is not reliable, since the Pow function introduces rounding issues to the invariant. + // For testing purposes, we are using the Weighted Pool rate, but with a tolerance to errors, just to check + // whether the rate change is contained and small, but in production we should avoid using Weighted Pool rate. maxRateTolerance = 500; } From bc7f83e40241de9c0143c4a95d69bd276de386f2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Bruno?= Date: Mon, 14 Oct 2024 16:48:29 -0300 Subject: [PATCH 19/29] Set swap fee percentage to 0 --- .../AddAndRemoveLiquidityStable.medusa.sol | 3 +- .../AddAndRemoveLiquidityWeighted.medusa.sol | 6 +- .../fuzz/AddAndRemoveLiquidity.medusa.sol | 67 +++---------------- 3 files changed, 17 insertions(+), 59 deletions(-) diff --git a/pkg/pool-stable/test/foundry/fuzz/AddAndRemoveLiquidityStable.medusa.sol b/pkg/pool-stable/test/foundry/fuzz/AddAndRemoveLiquidityStable.medusa.sol index a50260f17..45d897495 100644 --- a/pkg/pool-stable/test/foundry/fuzz/AddAndRemoveLiquidityStable.medusa.sol +++ b/pkg/pool-stable/test/foundry/fuzz/AddAndRemoveLiquidityStable.medusa.sol @@ -19,6 +19,7 @@ import { StablePoolFactory } from "../../../contracts/StablePoolFactory.sol"; import { StablePool } from "../../../contracts/StablePool.sol"; contract AddAndRemoveLiquidityStableMedusaTest is AddAndRemoveLiquidityMedusaTest { + uint256 private constant DEFAULT_SWAP_FEE = 1e16; uint256 internal constant _AMPLIFICATION_PARAMETER = 1000; constructor() AddAndRemoveLiquidityMedusaTest() { @@ -39,7 +40,7 @@ contract AddAndRemoveLiquidityStableMedusaTest is AddAndRemoveLiquidityMedusaTes vault.buildTokenConfig(tokens), _AMPLIFICATION_PARAMETER, roleAccounts, - DEFAULT_SWAP_FEE, // 1% swap fee, but test will override it + DEFAULT_SWAP_FEE, // Swap fee is set to 0 in the test constructor address(0), // No hooks false, // Do not enable donations false, // Do not disable unbalanced add/remove liquidity diff --git a/pkg/pool-weighted/test/foundry/fuzz/AddAndRemoveLiquidityWeighted.medusa.sol b/pkg/pool-weighted/test/foundry/fuzz/AddAndRemoveLiquidityWeighted.medusa.sol index 4533fbd5b..9d77ced91 100644 --- a/pkg/pool-weighted/test/foundry/fuzz/AddAndRemoveLiquidityWeighted.medusa.sol +++ b/pkg/pool-weighted/test/foundry/fuzz/AddAndRemoveLiquidityWeighted.medusa.sol @@ -19,6 +19,8 @@ import { WeightedPoolFactory } from "../../../contracts/WeightedPoolFactory.sol" import { WeightedPool } from "../../../contracts/WeightedPool.sol"; contract AddAndRemoveLiquidityWeightedMedusaTest is AddAndRemoveLiquidityMedusaTest { + uint256 private constant DEFAULT_SWAP_FEE = 1e16; + uint256 private constant _WEIGHT1 = 33e16; uint256 private constant _WEIGHT2 = 33e16; @@ -26,7 +28,7 @@ contract AddAndRemoveLiquidityWeightedMedusaTest is AddAndRemoveLiquidityMedusaT // Weighted Pool rate is not reliable, since the Pow function introduces rounding issues to the invariant. // For testing purposes, we are using the Weighted Pool rate, but with a tolerance to errors, just to check // whether the rate change is contained and small, but in production we should avoid using Weighted Pool rate. - maxRateTolerance = 500; + maxRateTolerance = 10; } function createPool( @@ -54,7 +56,7 @@ contract AddAndRemoveLiquidityWeightedMedusaTest is AddAndRemoveLiquidityMedusaT vault.buildTokenConfig(tokens), weights, roleAccounts, - DEFAULT_SWAP_FEE, // 1% swap fee + DEFAULT_SWAP_FEE, // Swap fee is set to 0 in the test constructor address(0), // No hooks false, // Do not enable donations false, // Do not disable unbalanced add/remove liquidity diff --git a/pkg/vault/test/foundry/fuzz/AddAndRemoveLiquidity.medusa.sol b/pkg/vault/test/foundry/fuzz/AddAndRemoveLiquidity.medusa.sol index 159cfe083..8f8adda6e 100644 --- a/pkg/vault/test/foundry/fuzz/AddAndRemoveLiquidity.medusa.sol +++ b/pkg/vault/test/foundry/fuzz/AddAndRemoveLiquidity.medusa.sol @@ -20,8 +20,6 @@ import "../utils/BaseMedusaTest.sol"; contract AddAndRemoveLiquidityMedusaTest is BaseMedusaTest { using FixedPoint for uint256; - uint256 internal constant DEFAULT_SWAP_FEE = 1e16; // 1% - uint256 private constant _MAX_BALANCE = 2 ** (128) - 1; // from PackedTokenBalance.sol uint256 private constant _MINIMUM_TRADE_AMOUNT = 1e6; uint256 private constant _POOL_MINIMUM_TOTAL_SUPPLY = 1e6; @@ -36,6 +34,9 @@ contract AddAndRemoveLiquidityMedusaTest is BaseMedusaTest { constructor() BaseMedusaTest() { initialRate = vault.getBptRate(address(pool)); + // Set swap fee percentage to 0, which is the worst scenario since there's no LP fees. Circumvent minimum swap + // fees, for testing purposes. + vault.manuallySetSwapFee(address(pool), 0); } /******************************************************************************* @@ -54,15 +55,7 @@ contract AddAndRemoveLiquidityMedusaTest is BaseMedusaTest { Symmetrical Add/Remove Liquidity *******************************************************************************/ - function computeAddAndRemoveLiquiditySingleToken( - uint256 tokenIndex, - uint256 exactBptAmountOut, - uint256 swapFeePercentage - ) public { - // Fee % between 0% and 100% - swapFeePercentage = bound(swapFeePercentage, 0, 1e18); - vault.manualSetStaticSwapFeePercentage(address(pool), swapFeePercentage); - + function computeAddAndRemoveLiquiditySingleToken(uint256 tokenIndex, uint256 exactBptAmountOut) public { tokenIndex = boundTokenIndex(tokenIndex); exactBptAmountOut = boundBptMint(exactBptAmountOut); @@ -95,15 +88,7 @@ contract AddAndRemoveLiquidityMedusaTest is BaseMedusaTest { bptProfit += int256(exactBptAmountOut) - int256(bptAmountIn); } - function computeRemoveAndAddLiquiditySingleToken( - uint256 tokenIndex, - uint256 tokenAmountOut, - uint256 swapFeePercentage - ) public { - // Fee % between 0% and 100% - swapFeePercentage = bound(swapFeePercentage, 0, 1e18); - vault.manualSetStaticSwapFeePercentage(address(pool), swapFeePercentage); - + function computeRemoveAndAddLiquiditySingleToken(uint256 tokenIndex, uint256 tokenAmountOut) public { tokenIndex = boundTokenIndex(tokenIndex); tokenAmountOut = boundTokenAmountOut(tokenAmountOut, tokenIndex); @@ -132,11 +117,7 @@ contract AddAndRemoveLiquidityMedusaTest is BaseMedusaTest { bptProfit += int256(bptAmountOut) - int256(bptAmountIn); } - function computeAddAndRemoveLiquidityMultiToken(uint256 exactBptAmountOut, uint256 swapFeePercentage) public { - // Fee % between 0% and 100% - swapFeePercentage = bound(swapFeePercentage, 0, 1e18); - vault.manualSetStaticSwapFeePercentage(address(pool), swapFeePercentage); - + function computeAddAndRemoveLiquidityMultiToken(uint256 exactBptAmountOut) public { (IERC20[] memory tokens, , , ) = vault.getPoolTokenInfo(address(pool)); exactBptAmountOut = boundBptMint(exactBptAmountOut); @@ -173,11 +154,7 @@ contract AddAndRemoveLiquidityMedusaTest is BaseMedusaTest { bptProfit += int256(exactBptAmountOut) - int256(bptAmountIn); } - function computeRemoveAndAddLiquidityMultiToken(uint256 exactBptAmountIn, uint256 swapFeePercentage) public { - // Fee % between 0% and 100% - swapFeePercentage = bound(swapFeePercentage, 0, 1e18); - vault.manualSetStaticSwapFeePercentage(address(pool), swapFeePercentage); - + function computeRemoveAndAddLiquidityMultiToken(uint256 exactBptAmountIn) public { exactBptAmountIn = boundBptBurn(exactBptAmountIn); uint256[] memory minAmountsOut = getMinAmountsOut(); @@ -231,14 +208,7 @@ contract AddAndRemoveLiquidityMedusaTest is BaseMedusaTest { updateRateDecrease(); } - function computeAddLiquidityUnbalanced( - uint256[] memory exactAmountsIn, - uint256 swapFeePercentage - ) public returns (uint256 bptAmountOut) { - // Fee % between 0% and 100% - swapFeePercentage = bound(swapFeePercentage, 0, 1e18); - vault.manualSetStaticSwapFeePercentage(address(pool), swapFeePercentage); - + function computeAddLiquidityUnbalanced(uint256[] memory exactAmountsIn) public returns (uint256 bptAmountOut) { exactAmountsIn = boundBalanceLength(exactAmountsIn); for (uint256 i = 0; i < exactAmountsIn.length; i++) { exactAmountsIn[i] = boundTokenDeposit(exactAmountsIn[i], i); @@ -252,13 +222,8 @@ contract AddAndRemoveLiquidityMedusaTest is BaseMedusaTest { function computeAddLiquiditySingleTokenExactOut( uint256 tokenInIndex, - uint256 exactBptAmountOut, - uint256 swapFeePercentage + uint256 exactBptAmountOut ) public returns (uint256 amountIn) { - // Fee % between 0% and 100% - swapFeePercentage = bound(swapFeePercentage, 0, 1e18); - vault.manualSetStaticSwapFeePercentage(address(pool), swapFeePercentage); - assumeValidTradeAmount(exactBptAmountOut); tokenInIndex = boundTokenIndex(tokenInIndex); exactBptAmountOut = boundBptMint(exactBptAmountOut); @@ -280,13 +245,8 @@ contract AddAndRemoveLiquidityMedusaTest is BaseMedusaTest { function computeRemoveLiquiditySingleTokenExactOut( uint256 tokenOutIndex, - uint256 exactAmountOut, - uint256 swapFeePercentage + uint256 exactAmountOut ) public returns (uint256 bptAmountIn) { - // Fee % between 0% and 100% - swapFeePercentage = bound(swapFeePercentage, 0, 1e18); - vault.manualSetStaticSwapFeePercentage(address(pool), swapFeePercentage); - assumeValidTradeAmount(exactAmountOut); tokenOutIndex = boundTokenIndex(tokenOutIndex); exactAmountOut = boundTokenAmountOut(exactAmountOut, tokenOutIndex); @@ -308,13 +268,8 @@ contract AddAndRemoveLiquidityMedusaTest is BaseMedusaTest { function computeRemoveLiquiditySingleTokenExactIn( uint256 tokenOutIndex, - uint256 exactBptAmountIn, - uint256 swapFeePercentage + uint256 exactBptAmountIn ) public returns (uint256 amountOut) { - // Fee % between 0% and 100% - swapFeePercentage = bound(swapFeePercentage, 0, 1e18); - vault.manualSetStaticSwapFeePercentage(address(pool), swapFeePercentage); - assumeValidTradeAmount(exactBptAmountIn); tokenOutIndex = boundTokenIndex(tokenOutIndex); exactBptAmountIn = boundBptBurn(exactBptAmountIn); From fb03881b594036ce220867e0ba394dcd163a66c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Bruno?= Date: Tue, 22 Oct 2024 16:19:51 -0300 Subject: [PATCH 20/29] Fix comment --- pkg/vault/test/foundry/fuzz/AddAndRemoveLiquidity.medusa.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/vault/test/foundry/fuzz/AddAndRemoveLiquidity.medusa.sol b/pkg/vault/test/foundry/fuzz/AddAndRemoveLiquidity.medusa.sol index 8f8adda6e..c2898b14c 100644 --- a/pkg/vault/test/foundry/fuzz/AddAndRemoveLiquidity.medusa.sol +++ b/pkg/vault/test/foundry/fuzz/AddAndRemoveLiquidity.medusa.sol @@ -61,7 +61,7 @@ contract AddAndRemoveLiquidityMedusaTest is BaseMedusaTest { (IERC20[] memory tokens, , , ) = vault.getPoolTokenInfo(address(pool)); - // deposit tokenAmt to mint exactly bptMintAmt + // deposit tokenAmountIn to mint exactly exactBptAmountOut medusa.prank(lp); uint256 tokenAmountIn = router.addLiquiditySingleTokenExactOut( address(pool), From 7bc5bd5eb7cdca96c026a72c60c5bd8dd106b07d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Bruno?= Date: Wed, 27 Nov 2024 15:58:21 -0300 Subject: [PATCH 21/29] Fix constant --- pkg/vault/test/foundry/fuzz/AddAndRemoveLiquidity.medusa.sol | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pkg/vault/test/foundry/fuzz/AddAndRemoveLiquidity.medusa.sol b/pkg/vault/test/foundry/fuzz/AddAndRemoveLiquidity.medusa.sol index c2898b14c..ea510ac10 100644 --- a/pkg/vault/test/foundry/fuzz/AddAndRemoveLiquidity.medusa.sol +++ b/pkg/vault/test/foundry/fuzz/AddAndRemoveLiquidity.medusa.sol @@ -20,7 +20,8 @@ import "../utils/BaseMedusaTest.sol"; contract AddAndRemoveLiquidityMedusaTest is BaseMedusaTest { using FixedPoint for uint256; - uint256 private constant _MAX_BALANCE = 2 ** (128) - 1; // from PackedTokenBalance.sol + // PackedTokenBalance.sol defines a max of 128 bits to store the balance of the pool. + uint256 private constant _MAX_BALANCE = type(uint128).max; uint256 private constant _MINIMUM_TRADE_AMOUNT = 1e6; uint256 private constant _POOL_MINIMUM_TOTAL_SUPPLY = 1e6; From d3c03d061083ac6b82303a1d201713969be43245 Mon Sep 17 00:00:00 2001 From: elshan_eth Date: Mon, 2 Dec 2024 11:17:36 +0100 Subject: [PATCH 22/29] add main swap medusa tests --- .../test/foundry/fuzz/SwapStable.medusa.sol | 54 +++++++++++ pkg/vault/medusa.json | 16 +--- .../fuzz/AddAndRemoveLiquidity.medusa.sol | 5 +- pkg/vault/test/foundry/fuzz/Swap.medusa.sol | 94 +++++++++++++++++++ .../test/foundry/utils/BaseMedusaTest.sol | 4 + 5 files changed, 158 insertions(+), 15 deletions(-) create mode 100644 pkg/pool-stable/test/foundry/fuzz/SwapStable.medusa.sol create mode 100644 pkg/vault/test/foundry/fuzz/Swap.medusa.sol diff --git a/pkg/pool-stable/test/foundry/fuzz/SwapStable.medusa.sol b/pkg/pool-stable/test/foundry/fuzz/SwapStable.medusa.sol new file mode 100644 index 000000000..f543fc8b8 --- /dev/null +++ b/pkg/pool-stable/test/foundry/fuzz/SwapStable.medusa.sol @@ -0,0 +1,54 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +pragma solidity ^0.8.24; + +import "forge-std/Test.sol"; + +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + +import { IVault } from "@balancer-labs/v3-interfaces/contracts/vault/IVault.sol"; +import "@balancer-labs/v3-interfaces/contracts/vault/VaultTypes.sol"; + +import { InputHelpers } from "@balancer-labs/v3-solidity-utils/contracts/helpers/InputHelpers.sol"; + +import { SwapMedusaTest } from "@balancer-labs/v3-vault/test/foundry/fuzz/SwapMedusaTest.medusa.sol"; + +import { StablePoolFactory } from "../../../contracts/StablePoolFactory.sol"; +import { StablePool } from "../../../contracts/StablePool.sol"; + +contract SwapStableMedusaTest is SwapMedusaTest { + uint256 private constant DEFAULT_SWAP_FEE = 1e16; + uint256 internal constant _AMPLIFICATION_PARAMETER = 1000; + + constructor() SwapMedusaTest() {} + + function createPool( + IERC20[] memory tokens, + uint256[] memory initialBalances + ) internal override returns (address newPool) { + StablePoolFactory factory = new StablePoolFactory(IVault(address(vault)), 365 days, "Factory v1", "Pool v1"); + PoolRoleAccounts memory roleAccounts; + + StablePool newPool = StablePool( + factory.create( + "Stable Pool", + "STABLE", + vault.buildTokenConfig(tokens), + _AMPLIFICATION_PARAMETER, + roleAccounts, + DEFAULT_SWAP_FEE, // Swap fee is set to 0 in the test constructor + address(0), // No hooks + false, // Do not enable donations + false, // Do not disable unbalanced add/remove liquidity + // NOTE: sends a unique salt. + bytes32(poolCreationNonce++) + ) + ); + + // Initialize liquidity of stable pool. + medusa.prank(lp); + router.initialize(address(newPool), tokens, initialBalances, 0, false, bytes("")); + + return address(newPool); + } +} diff --git a/pkg/vault/medusa.json b/pkg/vault/medusa.json index 7f422cbf3..547328038 100644 --- a/pkg/vault/medusa.json +++ b/pkg/vault/medusa.json @@ -8,16 +8,12 @@ "callSequenceLength": 10, "corpusDirectory": "medusa-corpus", "coverageEnabled": true, - "targetContracts": ["AddAndRemoveLiquidityMedusaTest"], + "targetContracts": ["SwapMedusaTest"], "predeployedContracts": {}, "targetContractsBalances": [], "constructorArgs": {}, "deployerAddress": "0x30000", - "senderAddresses": [ - "0x10000", - "0x20000", - "0x30000" - ], + "senderAddresses": ["0x10000", "0x20000", "0x30000"], "blockNumberDelayMax": 60480, "blockTimestampDelayMax": 604800, "blockGasLimit": 1250000000, @@ -46,15 +42,11 @@ }, "propertyTesting": { "enabled": true, - "testPrefixes": [ - "property_" - ] + "testPrefixes": ["property_"] }, "optimizationTesting": { "enabled": true, - "testPrefixes": [ - "optimize_" - ] + "testPrefixes": ["optimize_"] }, "targetFunctionSignatures": [], "excludeFunctionSignatures": [] diff --git a/pkg/vault/test/foundry/fuzz/AddAndRemoveLiquidity.medusa.sol b/pkg/vault/test/foundry/fuzz/AddAndRemoveLiquidity.medusa.sol index c2898b14c..323d9565b 100644 --- a/pkg/vault/test/foundry/fuzz/AddAndRemoveLiquidity.medusa.sol +++ b/pkg/vault/test/foundry/fuzz/AddAndRemoveLiquidity.medusa.sol @@ -20,7 +20,6 @@ import "../utils/BaseMedusaTest.sol"; contract AddAndRemoveLiquidityMedusaTest is BaseMedusaTest { using FixedPoint for uint256; - uint256 private constant _MAX_BALANCE = 2 ** (128) - 1; // from PackedTokenBalance.sol uint256 private constant _MINIMUM_TRADE_AMOUNT = 1e6; uint256 private constant _POOL_MINIMUM_TOTAL_SUPPLY = 1e6; @@ -324,7 +323,7 @@ contract AddAndRemoveLiquidityMedusaTest is BaseMedusaTest { uint256 tokenIndex ) internal view returns (uint256 boundedAmountIn) { (, , uint256[] memory balancesRaw, ) = vault.getPoolTokenInfo(address(pool)); - boundedAmountIn = bound(tokenAmountIn, 0, _MAX_BALANCE - balancesRaw[tokenIndex]); + boundedAmountIn = bound(tokenAmountIn, 0, MAX_BALANCE - balancesRaw[tokenIndex]); } function boundTokenAmountOut( @@ -337,7 +336,7 @@ contract AddAndRemoveLiquidityMedusaTest is BaseMedusaTest { function boundBptMint(uint256 bptAmount) internal view returns (uint256 boundedAmt) { uint256 totalSupply = BalancerPoolToken(address(pool)).totalSupply(); - boundedAmt = bound(bptAmount, 0, _MAX_BALANCE - totalSupply); + boundedAmt = bound(bptAmount, 0, MAX_BALANCE - totalSupply); } function boundBptBurn(uint256 bptAmt) internal view returns (uint256 boundedAmt) { diff --git a/pkg/vault/test/foundry/fuzz/Swap.medusa.sol b/pkg/vault/test/foundry/fuzz/Swap.medusa.sol new file mode 100644 index 000000000..0d8a66048 --- /dev/null +++ b/pkg/vault/test/foundry/fuzz/Swap.medusa.sol @@ -0,0 +1,94 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +pragma solidity ^0.8.24; + +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + +import { IBasePool } from "@balancer-labs/v3-interfaces/contracts/vault/IBasePool.sol"; +import { Rounding } from "@balancer-labs/v3-interfaces/contracts/vault/VaultTypes.sol"; + +import { FixedPoint } from "@balancer-labs/v3-solidity-utils/contracts/math/FixedPoint.sol"; +import { InputHelpers } from "@balancer-labs/v3-solidity-utils/contracts/helpers/InputHelpers.sol"; + +import { BasePoolMath } from "../../../contracts/BasePoolMath.sol"; +import { BalancerPoolToken } from "../../../contracts/BalancerPoolToken.sol"; + +import "../utils/BaseMedusaTest.sol"; + +abstract contract SwapMedusaTest is BaseMedusaTest { + int256 internal invariantBefore = 0; + int256 internal currentInvariant = 0; + + constructor() BaseMedusaTest() { + currentInvariant = computeInvariant(); + invariantBefore = currentInvariant; + + vault.manuallySetSwapFee(address(pool), 0); + } + + function optimize_currentInvariant() public view returns (int256) { + return currentInvariant; + } + + function property_currentInvariant_never_increases() public returns (bool) { + return assertInvariant(); + } + + function assertInvariant() internal returns (bool) { + updateInvariant(); + return currentInvariant <= invariantBefore; + } + + function computeInvariant() internal view returns (int256) { + (, , , uint256[] memory lastBalancesLiveScaled18) = vault.getPoolTokenInfo(address(pool)); + return int256(pool.computeInvariant(lastBalancesLiveScaled18, Rounding.ROUND_UP)); + } + + function computeSwapExactIn(uint256 tokenIndexIn, uint256 tokenIndexOut, uint256 exactAmountIn) public { + tokenIndexIn = boundTokenIndex(tokenIndexIn); + tokenIndexOut = boundTokenIndex(tokenIndexOut); + + if (tokenIndexIn == tokenIndexOut) { + tokenIndexIn = 0; + tokenIndexOut = 1; + } + + exactAmountIn = boundSwapAmount(exactAmountIn, tokenIndexIn); + + (IERC20[] memory tokens, , , ) = vault.getPoolTokenInfo(address(pool)); + + medusa.prank(alice); + router.swapSingleTokenExactIn( + address(pool), + tokens[tokenIndexIn], + tokens[tokenIndexOut], + exactAmountIn, + 0, + MAX_UINT256, + false, + bytes("") + ); + updateInvariant(); + } + + function updateInvariant() internal { + invariantBefore = currentInvariant; + currentInvariant = computeInvariant(); + + emit Debug("invariant before", uint256(invariantBefore)); + emit Debug("current invariant", uint256(currentInvariant)); + } + + function boundTokenIndex(uint256 tokenIndex) internal view returns (uint256 boundedIndex) { + uint256 len = vault.getPoolTokens(address(pool)).length; + boundedIndex = bound(tokenIndex, 0, len - 1); + } + + function boundSwapAmount( + uint256 tokenAmountIn, + uint256 tokenIndex + ) internal view returns (uint256 boundedAmountIn) { + (, , uint256[] memory balancesRaw, ) = vault.getPoolTokenInfo(address(pool)); + boundedAmountIn = bound(tokenAmountIn, 0, MAX_BALANCE - balancesRaw[tokenIndex]); + } +} diff --git a/pkg/vault/test/foundry/utils/BaseMedusaTest.sol b/pkg/vault/test/foundry/utils/BaseMedusaTest.sol index b976c2c76..c55ef5665 100644 --- a/pkg/vault/test/foundry/utils/BaseMedusaTest.sol +++ b/pkg/vault/test/foundry/utils/BaseMedusaTest.sol @@ -59,6 +59,10 @@ contract BaseMedusaTest is Test { IBasePool internal pool; uint256 internal poolCreationNonce; + uint256 internal constant MAX_UINT128 = type(uint128).max; + uint256 internal constant MAX_UINT256 = type(uint256).max; + uint256 internal constant MAX_BALANCE = MAX_UINT128; + uint256 internal constant DEFAULT_USER_BALANCE = 1e18 * 1e18; uint256 internal constant DEFAULT_INITIAL_POOL_BALANCE = 1e6 * 1e18; From 7a83ab8795a136a9bfd9510fb8f75e40b2f4170f Mon Sep 17 00:00:00 2001 From: elshan_eth Date: Tue, 3 Dec 2024 15:29:01 +0100 Subject: [PATCH 23/29] fix swap medusa tests --- pkg/vault/test/foundry/fuzz/Swap.medusa.sol | 32 +++++++++++---------- 1 file changed, 17 insertions(+), 15 deletions(-) diff --git a/pkg/vault/test/foundry/fuzz/Swap.medusa.sol b/pkg/vault/test/foundry/fuzz/Swap.medusa.sol index 0d8a66048..c15981d03 100644 --- a/pkg/vault/test/foundry/fuzz/Swap.medusa.sol +++ b/pkg/vault/test/foundry/fuzz/Swap.medusa.sol @@ -15,7 +15,9 @@ import { BalancerPoolToken } from "../../../contracts/BalancerPoolToken.sol"; import "../utils/BaseMedusaTest.sol"; -abstract contract SwapMedusaTest is BaseMedusaTest { +contract SwapMedusaTest is BaseMedusaTest { + uint256 internal constant MIN_SWAP_AMOUNT = 1e6; + int256 internal invariantBefore = 0; int256 internal currentInvariant = 0; @@ -27,21 +29,11 @@ abstract contract SwapMedusaTest is BaseMedusaTest { } function optimize_currentInvariant() public view returns (int256) { - return currentInvariant; - } - - function property_currentInvariant_never_increases() public returns (bool) { - return assertInvariant(); - } - - function assertInvariant() internal returns (bool) { - updateInvariant(); - return currentInvariant <= invariantBefore; + return -int256(currentInvariant); } - function computeInvariant() internal view returns (int256) { - (, , , uint256[] memory lastBalancesLiveScaled18) = vault.getPoolTokenInfo(address(pool)); - return int256(pool.computeInvariant(lastBalancesLiveScaled18, Rounding.ROUND_UP)); + function property_currentInvariant() public returns (bool) { + return currentInvariant >= invariantBefore; } function computeSwapExactIn(uint256 tokenIndexIn, uint256 tokenIndexOut, uint256 exactAmountIn) public { @@ -68,6 +60,11 @@ abstract contract SwapMedusaTest is BaseMedusaTest { false, bytes("") ); + + emit Debug("token index in", tokenIndexIn); + emit Debug("token index out", tokenIndexOut); + emit Debug("exact amount in", exactAmountIn); + updateInvariant(); } @@ -79,6 +76,11 @@ abstract contract SwapMedusaTest is BaseMedusaTest { emit Debug("current invariant", uint256(currentInvariant)); } + function computeInvariant() internal view returns (int256) { + (, , , uint256[] memory lastBalancesLiveScaled18) = vault.getPoolTokenInfo(address(pool)); + return int256(pool.computeInvariant(lastBalancesLiveScaled18, Rounding.ROUND_UP)); + } + function boundTokenIndex(uint256 tokenIndex) internal view returns (uint256 boundedIndex) { uint256 len = vault.getPoolTokens(address(pool)).length; boundedIndex = bound(tokenIndex, 0, len - 1); @@ -89,6 +91,6 @@ abstract contract SwapMedusaTest is BaseMedusaTest { uint256 tokenIndex ) internal view returns (uint256 boundedAmountIn) { (, , uint256[] memory balancesRaw, ) = vault.getPoolTokenInfo(address(pool)); - boundedAmountIn = bound(tokenAmountIn, 0, MAX_BALANCE - balancesRaw[tokenIndex]); + boundedAmountIn = bound(tokenAmountIn, MIN_SWAP_AMOUNT, MAX_BALANCE - balancesRaw[tokenIndex]); } } From 9bb49e5936901ca7aca9e4908fec43012f6df089 Mon Sep 17 00:00:00 2001 From: elshan_eth Date: Tue, 3 Dec 2024 16:48:24 +0100 Subject: [PATCH 24/29] fix bound --- pkg/vault/test/foundry/fuzz/Swap.medusa.sol | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/pkg/vault/test/foundry/fuzz/Swap.medusa.sol b/pkg/vault/test/foundry/fuzz/Swap.medusa.sol index c15981d03..d9e05b03c 100644 --- a/pkg/vault/test/foundry/fuzz/Swap.medusa.sol +++ b/pkg/vault/test/foundry/fuzz/Swap.medusa.sol @@ -49,6 +49,15 @@ contract SwapMedusaTest is BaseMedusaTest { (IERC20[] memory tokens, , , ) = vault.getPoolTokenInfo(address(pool)); + (, , , uint256[] memory lastBalancesLiveScaled18) = vault.getPoolTokenInfo(address(pool)); + for (uint256 i = 0; i < tokens.length; i++) { + emit Debug("balance", lastBalancesLiveScaled18[i]); + } + + emit Debug("token index in", tokenIndexIn); + emit Debug("token index out", tokenIndexOut); + emit Debug("exact amount in", exactAmountIn); + medusa.prank(alice); router.swapSingleTokenExactIn( address(pool), @@ -61,10 +70,6 @@ contract SwapMedusaTest is BaseMedusaTest { bytes("") ); - emit Debug("token index in", tokenIndexIn); - emit Debug("token index out", tokenIndexOut); - emit Debug("exact amount in", exactAmountIn); - updateInvariant(); } @@ -91,6 +96,6 @@ contract SwapMedusaTest is BaseMedusaTest { uint256 tokenIndex ) internal view returns (uint256 boundedAmountIn) { (, , uint256[] memory balancesRaw, ) = vault.getPoolTokenInfo(address(pool)); - boundedAmountIn = bound(tokenAmountIn, MIN_SWAP_AMOUNT, MAX_BALANCE - balancesRaw[tokenIndex]); + boundedAmountIn = bound(tokenAmountIn, MIN_SWAP_AMOUNT, balancesRaw[tokenIndex] / 3); } } From bd5eb700963bddd6ef0aaf3d10b142df4449db2b Mon Sep 17 00:00:00 2001 From: elshan_eth Date: Wed, 4 Dec 2024 15:42:27 +0100 Subject: [PATCH 25/29] fix swap medusa tests --- pkg/vault/test/foundry/fuzz/Swap.medusa.sol | 27 +++++-------------- .../test/foundry/utils/BaseMedusaTest.sol | 1 + 2 files changed, 8 insertions(+), 20 deletions(-) diff --git a/pkg/vault/test/foundry/fuzz/Swap.medusa.sol b/pkg/vault/test/foundry/fuzz/Swap.medusa.sol index d9e05b03c..052f8ad1b 100644 --- a/pkg/vault/test/foundry/fuzz/Swap.medusa.sol +++ b/pkg/vault/test/foundry/fuzz/Swap.medusa.sol @@ -18,25 +18,22 @@ import "../utils/BaseMedusaTest.sol"; contract SwapMedusaTest is BaseMedusaTest { uint256 internal constant MIN_SWAP_AMOUNT = 1e6; - int256 internal invariantBefore = 0; - int256 internal currentInvariant = 0; + int256 internal invariantDiff = 0; constructor() BaseMedusaTest() { - currentInvariant = computeInvariant(); - invariantBefore = currentInvariant; - vault.manuallySetSwapFee(address(pool), 0); } function optimize_currentInvariant() public view returns (int256) { - return -int256(currentInvariant); + return -int256(invariantDiff); } function property_currentInvariant() public returns (bool) { - return currentInvariant >= invariantBefore; + return invariantDiff >= 0; } function computeSwapExactIn(uint256 tokenIndexIn, uint256 tokenIndexOut, uint256 exactAmountIn) public { + int256 invariantBefore = computeInvariant(); tokenIndexIn = boundTokenIndex(tokenIndexIn); tokenIndexOut = boundTokenIndex(tokenIndexOut); @@ -49,11 +46,6 @@ contract SwapMedusaTest is BaseMedusaTest { (IERC20[] memory tokens, , , ) = vault.getPoolTokenInfo(address(pool)); - (, , , uint256[] memory lastBalancesLiveScaled18) = vault.getPoolTokenInfo(address(pool)); - for (uint256 i = 0; i < tokens.length; i++) { - emit Debug("balance", lastBalancesLiveScaled18[i]); - } - emit Debug("token index in", tokenIndexIn); emit Debug("token index out", tokenIndexOut); emit Debug("exact amount in", exactAmountIn); @@ -70,15 +62,10 @@ contract SwapMedusaTest is BaseMedusaTest { bytes("") ); - updateInvariant(); - } - - function updateInvariant() internal { - invariantBefore = currentInvariant; - currentInvariant = computeInvariant(); + int256 invariantAfter = computeInvariant(); - emit Debug("invariant before", uint256(invariantBefore)); - emit Debug("current invariant", uint256(currentInvariant)); + invariantDiff = invariantAfter - invariantBefore; + emit Debug("invariantDiff", invariantDiff); } function computeInvariant() internal view returns (int256) { diff --git a/pkg/vault/test/foundry/utils/BaseMedusaTest.sol b/pkg/vault/test/foundry/utils/BaseMedusaTest.sol index c55ef5665..783a81ec4 100644 --- a/pkg/vault/test/foundry/utils/BaseMedusaTest.sol +++ b/pkg/vault/test/foundry/utils/BaseMedusaTest.sol @@ -42,6 +42,7 @@ contract BaseMedusaTest is Test { // In certain places, console.log will not print to stdout the intended message, so we use this event to print // messages and values. event Debug(string, uint256); + event Debug(string, int256); IPermit2 internal permit2; From 58ba4c87108a4e2c8a1669a4c827e1752b559520 Mon Sep 17 00:00:00 2001 From: elshan_eth Date: Wed, 4 Dec 2024 15:43:26 +0100 Subject: [PATCH 26/29] fix medusa config --- pkg/vault/medusa.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/vault/medusa.json b/pkg/vault/medusa.json index 547328038..12221cf73 100644 --- a/pkg/vault/medusa.json +++ b/pkg/vault/medusa.json @@ -8,7 +8,7 @@ "callSequenceLength": 10, "corpusDirectory": "medusa-corpus", "coverageEnabled": true, - "targetContracts": ["SwapMedusaTest"], + "targetContracts": ["AddAndRemoveLiquidityMedusaTest", "SwapMedusaTest"], "predeployedContracts": {}, "targetContractsBalances": [], "constructorArgs": {}, From 9a53d80c8590d602a30b76c999beb083dd7de8cb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Bruno?= Date: Wed, 4 Dec 2024 16:07:12 -0300 Subject: [PATCH 27/29] Fix medusa --- pkg/vault/test/foundry/utils/BaseMedusaTest.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/vault/test/foundry/utils/BaseMedusaTest.sol b/pkg/vault/test/foundry/utils/BaseMedusaTest.sol index b976c2c76..caafb47f5 100644 --- a/pkg/vault/test/foundry/utils/BaseMedusaTest.sol +++ b/pkg/vault/test/foundry/utils/BaseMedusaTest.sol @@ -156,7 +156,7 @@ contract BaseMedusaTest is Test { minWrapAmount ); vaultExtension = new VaultExtensionMock(IVault(payable(address(newVault))), vaultAdmin); - feeController = new ProtocolFeeControllerMock(IVault(payable(address(newVault)))); + feeController = new ProtocolFeeControllerMock(IVaultMock(payable(address(newVault)))); _create3(abi.encode(vaultExtension, authorizer, feeController), vaultMockBytecode, salt); From 50603e2c008423954bc6287d04b7264c68dc952c Mon Sep 17 00:00:00 2001 From: elshan_eth Date: Thu, 5 Dec 2024 13:11:05 +0100 Subject: [PATCH 28/29] add tests for stable and weighted pools --- pkg/pool-stable/medusa.json | 2 +- .../test/foundry/fuzz/SwapStable.medusa.sol | 6 +- pkg/pool-weighted/medusa.json | 16 ++--- .../test/foundry/fuzz/SwapWeighted.medusa.sol | 68 +++++++++++++++++++ 4 files changed, 74 insertions(+), 18 deletions(-) create mode 100644 pkg/pool-weighted/test/foundry/fuzz/SwapWeighted.medusa.sol diff --git a/pkg/pool-stable/medusa.json b/pkg/pool-stable/medusa.json index 7cfb367fc..0360e0d3d 100644 --- a/pkg/pool-stable/medusa.json +++ b/pkg/pool-stable/medusa.json @@ -8,7 +8,7 @@ "callSequenceLength": 10, "corpusDirectory": "medusa-corpus", "coverageEnabled": true, - "targetContracts": ["AddAndRemoveLiquidityStableMedusaTest"], + "targetContracts": ["AddAndRemoveLiquidityStableMedusaTest", "SwapMedusaTest"], "predeployedContracts": {}, "targetContractsBalances": [], "constructorArgs": {}, diff --git a/pkg/pool-stable/test/foundry/fuzz/SwapStable.medusa.sol b/pkg/pool-stable/test/foundry/fuzz/SwapStable.medusa.sol index f543fc8b8..756c756b4 100644 --- a/pkg/pool-stable/test/foundry/fuzz/SwapStable.medusa.sol +++ b/pkg/pool-stable/test/foundry/fuzz/SwapStable.medusa.sol @@ -2,16 +2,12 @@ pragma solidity ^0.8.24; -import "forge-std/Test.sol"; - import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import { IVault } from "@balancer-labs/v3-interfaces/contracts/vault/IVault.sol"; import "@balancer-labs/v3-interfaces/contracts/vault/VaultTypes.sol"; -import { InputHelpers } from "@balancer-labs/v3-solidity-utils/contracts/helpers/InputHelpers.sol"; - -import { SwapMedusaTest } from "@balancer-labs/v3-vault/test/foundry/fuzz/SwapMedusaTest.medusa.sol"; +import { SwapMedusaTest } from "@balancer-labs/v3-vault/test/foundry/fuzz/Swap.medusa.sol"; import { StablePoolFactory } from "../../../contracts/StablePoolFactory.sol"; import { StablePool } from "../../../contracts/StablePool.sol"; diff --git a/pkg/pool-weighted/medusa.json b/pkg/pool-weighted/medusa.json index 36312e1e5..072b8db11 100644 --- a/pkg/pool-weighted/medusa.json +++ b/pkg/pool-weighted/medusa.json @@ -8,16 +8,12 @@ "callSequenceLength": 10, "corpusDirectory": "medusa-corpus", "coverageEnabled": true, - "targetContracts": ["AddAndRemoveLiquidityWeightedMedusaTest"], + "targetContracts": ["AddAndRemoveLiquidityWeightedMedusaTest", "SwapMedusaTest"], "predeployedContracts": {}, "targetContractsBalances": [], "constructorArgs": {}, "deployerAddress": "0x30000", - "senderAddresses": [ - "0x10000", - "0x20000", - "0x30000" - ], + "senderAddresses": ["0x10000", "0x20000", "0x30000"], "blockNumberDelayMax": 60480, "blockTimestampDelayMax": 604800, "blockGasLimit": 1250000000, @@ -46,15 +42,11 @@ }, "propertyTesting": { "enabled": true, - "testPrefixes": [ - "property_" - ] + "testPrefixes": ["property_"] }, "optimizationTesting": { "enabled": true, - "testPrefixes": [ - "optimize_" - ] + "testPrefixes": ["optimize_"] }, "targetFunctionSignatures": [], "excludeFunctionSignatures": [] diff --git a/pkg/pool-weighted/test/foundry/fuzz/SwapWeighted.medusa.sol b/pkg/pool-weighted/test/foundry/fuzz/SwapWeighted.medusa.sol new file mode 100644 index 000000000..22a9cd541 --- /dev/null +++ b/pkg/pool-weighted/test/foundry/fuzz/SwapWeighted.medusa.sol @@ -0,0 +1,68 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +pragma solidity ^0.8.24; + +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + +import { IVault } from "@balancer-labs/v3-interfaces/contracts/vault/IVault.sol"; +import "@balancer-labs/v3-interfaces/contracts/vault/VaultTypes.sol"; + +import { SwapMedusaTest } from "@balancer-labs/v3-vault/test/foundry/fuzz/Swap.medusa.sol"; + +import { WeightedPoolFactory } from "../../../contracts/WeightedPoolFactory.sol"; +import { WeightedPool } from "../../../contracts/WeightedPool.sol"; + +contract SwapWeightedMedusaTest is SwapMedusaTest { + uint256 private constant DEFAULT_SWAP_FEE = 1e16; + + uint256 private constant _WEIGHT1 = 33e16; + uint256 private constant _WEIGHT2 = 33e16; + + constructor() SwapMedusaTest() {} + + function createPool( + IERC20[] memory tokens, + uint256[] memory initialBalances + ) internal override returns (address newPool) { + uint256[] memory weights = new uint256[](3); + weights[0] = _WEIGHT1; + weights[1] = _WEIGHT2; + // Sum of weights should equal 100%. + weights[2] = 100e16 - (weights[0] + weights[1]); + + WeightedPoolFactory factory = new WeightedPoolFactory( + IVault(address(vault)), + 365 days, + "Factory v1", + "Pool v1" + ); + PoolRoleAccounts memory roleAccounts; + + WeightedPool newPool = WeightedPool( + factory.create( + "Weighted Pool", + "WP", + vault.buildTokenConfig(tokens), + weights, + roleAccounts, + DEFAULT_SWAP_FEE, // Swap fee is set to 0 in the test constructor + address(0), // No hooks + false, // Do not enable donations + false, // Do not disable unbalanced add/remove liquidity + // NOTE: sends a unique salt. + bytes32(poolCreationNonce++) + ) + ); + + // Cannot set the pool creator directly on a standard Balancer weighted pool factory. + vault.manualSetPoolCreator(address(newPool), lp); + + feeController.manualSetPoolCreator(address(newPool), lp); + + // Initialize liquidity of weighted pool. + medusa.prank(lp); + router.initialize(address(newPool), tokens, initialBalances, 0, false, bytes("")); + + return address(newPool); + } +} From 52e42f1c03df30e93d2d9c1ef8d2c4f5efda40d8 Mon Sep 17 00:00:00 2001 From: elshan_eth Date: Tue, 17 Dec 2024 18:09:29 +0100 Subject: [PATCH 29/29] small fixes --- pkg/vault/test/foundry/fuzz/Swap.medusa.sol | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/pkg/vault/test/foundry/fuzz/Swap.medusa.sol b/pkg/vault/test/foundry/fuzz/Swap.medusa.sol index 052f8ad1b..0ce42e937 100644 --- a/pkg/vault/test/foundry/fuzz/Swap.medusa.sol +++ b/pkg/vault/test/foundry/fuzz/Swap.medusa.sol @@ -18,22 +18,24 @@ import "../utils/BaseMedusaTest.sol"; contract SwapMedusaTest is BaseMedusaTest { uint256 internal constant MIN_SWAP_AMOUNT = 1e6; - int256 internal invariantDiff = 0; + int256 internal prevInvariant; constructor() BaseMedusaTest() { + prevInvariant = computeInvariant(); vault.manuallySetSwapFee(address(pool), 0); } function optimize_currentInvariant() public view returns (int256) { - return -int256(invariantDiff); + return -int256(computeInvariant()); } function property_currentInvariant() public returns (bool) { - return invariantDiff >= 0; + int256 currentInvariant = computeInvariant(); + return currentInvariant >= prevInvariant; } function computeSwapExactIn(uint256 tokenIndexIn, uint256 tokenIndexOut, uint256 exactAmountIn) public { - int256 invariantBefore = computeInvariant(); + prevInvariant = computeInvariant(); tokenIndexIn = boundTokenIndex(tokenIndexIn); tokenIndexOut = boundTokenIndex(tokenIndexOut); @@ -62,10 +64,8 @@ contract SwapMedusaTest is BaseMedusaTest { bytes("") ); - int256 invariantAfter = computeInvariant(); - - invariantDiff = invariantAfter - invariantBefore; - emit Debug("invariantDiff", invariantDiff); + emit Debug("prevInvariant", prevInvariant); + emit Debug("currentInvariant", computeInvariant()); } function computeInvariant() internal view returns (int256) {