diff --git a/protocol/README.md b/protocol/README.md index 91250b10d5..ef68ce71c8 100644 --- a/protocol/README.md +++ b/protocol/README.md @@ -51,9 +51,13 @@ _Note: The Beanstalk repo is a monorepo with induvidual projects inside it. See 1. Clone the repository and install dependencies +*If using Mac on Apple Silicon* +`/usr/sbin/softwareupdate --install-rosetta --agree-to-license` + ```bash git clone https://github.com/BeanstalkFarms/Beanstalk cd Beanstalk/protocol +export YARN_IGNORE_NODE=1 yarn ``` diff --git a/protocol/contracts/beanstalk/ForkSystem/TransmitInFacet.sol b/protocol/contracts/beanstalk/ForkSystem/TransmitInFacet.sol new file mode 100644 index 0000000000..bdeea15423 --- /dev/null +++ b/protocol/contracts/beanstalk/ForkSystem/TransmitInFacet.sol @@ -0,0 +1,38 @@ +/** + * SPDX-License-Identifier: MIT + **/ + +pragma solidity ^0.8.20; + +import {AppStorage} from "contracts/beanstalk/storage/AppStorage.sol"; +import {Invariable} from "contracts/beanstalk/Invariable.sol"; +import {LibTransmitIn} from "contracts/libraries/ForkSystem/LibTransmitIn.sol"; + +/** + * @title TransmitInFacet + * @author funderbrker + * @notice Destination instance logic for receiving transmitted assets from another version. + * @notice Destination has knowledge of valid Sources and their configurations at deployment time. + **/ +contract TransmitInFacet is Invariable { + AppStorage internal s; + + /** + * @notice Process the inbound migration locally. + * @dev Reverts if failure to mint assets or handle migration in. + * @dev Arguments are bytes because different sources may use different encodings. + */ + function transmitIn( + address user, + bytes[] calldata deposits, + bytes[] calldata plots, + bytes[] calldata fertilizer, + bytes calldata // data + ) external fundsSafu { + require(s.sys.supportedSourceForks[msg.sender], "Unsupported source"); + + LibTransmitIn.transmitInDeposits(user, deposits); + LibTransmitIn.transmitInPlots(user, plots); + LibTransmitIn.transmitInFertilizer(user, fertilizer); + } +} diff --git a/protocol/contracts/beanstalk/ForkSystem/TransmitOutFacet.sol b/protocol/contracts/beanstalk/ForkSystem/TransmitOutFacet.sol new file mode 100644 index 0000000000..6bbd465117 --- /dev/null +++ b/protocol/contracts/beanstalk/ForkSystem/TransmitOutFacet.sol @@ -0,0 +1,50 @@ +/** + * SPDX-License-Identifier: MIT + **/ + +pragma solidity ^0.8.20; + +import {Invariable} from "contracts/beanstalk/Invariable.sol"; +import {LibTractor} from "contracts/libraries/LibTractor.sol"; +import {LibTransmitOut} from "contracts/libraries/ForkSystem/LibTransmitOut.sol"; +import {ITransmitInFacet} from "contracts/interfaces/ITransmitInFacet.sol"; + +/** + * @title TransmitOutFacet + * @author funderbrker + * @notice Source instance logic for migrating assets to new version. + * @notice Source instance has no knowledge of possible Destinations or their configurations. + **/ +contract TransmitOutFacet is Invariable { + /** + * @notice Process the outbound migration and transfer necessary assets to destination. + * @dev Reverts if failure to burn assets or destination fails. + */ + function transmitOut( + address destination, + LibTransmitOut.SourceDeposit[] calldata sourceDeposits, + LibTransmitOut.SourcePlot[] calldata sourcePlots, + LibTransmitOut.SourceFertilizer[] calldata sourceFertilizer, + bytes calldata // data + ) external fundsSafu { + bytes[] memory deposits = LibTransmitOut.transmitOutDeposits( + LibTractor._user(), + destination, + sourceDeposits + ); + bytes[] memory plots = LibTransmitOut.transmitOutPlots(LibTractor._user(), sourcePlots); + bytes[] memory fertilizer = LibTransmitOut.transmitOutFertilizer( + LibTractor._user(), + sourceFertilizer + ); + + // Reverts if Destination fails to handle transmitted assets. + ITransmitInFacet(destination).transmitIn( + LibTractor._user(), + deposits, + plots, + fertilizer, + abi.encode("") + ); + } +} diff --git a/protocol/contracts/beanstalk/Invariable.sol b/protocol/contracts/beanstalk/Invariable.sol index 6fd6558bd0..bd7296aea9 100644 --- a/protocol/contracts/beanstalk/Invariable.sol +++ b/protocol/contracts/beanstalk/Invariable.sol @@ -186,7 +186,7 @@ abstract contract Invariable { s.sys.fert.leftoverBeans) + // unrinsed rinsable beans s.sys.silo.unripeSettings[C.UNRIPE_BEAN].balanceOfUnderlying; // unchopped underlying beans for (uint256 j; j < s.sys.fieldCount; j++) { - entitlements[i] += (s.sys.fields[j].harvestable - s.sys.fields[j].harvested); // unharvested harvestable beans + entitlements[i] += (s.sys.fields[j].harvestable - s.sys.fields[j].processed); // unharvested harvestable beans } } else if (tokens[i] == LibUnripe._getUnderlyingToken(C.UNRIPE_LP)) { entitlements[i] += s.sys.silo.unripeSettings[C.UNRIPE_LP].balanceOfUnderlying; diff --git a/protocol/contracts/beanstalk/barn/FertilizerFacet.sol b/protocol/contracts/beanstalk/barn/FertilizerFacet.sol index 877aa44d02..88df2bcd38 100644 --- a/protocol/contracts/beanstalk/barn/FertilizerFacet.sol +++ b/protocol/contracts/beanstalk/barn/FertilizerFacet.sol @@ -48,9 +48,7 @@ contract FertilizerFacet is Invariable { uint256[] calldata ids, LibTransfer.To mode ) external payable fundsSafu noSupplyChange oneOutFlow(C.BEAN) { - uint256 amount = C.fertilizer().beanstalkUpdate(LibTractor._user(), ids, s.sys.fert.bpf); - s.sys.fert.fertilizedPaidIndex += amount; - LibTransfer.sendToken(C.bean(), amount, LibTractor._user(), mode); + LibFertilizer.claimFertilized(ids, mode); } /** diff --git a/protocol/contracts/beanstalk/field/FieldFacet.sol b/protocol/contracts/beanstalk/field/FieldFacet.sol index c6c1e1a533..e51bd3831a 100644 --- a/protocol/contracts/beanstalk/field/FieldFacet.sol +++ b/protocol/contracts/beanstalk/field/FieldFacet.sol @@ -16,6 +16,8 @@ import {ReentrancyGuard} from "../ReentrancyGuard.sol"; import {Invariable} from "contracts/beanstalk/Invariable.sol"; import {LibDiamond} from "contracts/libraries/LibDiamond.sol"; import {LibMarket} from "contracts/libraries/LibMarket.sol"; +import {LibField} from "contracts/libraries/LibField.sol"; + interface IBeanstalk { function cancelPodListing(uint256 fieldId, uint256 index) external; @@ -156,6 +158,7 @@ contract FieldFacet is Invariable, ReentrancyGuard { * * Pods are "burned" when the corresponding Plot is deleted from * `s.accts[account].fields[fieldId].plots`. + * @dev If Plot has been Slashed, burn the Plot. Anyone can burn Slashed Plots. */ function harvest( uint256 fieldId, @@ -169,6 +172,7 @@ contract FieldFacet is Invariable, ReentrancyGuard { /** * @dev Ensure that each Plot is at least partially harvestable, burn the Plot, * update the total harvested, and emit a {Harvest} event. + * @dev If Plot has been Slashed, burn the Plot. */ function _harvest( uint256 fieldId, @@ -181,20 +185,24 @@ contract FieldFacet is Invariable, ReentrancyGuard { uint256 harvested = _harvestPlot(LibTractor._user(), fieldId, plots[i]); beansHarvested += harvested; } - s.sys.fields[fieldId].harvested += beansHarvested; + s.sys.fields[fieldId].processed += beansHarvested; emit Harvest(LibTractor._user(), fieldId, plots, beansHarvested); } /** * @dev Check if a Plot is at least partially Harvestable; calculate how many * Pods are Harvestable, create a new Plot if necessary. + * @dev If Plot has been Slashed, burn the Plot. */ function _harvestPlot( address account, uint256 fieldId, uint256 index ) private returns (uint256 harvestablePods) { - // Check that `account` holds this Plot. + // If Plot held by null address, it has been Slashed. Burn Plot. + if (s.accts[address(0)].fields[fieldId].plots[index] > 0) { + account = address(0); + } uint256 pods = s.accts[account].fields[fieldId].plots[index]; require(pods > 0, "Field: no plot"); @@ -203,10 +211,14 @@ contract FieldFacet is Invariable, ReentrancyGuard { // are harvestable. harvestablePods = s.sys.fields[fieldId].harvestable.sub(index); - LibMarket._cancelPodListing(LibTractor._user(), fieldId, index); + LibMarket._cancelPodListing(account, fieldId, index); - delete s.accts[account].fields[fieldId].plots[index]; - LibDibbler.removePlotIndexFromAccount(account, fieldId, index); + LibField.deletePlot(account, fieldId, index); + + // If burning a Slashed Plot, amount harvestable does not decrease. + if (account == address(0)) { + s.sys.fields[fieldId].harvestable += harvestablePods; + } // If the entire Plot was harvested, exit. if (harvestablePods >= pods) { @@ -273,19 +285,19 @@ contract FieldFacet is Invariable, ReentrancyGuard { /** * @notice Returns the number of outstanding Pods. Includes Pods that are - * currently Harvestable but have not yet been Harvested. + * currently Harvestable or Burnable but have not yet been Harvested. * @param fieldId The index of the Field to query. */ function totalPods(uint256 fieldId) public view returns (uint256) { - return s.sys.fields[fieldId].pods - s.sys.fields[fieldId].harvested; + return s.sys.fields[fieldId].pods - s.sys.fields[fieldId].processed; } /** - * @notice Returns the number of Pods that have ever been Harvested. + * @notice Returns the number of Pods that have ever been Harvested or Burned. * @param fieldId The index of the Field to query. */ - function totalHarvested(uint256 fieldId) public view returns (uint256) { - return s.sys.fields[fieldId].harvested; + function totalProcessed(uint256 fieldId) public view returns (uint256) { + return s.sys.fields[fieldId].processed; } /** @@ -293,22 +305,24 @@ contract FieldFacet is Invariable, ReentrancyGuard { * have not yet been Harvested. * @dev This is the number of Pods that Beanstalk is prepared to pay back, * but that haven’t yet been claimed via the `harvest()` function. + * @dev Cannot use this number as an index, as there is no accounting for Slashed plots. * @param fieldId The index of the Field to query. */ function totalHarvestable(uint256 fieldId) public view returns (uint256) { - return s.sys.fields[fieldId].harvestable - s.sys.fields[fieldId].harvested; + return s.sys.fields[fieldId].harvestable - s.sys.fields[fieldId].processed; } /** * @notice Returns the number of Pods that are currently Harvestable for the active Field. + * @dev Cannot use this number as an index, as there is no accounting for Slashed plots. */ function totalHarvestableForActiveField() public view returns (uint256) { - return - s.sys.fields[s.sys.activeField].harvestable - s.sys.fields[s.sys.activeField].harvested; + return totalHarvestable(s.sys.activeField); } /** * @notice Returns the number of Pods that are not yet Harvestable. Also known as the Pod Line. + * @dev Includes Slashed Pods. * @param fieldId The index of the Field to query. */ function totalUnharvestable(uint256 fieldId) public view returns (uint256) { diff --git a/protocol/contracts/beanstalk/init/InitDiamond.sol b/protocol/contracts/beanstalk/init/InitDiamond.sol index 31ff18ce3a..ac7e54e1e0 100644 --- a/protocol/contracts/beanstalk/init/InitDiamond.sol +++ b/protocol/contracts/beanstalk/init/InitDiamond.sol @@ -4,68 +4,23 @@ pragma solidity ^0.8.20; -import {AppStorage} from "../storage/AppStorage.sol"; -import {IERC165} from "@openzeppelin/contracts/utils/introspection/IERC165.sol"; -import {IERC1155} from "@openzeppelin/contracts/token/ERC1155/IERC1155.sol"; -import {IDiamondCut} from "../../interfaces/IDiamondCut.sol"; -import {IDiamondLoupe} from "../../interfaces/IDiamondLoupe.sol"; -import {LibDiamond} from "../../libraries/LibDiamond.sol"; -import {LibIncentive} from "../../libraries/LibIncentive.sol"; -import {LibCases} from "../../libraries/LibCases.sol"; -import {LibGauge} from "contracts/libraries/LibGauge.sol"; -import {C} from "../../C.sol"; -import {IBean} from "../../interfaces/IBean.sol"; -import {IWETH} from "../../interfaces/IWETH.sol"; -import {MockToken} from "../../mocks/MockToken.sol"; -import {Weather} from "contracts/beanstalk/sun/SeasonFacet/Weather.sol"; -import {LibIncentive} from "contracts/libraries/LibIncentive.sol"; +import {InitializeDiamond} from "contracts/beanstalk/init/InitializeDiamond.sol"; +import {C} from "contracts/C.sol"; /** - * @author Publius + * @author Publius, Brean * @title InitDiamond * @notice InitDiamond initializes the Beanstalk Diamond. + * A new bean token and bean:TOKEN well are deployed. + * **/ -contract InitDiamond is Weather { - address private constant PEG_PAIR = address(0xB4e16d0168e52d35CaCD2c6185b44281Ec28C9Dc); +contract InitDiamond is InitializeDiamond { + // initial reward for deploying beanstalk. + uint256 constant INIT_SUPPLY = 100e6; function init() external { - LibDiamond.DiamondStorage storage ds = LibDiamond.diamondStorage(); + initializeDiamond(C.BEAN, C.BEAN_ETH_WELL); - ds.supportedInterfaces[type(IERC165).interfaceId] = true; - ds.supportedInterfaces[type(IDiamondCut).interfaceId] = true; - ds.supportedInterfaces[type(IDiamondLoupe).interfaceId] = true; - ds.supportedInterfaces[0xd9b67a26] = true; // ERC1155 - ds.supportedInterfaces[0x0e89341c] = true; // ERC1155Metadata - - LibCases.setCasesV2(); - s.sys.weather.temp = 1; - - s.sys.season.current = 1; - s.sys.season.withdrawSeasons = 25; - s.sys.season.period = C.getSeasonPeriod(); - s.sys.season.timestamp = block.timestamp; - s.sys.season.start = s.sys.season.period > 0 - ? (block.timestamp / s.sys.season.period) * s.sys.season.period - : block.timestamp; - - s.sys.weather.thisSowTime = type(uint32).max; - s.sys.weather.lastSowTime = type(uint32).max; - s.sys.isFarm = 1; - - s.sys.usdTokenPrice[C.BEAN_ETH_WELL] = 1; - s.sys.twaReserves[C.BEAN_ETH_WELL].reserve0 = 1; - s.sys.twaReserves[C.BEAN_ETH_WELL].reserve1 = 1; - - s.sys.seedGauge.beanToMaxLpGpPerBdvRatio = 50e18; // 50% - s.sys.seedGauge.averageGrownStalkPerBdvPerSeason = 3e6; - - emit BeanToMaxLpGpPerBdvRatioChange( - s.sys.season.current, - type(uint256).max, - int80(int128(s.sys.seedGauge.beanToMaxLpGpPerBdvRatio)) - ); - emit LibGauge.UpdateAverageStalkPerBdvPerSeason( - s.sys.seedGauge.averageGrownStalkPerBdvPerSeason - ); + C.bean().mint(msg.sender, INIT_SUPPLY); } -} +} \ No newline at end of file diff --git a/protocol/contracts/beanstalk/init/InitalizeDiamond.sol b/protocol/contracts/beanstalk/init/InitializeDiamond.sol similarity index 85% rename from protocol/contracts/beanstalk/init/InitalizeDiamond.sol rename to protocol/contracts/beanstalk/init/InitializeDiamond.sol index 4df065acd8..69248c85c0 100644 --- a/protocol/contracts/beanstalk/init/InitalizeDiamond.sol +++ b/protocol/contracts/beanstalk/init/InitializeDiamond.sol @@ -20,11 +20,11 @@ import {C} from "contracts/C.sol"; /** * @author Publius, Brean - * @title InitalizeDiamond - * @notice InitalizeDiamond provides helper functions to initalize beanstalk. + * @title InitializeDiamond + * @notice InitializeDiamond provides helper functions to initialize beanstalk. **/ -contract InitalizeDiamond { +contract InitializeDiamond { AppStorage internal s; // INITIAL CONSTANTS // @@ -62,24 +62,24 @@ contract InitalizeDiamond { event BeanToMaxLpGpPerBdvRatioChange(uint256 indexed season, uint256 caseId, int80 absChange); /** - * @notice Initalizes the diamond with base conditions. - * @dev the base initalization initalizes various parameters, + * @notice Initializes the diamond with base conditions. + * @dev the base initalization initializes various parameters, * as well as whitelists the bean and bean:TKN pools. */ - function initalizeDiamond(address bean, address beanTokenWell) internal { + function initializeDiamond(address bean, address beanTokenWell) internal { addInterfaces(); - initalizeSeason(); - initalizeField(); - initalizeFarmAndTractor(); - initalizeSilo(uint16(s.sys.season.current)); - initalizeSeedGauge(INIT_BEAN_TO_MAX_LP_GP_RATIO, INIT_AVG_GSPBDV); + initializeSeason(); + initializeField(); + initializeFarmAndTractor(); + initializeSilo(uint16(s.sys.season.current)); + initializeSeedGauge(INIT_BEAN_TO_MAX_LP_GP_RATIO, INIT_AVG_GSPBDV); address[] memory tokens = new address[](2); tokens[0] = bean; tokens[1] = beanTokenWell; // note: bean and assets that are not in the gauge system - // do not need to initalize the gauge system. + // do not need to initialize the gauge system. Implementation memory impl = Implementation(address(0), bytes4(0), bytes1(0)); Implementation memory liquidityWeightImpl = Implementation( address(0), @@ -128,12 +128,16 @@ contract InitalizeDiamond { // init usdTokenPrice. C.Bean_eth_well should be // a bean well w/ the native token of the network. - s.sys.usdTokenPrice[C.BEAN_ETH_WELL] = 1; + s.sys.usdTokenPrice[beanTokenWell] = 1; s.sys.twaReserves[beanTokenWell].reserve0 = 1; s.sys.twaReserves[beanTokenWell].reserve1 = 1; // init tractor. LibTractor._tractorStorage().activePublisher = payable(address(1)); + + // Set the sources that can migrate into this Beanstalk. + // TODO: Change this based on each fork instance. + // s.sys.supportedSourceForks[address()] = true; } /** @@ -147,38 +151,38 @@ contract InitalizeDiamond { } /** - * @notice Initalizes field parameters. + * @notice Initializes field parameters. */ - function initalizeField() internal { + function initializeField() internal { s.sys.weather.temp = 1; s.sys.weather.thisSowTime = type(uint32).max; s.sys.weather.lastSowTime = type(uint32).max; } /** - * @notice Initalizes season parameters. + * @notice Initializes season parameters. */ - function initalizeSeason() internal { + function initializeSeason() internal { // set current season to 1. s.sys.season.current = 1; // set withdraw seasons to 0. Kept here for verbosity. s.sys.season.withdrawSeasons = 0; - // initalize the duration of 1 season in seconds. + // initialize the duration of 1 season in seconds. s.sys.season.period = C.getSeasonPeriod(); - // initalize current timestamp. + // initialize current timestamp. s.sys.season.timestamp = block.timestamp; - // initalize the start timestamp. + // initialize the start timestamp. // Rounds down to the nearest hour // if needed. s.sys.season.start = s.sys.season.period > 0 ? (block.timestamp / s.sys.season.period) * s.sys.season.period : block.timestamp; - // initalizes the cases that beanstalk uses + // initializes the cases that beanstalk uses // to change certain parameters of itself. setCases(); @@ -186,29 +190,29 @@ contract InitalizeDiamond { } /** - * @notice Initalize the cases for the diamond. + * @notice Initialize the cases for the diamond. */ function setCases() internal { LibCases.setCasesV2(); } /** - * Initalizes silo parameters. + * Initializes silo parameters. */ - function initalizeSilo(uint16 season) internal { - // initalize when the silo started silo V3. + function initializeSilo(uint16 season) internal { + // initialize when the silo started silo V3. s.sys.season.stemStartSeason = season; s.sys.season.stemScaleSeason = season; } - function initalizeSeedGauge( + function initializeSeedGauge( uint128 beanToMaxLpGpRatio, uint128 averageGrownStalkPerBdvPerSeason ) internal { - // initalize the ratio of bean to max lp gp per bdv. + // initialize the ratio of bean to max lp gp per bdv. s.sys.seedGauge.beanToMaxLpGpPerBdvRatio = beanToMaxLpGpRatio; - // initalize the average grown stalk per bdv per season. + // initialize the average grown stalk per bdv per season. s.sys.seedGauge.averageGrownStalkPerBdvPerSeason = averageGrownStalkPerBdvPerSeason; // emit events. @@ -268,7 +272,7 @@ contract InitalizeDiamond { s.sys.seedGaugeSettings.excessivePriceThreshold = EXCESSIVE_PRICE_THRESHOLD; } - function initalizeFarmAndTractor() public { + function initializeFarmAndTractor() public { s.sys.isFarm = 1; LibTractor._resetPublisher(); } diff --git a/protocol/contracts/beanstalk/init/newInitDiamond.sol b/protocol/contracts/beanstalk/init/newInitDiamond.sol deleted file mode 100644 index a82740629f..0000000000 --- a/protocol/contracts/beanstalk/init/newInitDiamond.sol +++ /dev/null @@ -1,26 +0,0 @@ -/* - SPDX-License-Identifier: MIT -*/ - -pragma solidity ^0.8.20; - -import {InitalizeDiamond} from "contracts/beanstalk/init/InitalizeDiamond.sol"; -import {C} from "contracts/C.sol"; - -/** - * @author Publius, Brean - * @title InitDiamond - * @notice InitDiamond initializes the Beanstalk Diamond. - * A new bean token and bean:TOKEN well are deployed. - * - **/ -contract InitDiamond is InitalizeDiamond { - // initial reward for deploying beanstalk. - uint256 constant INIT_SUPPLY = 100e6; - - function init() external { - initalizeDiamond(C.BEAN, C.BEAN_ETH_WELL); - - C.bean().mint(msg.sender, INIT_SUPPLY); - } -} diff --git a/protocol/contracts/beanstalk/init/reseed/L2/ReseedField.sol b/protocol/contracts/beanstalk/init/reseed/L2/ReseedField.sol index 8fc964f7d3..b795b982d7 100644 --- a/protocol/contracts/beanstalk/init/reseed/L2/ReseedField.sol +++ b/protocol/contracts/beanstalk/init/reseed/L2/ReseedField.sol @@ -61,10 +61,6 @@ contract ReseedField { require(totalPods >= harvestable, "ReseedField: harvestable mismatch"); require(harvestable >= harvested, "ReseedField: harvested mismatch"); - s.sys.field.pods = totalPods; - s.sys.field.harvestable = harvestable; - s.sys.field.harvested = harvested; - // soil demand initialization. s.sys.weather.thisSowTime = type(uint32).max; s.sys.weather.lastSowTime = type(uint32).max; diff --git a/protocol/contracts/beanstalk/market/MarketplaceFacet/PodTransfer.sol b/protocol/contracts/beanstalk/market/MarketplaceFacet/PodTransfer.sol index cd12a69362..263f77d9e1 100644 --- a/protocol/contracts/beanstalk/market/MarketplaceFacet/PodTransfer.sol +++ b/protocol/contracts/beanstalk/market/MarketplaceFacet/PodTransfer.sol @@ -5,7 +5,7 @@ pragma solidity ^0.8.20; import {ReentrancyGuard} from "contracts/beanstalk/ReentrancyGuard.sol"; -import {LibDibbler} from "contracts/libraries/LibDibbler.sol"; +import {LibField} from "contracts/libraries/LibField.sol"; import {C} from "contracts/C.sol"; /** @@ -53,16 +53,11 @@ contract PodTransfer is ReentrancyGuard { uint256 amount ) internal { require(from != to, "Field: Cannot transfer Pods to oneself."); - insertPlot(to, fieldId, index + start, amount); + LibField.createPlot(to, fieldId, index + start, amount); removePlot(from, fieldId, index, start, amount + start); emit PlotTransfer(from, to, index + start, amount); } - function insertPlot(address account, uint256 fieldId, uint256 index, uint256 amount) internal { - s.accts[account].fields[fieldId].plots[index] = amount; - s.accts[account].fields[fieldId].plotIndexes.push(index); - } - function removePlot( address account, uint256 fieldId, @@ -75,8 +70,7 @@ contract PodTransfer is ReentrancyGuard { if (start > 0) { s.accts[account].fields[fieldId].plots[index] = start; } else { - delete s.accts[account].fields[fieldId].plots[index]; - LibDibbler.removePlotIndexFromAccount(account, fieldId, index); + LibField.deletePlot(account, fieldId, index); } if (amountAfterEnd > 0) { diff --git a/protocol/contracts/beanstalk/silo/ConvertFacet.sol b/protocol/contracts/beanstalk/silo/ConvertFacet.sol index 3eb2b19171..e78c9efe12 100644 --- a/protocol/contracts/beanstalk/silo/ConvertFacet.sol +++ b/protocol/contracts/beanstalk/silo/ConvertFacet.sol @@ -98,7 +98,7 @@ contract ConvertFacet is Invariable, ReentrancyGuard { uint256 newBdv = LibTokenSilo.beanDenominatedValue(toToken, toAmount); toBdv = newBdv > fromBdv ? newBdv : fromBdv; - toStem = LibConvert._depositTokensForConvert(toToken, toAmount, toBdv, pipeData.grownStalk); + toStem = LibConvert._depositTokensForConvert(LibTractor._user(), toToken, toAmount, toBdv, pipeData.grownStalk); emit Convert(LibTractor._user(), fromToken, toToken, fromAmount, toAmount); } diff --git a/protocol/contracts/beanstalk/silo/PipelineConvertFacet.sol b/protocol/contracts/beanstalk/silo/PipelineConvertFacet.sol index 6d32afa004..9e6c0fccb1 100644 --- a/protocol/contracts/beanstalk/silo/PipelineConvertFacet.sol +++ b/protocol/contracts/beanstalk/silo/PipelineConvertFacet.sol @@ -104,7 +104,7 @@ contract PipelineConvertFacet is Invariable, ReentrancyGuard { advancedFarmCalls ); - toStem = LibConvert._depositTokensForConvert(outputToken, toAmount, toBdv, grownStalk); + toStem = LibConvert._depositTokensForConvert(LibTractor._user(), outputToken, toAmount, toBdv, grownStalk); emit Convert(LibTractor._user(), inputToken, outputToken, fromAmount, toAmount); } diff --git a/protocol/contracts/beanstalk/silo/SiloFacet/SiloFacet.sol b/protocol/contracts/beanstalk/silo/SiloFacet/SiloFacet.sol index 4bde47a383..5753dbefdb 100644 --- a/protocol/contracts/beanstalk/silo/SiloFacet/SiloFacet.sol +++ b/protocol/contracts/beanstalk/silo/SiloFacet/SiloFacet.sol @@ -86,7 +86,7 @@ contract SiloFacet is Invariable, TokenSilo { uint256 amount, LibTransfer.To mode ) external payable fundsSafu noSupplyChange oneOutFlow(token) mowSender(token) nonReentrant { - _withdrawDeposit(LibTractor._user(), token, stem, amount); + LibSilo._withdrawDeposit(LibTractor._user(), token, stem, amount); LibTransfer.sendToken(IERC20(token), amount, LibTractor._user(), mode); } @@ -110,7 +110,7 @@ contract SiloFacet is Invariable, TokenSilo { uint256[] calldata amounts, LibTransfer.To mode ) external payable fundsSafu noSupplyChange oneOutFlow(token) mowSender(token) nonReentrant { - uint256 amount = _withdrawDeposits(LibTractor._user(), token, stems, amounts); + uint256 amount = LibSilo._withdrawDeposits(LibTractor._user(), token, stems, amounts); LibTransfer.sendToken(IERC20(token), amount, LibTractor._user(), mode); } diff --git a/protocol/contracts/beanstalk/silo/SiloFacet/SiloGettersFacet.sol b/protocol/contracts/beanstalk/silo/SiloFacet/SiloGettersFacet.sol index b15da9244a..2de45407ec 100644 --- a/protocol/contracts/beanstalk/silo/SiloFacet/SiloGettersFacet.sol +++ b/protocol/contracts/beanstalk/silo/SiloFacet/SiloGettersFacet.sol @@ -541,7 +541,7 @@ contract SiloGettersFacet is ReentrancyGuard { } /** - * @notice returns the season in which beanstalk initalized siloV3. + * @notice returns the season in which beanstalk initialized siloV3. */ function stemStartSeason() external view virtual returns (uint16) { return s.sys.season.stemStartSeason; diff --git a/protocol/contracts/beanstalk/silo/SiloFacet/TokenSilo.sol b/protocol/contracts/beanstalk/silo/SiloFacet/TokenSilo.sol index d0927f8e8a..d1b0f75855 100644 --- a/protocol/contracts/beanstalk/silo/SiloFacet/TokenSilo.sol +++ b/protocol/contracts/beanstalk/silo/SiloFacet/TokenSilo.sol @@ -174,176 +174,6 @@ contract TokenSilo is ReentrancyGuard { LibSilo.mintGerminatingStalk(account, uint128(stalk), side); } - //////////////////////// WITHDRAW //////////////////////// - - /** - * @notice Handles withdraw accounting. - * - * - {LibSilo._removeDepositFromAccount} calculates the stalk - * assoicated with a given deposit, and removes the deposit from the account. - * emits `RemoveDeposit` and `TransferSingle` events. - * - * - {_withdraw} updates the total value deposited in the silo, and burns - * the stalk assoicated with the deposits. - * - */ - function _withdrawDeposit(address account, address token, int96 stem, uint256 amount) internal { - // Remove the Deposit from `account`. - ( - uint256 initalStalkRemoved, - uint256 grownStalkRemoved, - uint256 bdvRemoved, - GerminationSide side - ) = LibSilo._removeDepositFromAccount( - account, - token, - stem, - amount, - LibTokenSilo.Transfer.emitTransferSingle - ); - if (side == GerminationSide.NOT_GERMINATING) { - // remove the deposit from totals - _withdraw( - account, - token, - amount, - bdvRemoved, - initalStalkRemoved.add(grownStalkRemoved) - ); - } else { - // remove deposit from germination, and burn the grown stalk. - // grown stalk does not germinate and is not counted in germinating totals. - _withdrawGerminating(account, token, amount, bdvRemoved, initalStalkRemoved, side); - - if (grownStalkRemoved > 0) { - LibSilo.burnActiveStalk(account, grownStalkRemoved); - } - } - } - - /** - * @notice Handles withdraw accounting for multiple deposits. - * - * - {LibSilo._removeDepositsFromAccount} removes the deposits from the account, - * and returns the total tokens, stalk, and bdv removed from the account. - * - * - {_withdraw} updates the total value deposited in the silo, and burns - * the stalk assoicated with the deposits. - * - */ - function _withdrawDeposits( - address account, - address token, - int96[] calldata stems, - uint256[] calldata amounts - ) internal returns (uint256) { - require(stems.length == amounts.length, "Silo: Crates, amounts are diff lengths."); - - LibSilo.AssetsRemoved memory ar = LibSilo._removeDepositsFromAccount( - account, - token, - stems, - amounts, - LibSilo.ERC1155Event.EMIT_BATCH_EVENT - ); - - // withdraw deposits that are not germinating. - if (ar.active.tokens > 0) { - _withdraw(account, token, ar.active.tokens, ar.active.bdv, ar.active.stalk); - } - - // withdraw Germinating deposits from odd seasons - if (ar.odd.tokens > 0) { - _withdrawGerminating( - account, - token, - ar.odd.tokens, - ar.odd.bdv, - ar.odd.stalk, - GerminationSide.ODD - ); - } - - // withdraw Germinating deposits from even seasons - if (ar.even.tokens > 0) { - _withdrawGerminating( - account, - token, - ar.even.tokens, - ar.even.bdv, - ar.even.stalk, - GerminationSide.EVEN - ); - } - - if (ar.grownStalkFromGermDeposits > 0) { - LibSilo.burnActiveStalk(account, ar.grownStalkFromGermDeposits); - } - - // we return the summation of all tokens removed from the silo. - // to be used in {SiloFacet.withdrawDeposits}. - return ar.active.tokens.add(ar.odd.tokens).add(ar.even.tokens); - } - - /** - * @dev internal helper function for withdraw accounting. - */ - function _withdraw( - address account, - address token, - uint256 amount, - uint256 bdv, - uint256 stalk - ) private { - // Decrement total deposited in the silo. - LibTokenSilo.decrementTotalDeposited(token, amount, bdv); - // Burn stalk and roots associated with the stalk. - LibSilo.burnActiveStalk(account, stalk); - } - - /** - * @dev internal helper function for withdraw accounting with germination. - * @param side determines whether to withdraw from odd or even germination. - */ - function _withdrawGerminating( - address account, - address token, - uint256 amount, - uint256 bdv, - uint256 stalk, - GerminationSide side - ) private { - // Deposited Earned Beans do not germinate. Thus, when withdrawing a Bean Deposit - // with a Germinating Stem, Beanstalk needs to determine how many of the Beans - // were Planted vs Deposited from a Circulating/Farm balance. - // If a Farmer's Germinating Stalk for a given Season is less than the number of - // Deposited Beans in that Season, then it is assumed that the excess Beans were - // Planted. - if (token == C.BEAN) { - (uint256 germinatingStalk, uint256 earnedBeansStalk) = LibSilo.checkForEarnedBeans( - account, - stalk, - side - ); - // set the bdv and amount accordingly to the stalk. - stalk = germinatingStalk; - uint256 earnedBeans = earnedBeansStalk.div(C.STALK_PER_BEAN); - amount = amount - earnedBeans; - // note: the 1 Bean = 1 BDV assumption cannot be made here for input `bdv`, - // as a user converting a germinating LP deposit into bean may have an inflated bdv. - // thus, amount and bdv are decremented by the earnedBeans (where the 1 Bean = 1 BDV assumption can be made). - bdv = bdv - earnedBeans; - - // burn the earned bean stalk (which is active). - LibSilo.burnActiveStalk(account, earnedBeansStalk); - // calculate earnedBeans and decrement totalDeposited. - LibTokenSilo.decrementTotalDeposited(C.BEAN, earnedBeans, earnedBeans); - } - // Decrement from total germinating. - LibTokenSilo.decrementTotalGerminating(token, amount, bdv, side); // Decrement total Germinating in the silo. - LibSilo.burnGerminatingStalk(account, uint128(stalk), side); // Burn stalk and roots associated with the stalk. - } - //////////////////////// TRANSFER //////////////////////// /** diff --git a/protocol/contracts/beanstalk/storage/System.sol b/protocol/contracts/beanstalk/storage/System.sol index 336141b4d9..da1966190d 100644 --- a/protocol/contracts/beanstalk/storage/System.sol +++ b/protocol/contracts/beanstalk/storage/System.sol @@ -64,7 +64,6 @@ struct System { bytes32[16] _buffer_1; bytes32[144] casesV2; Silo silo; - Field field; Fertilizer fert; Season season; Weather weather; @@ -75,6 +74,7 @@ struct System { mapping(address => Implementation) oracleImplementation; SeedGaugeSettings seedGaugeSettings; SeasonOfPlenty sop; + mapping(address => bool) supportedSourceForks; } /** @@ -106,15 +106,19 @@ struct Silo { /** * @notice System-level Field state variables. * @param pods The pod index; the total number of Pods ever minted. - * @param harvested The harvested index; the total number of Pods that have ever been Harvested. - * @param harvestable The harvestable index; the total number of Pods that have ever been Harvestable. Included previously Harvested Beans. + * @param processed Amount of Pods that have ever been Harvested or Slashed. + * @param harvestable Index of Pods that have ever been Harvestable. Included Processed Pods. + * @param latestTransmittedPlotIndex The index of the latest Plot that has been transmitted in. + * @param latestTransmittedPlotOwner The owner of the latest Plot that has been transmitted in. * @param _buffer Reserved storage for future expansion. */ struct Field { uint256 pods; - uint256 harvested; + uint256 processed; uint256 harvestable; - bytes32[8] _buffer; + uint256 latestTransmittedPlotIndex; + address latestTransmittedPlotOwner; + bytes32[6] _buffer; } /** diff --git a/protocol/contracts/interfaces/IFertilizer.sol b/protocol/contracts/interfaces/IFertilizer.sol index febb564d20..9f9ba49891 100644 --- a/protocol/contracts/interfaces/IFertilizer.sol +++ b/protocol/contracts/interfaces/IFertilizer.sol @@ -13,6 +13,8 @@ interface IFertilizer { uint128 bpf ) external returns (uint256); function beanstalkMint(address account, uint256 id, uint128 amount, uint128 bpf) external; + function beanstalkBurn(address account, uint256 id, uint128 amount, uint128 bpf) external; + function balanceOf(address account, uint256 id) external view returns (uint256); function balanceOfFertilized( address account, uint256[] memory ids diff --git a/protocol/contracts/interfaces/IMockFBeanstalk.sol b/protocol/contracts/interfaces/IMockFBeanstalk.sol index d84708d5bd..8e4c6c010b 100644 --- a/protocol/contracts/interfaces/IMockFBeanstalk.sol +++ b/protocol/contracts/interfaces/IMockFBeanstalk.sol @@ -251,6 +251,31 @@ interface IMockFBeanstalk { bool isSoppable; } + struct SourceDeposit { + address token; + uint256 amount; + int96 stem; + uint256[] sourceMinTokenAmountsOut; // LP only + uint256 destMinLpOut; // LP only + uint256 _grownStalk; // not stalk // need to change logic + uint256 _burnedBeans; + address _transferredToken; // NOTE what if LP type is not supported at destination? + uint256 _transferredTokenAmount; + } + + struct SourcePlot { + uint256 fieldId; + uint256 index; + uint256 amount; + uint256 existingIndex; + } + + struct SourceFertilizer { + uint128 id; + uint256 amount; + uint128 _remainingBpf; + } + error AddressEmptyCode(address target); error AddressInsufficientBalance(address account); error ECDSAInvalidSignature(); @@ -1238,6 +1263,22 @@ interface IMockFBeanstalk { function migrationNeeded(address account) external view returns (bool hasMigrated); + function transmitOut( + address destination, + SourceDeposit[] calldata sourceDeposits, + SourcePlot[] calldata sourcePlots, + SourceFertilizer[] calldata sourceFertilizer, + bytes calldata data + ) external; + + function transmitIn( + address user, + bytes[] calldata deposits, + bytes[] calldata plots, + bytes[] calldata fertilizer, + bytes calldata data + ) external; + function mintBeans(address to, uint256 amount) external; function mintFertilizer( @@ -1266,7 +1307,7 @@ interface IMockFBeanstalk { GerminationSide side ) external; - function mockInitalizeGaugeForToken( + function mockInitializeGaugeForToken( address token, bytes4 gaugePointSelector, bytes4 liquidityWeightSelector, @@ -1617,6 +1658,8 @@ interface IMockFBeanstalk { uint8 mode ) external payable returns (uint256 pods); + function plot(address account, uint256 fieldId, uint256 index) external view returns (uint256); + function stemStartSeason() external view returns (uint16); function stemTipForToken(address token) external view returns (int96 _stemTip); diff --git a/protocol/contracts/interfaces/ITransmitInFacet.sol b/protocol/contracts/interfaces/ITransmitInFacet.sol new file mode 100644 index 0000000000..525bb37aa3 --- /dev/null +++ b/protocol/contracts/interfaces/ITransmitInFacet.sol @@ -0,0 +1,13 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.20; + +interface ITransmitInFacet { + function transmitIn( + address user, + bytes[] calldata deposits, + bytes[] calldata plots, + bytes[] calldata fertilizer, + bytes calldata data + ) external; +} diff --git a/protocol/contracts/interfaces/ITransmitOutFacet.sol b/protocol/contracts/interfaces/ITransmitOutFacet.sol new file mode 100644 index 0000000000..afaa31e5c8 --- /dev/null +++ b/protocol/contracts/interfaces/ITransmitOutFacet.sol @@ -0,0 +1,15 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.20; + +import {LibTransmitOut} from "contracts/libraries/ForkSystem/LibTransmitOut.sol"; + +interface ITransmitOutFacet { + function transmitOut( + address destination, + LibTransmitOut.SourceDeposit[] calldata sourceDeposits, + LibTransmitOut.SourcePlot[] calldata sourcePlots, + LibTransmitOut.SourceFertilizer[] calldata sourceFertilizer, + bytes calldata data + ) external; +} diff --git a/protocol/contracts/libraries/Convert/LibConvert.sol b/protocol/contracts/libraries/Convert/LibConvert.sol index 36378c386b..6e1bad32c5 100644 --- a/protocol/contracts/libraries/Convert/LibConvert.sol +++ b/protocol/contracts/libraries/Convert/LibConvert.sol @@ -510,6 +510,7 @@ library LibConvert { } function _depositTokensForConvert( + address user, address token, uint256 amount, uint256 bdv, @@ -529,21 +530,21 @@ library LibConvert { if (side == GerminationSide.NOT_GERMINATING) { LibTokenSilo.incrementTotalDeposited(token, amount, bdv); LibSilo.mintActiveStalk( - LibTractor._user(), + user, bdv.mul(LibTokenSilo.stalkIssuedPerBdv(token)).add(grownStalk) ); } else { LibTokenSilo.incrementTotalGerminating(token, amount, bdv, side); // safeCast not needed as stalk is <= max(uint128) LibSilo.mintGerminatingStalk( - LibTractor._user(), + user, uint128(bdv.mul(LibTokenSilo.stalkIssuedPerBdv(token))), side ); - LibSilo.mintActiveStalk(LibTractor._user(), grownStalk); + LibSilo.mintActiveStalk(user, grownStalk); } LibTokenSilo.addDepositToAccount( - LibTractor._user(), + user, token, stem, amount, diff --git a/protocol/contracts/libraries/ForkSystem/LibTransmitIn.sol b/protocol/contracts/libraries/ForkSystem/LibTransmitIn.sol new file mode 100644 index 0000000000..1a8ffb89eb --- /dev/null +++ b/protocol/contracts/libraries/ForkSystem/LibTransmitIn.sol @@ -0,0 +1,226 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.20; + +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import {SafeCast} from "@openzeppelin/contracts/utils/math/SafeCast.sol"; + +import {C} from "contracts/C.sol"; +import {AppStorage} from "contracts/libraries/LibAppStorage.sol"; +import {LibConvert} from "contracts/libraries/Convert/LibConvert.sol"; +import {LibWellBdv} from "contracts/libraries/Well/LibWellBdv.sol"; +import {LibWell} from "contracts/libraries/Well/LibWell.sol"; +import {IWell} from "contracts/interfaces/basin/IWell.sol"; +import {LibFertilizer} from "contracts/libraries/LibFertilizer.sol"; +import {LibAppStorage} from "contracts/libraries/LibAppStorage.sol"; +import {LibWhitelistedTokens} from "contracts/libraries/Silo/LibWhitelistedTokens.sol"; +import {LibField} from "contracts/libraries/LibField.sol"; + +/** + * @title LibTransmitIn + * @author funderbrker + * @notice Library handling inbound migration and minting of protocol assets. + */ +library LibTransmitIn { + using SafeCast for uint256; + + event DepositTransmittedIn(address indexed user, SourceDeposit deposit); + + event FertilizerTransmittedIn(address indexed user, SourceFertilizer fertilizer); + + event PlotTransmittedIn(address indexed user, SourcePlot sourcePlot); + + // Definitions must match source migration definitions. May require multiple definitions. + struct SourceDeposit { + address token; + uint256 amount; + int96 stem; // unused + uint256[] sourceMinTokenAmountsOut; // LP only + uint256 destMinLpOut; // LP only + uint256 _grownStalk; // not stalk // need to change logic + uint256 _burnedBeans; + address _transferredToken; // NOTE what if LP type is not supported at destination? + uint256 _transferredTokenAmount; + } + + struct SourcePlot { + uint256 fieldId; + uint256 index; + uint256 amount; + uint256 existingIndex; + } + + struct SourceFertilizer { + uint128 id; + uint256 amount; + uint128 _remainingBpf; + } + + uint256 internal constant IN_FIELD = 1; + + // Mint assets locally. + // Underlying external ERC20s have already been transferred to destination beanstalk. + // msg.sender == source instance + // Use _depositTokensForConvert() to calculate stem (includes germination logic, germiantion safety provided by source beanstalk). + function transmitInDeposits(address user, bytes[] calldata deposits) internal { + if (deposits.length == 0) return; + address[] memory whitelistedTokens = LibWhitelistedTokens.getWhitelistedTokens(); + for (uint256 i = 0; i < deposits.length; i++) { + SourceDeposit memory deposit = abi.decode(deposits[i], (SourceDeposit)); + // NOTE give 1:1 token + BDV ?? + C.bean().mint(address(this), deposit._burnedBeans); + + // If LP deposit. + if (deposit._transferredToken != address(0)) { + bool lpMatched; + // Look for corresponding whitelisted well. + for (uint j; j < whitelistedTokens.length; j++) { + address well = whitelistedTokens[j]; + if ( + address(LibWell.getNonBeanTokenFromWell(well)) != deposit._transferredToken + ) { + continue; + } + uint256[] memory tokenAmountsIn = new uint256[](2); + tokenAmountsIn[LibWell.getBeanIndexFromWell(well)] = deposit._burnedBeans; + tokenAmountsIn[LibWell.getNonBeanIndexFromWell(well)] = deposit + ._transferredTokenAmount; + + IERC20(deposit._transferredToken).approve( + well, + uint256(deposit._transferredTokenAmount) + ); + C.bean().approve(well, deposit._burnedBeans); + uint256 lpAmount = IWell(whitelistedTokens[j]).addLiquidity( + tokenAmountsIn, + deposit.destMinLpOut, + address(this), + block.number + ); + + LibConvert._depositTokensForConvert( + user, + well, + lpAmount, // amount + LibWellBdv.bdv(well, lpAmount), // bdv + deposit._grownStalk + ); + lpMatched = true; + break; + } + require(lpMatched, "LP not whitelisted"); + } + // else if Bean deposit. + else { + // Update Beanstalk state and mint Beans to user. Bypasses standard minting calcs. + LibConvert._depositTokensForConvert( + user, + C.BEAN, + deposit._burnedBeans, // amount + deposit._burnedBeans, // bdv + deposit._grownStalk + ); + } + emit DepositTransmittedIn(user, deposit); + } + } + + /** + * @notice Mint equivalent fertilizer to the user such that they retain all remaining BPF. + */ + function transmitInFertilizer(address user, bytes[] memory fertilizer) internal { + AppStorage storage s = LibAppStorage.diamondStorage(); + if (fertilizer.length == 0) return; + for (uint256 i = 0; i < fertilizer.length; i++) { + SourceFertilizer memory sourceFert = abi.decode(fertilizer[i], (SourceFertilizer)); + + // Update Beanstalk state and mint Fert to user. Bypasses standard minting calcs. + LibFertilizer.IncrementFertState(sourceFert.amount, sourceFert._remainingBpf); + C.fertilizer().beanstalkMint( + user, + s.sys.fert.bpf + sourceFert._remainingBpf, + sourceFert.amount.toUint128(), + s.sys.fert.bpf + ); + emit FertilizerTransmittedIn(user, sourceFert); + } + } + + /** + * @notice Create Plots in alternative Field and assign them to the migrating user. + * @dev Holes between transmitted-in Plots are filled with Slashed Plots. + * @dev Assumes that no sowing will occur in the same Field as inbound migrations. + * + * This design is painfully contorted. This is necessary to maintain constant time + * harvest operations on a pod line that may contain holes. Null plots represent + * hols in such a way that all pods can be accounted for and a plot can be acted + * on without any knowledge of other plots. + */ + function transmitInPlots(address user, bytes[] memory plots) internal { + AppStorage storage s = LibAppStorage.diamondStorage(); + if (plots.length == 0) return; + // require(s.sys.fields[IN_FIELD].sourcePlot.existingIndex); prev plot will be non-zero if preceding plot net yet transmitted, otherwise zero + // This Destination configuration expects all source Plots to be in the same Field. + for (uint256 i; i < plots.length; i++) { + SourcePlot memory sourcePlot = abi.decode(plots[i], (SourcePlot)); + require(sourcePlot.fieldId == 0, "Field unsupported"); + // require(sourcePlot.amount > 1000e6, "Too small"); + require(sourcePlot.index >= s.sys.fields[IN_FIELD].harvestable, "index already harvestable"); + if (sourcePlot.index > s.sys.fields[IN_FIELD].latestTransmittedPlotIndex) { + _insertAfterLastPlot(user, sourcePlot.index, sourcePlot.amount); + } else { + _insertInNullPlot( + user, + sourcePlot.index, + sourcePlot.amount, + sourcePlot.existingIndex + ); + } + emit PlotTransmittedIn(user, sourcePlot); + } + } + + // Assumes that 'previous' plot is owned by null. <- this may be true bc null plots are always injected (line is complete), but we have to allow case where existingIndex = index (and verify that existingIndex.amount is not zero). + // if plot before is already transmitted, the "prevPlot" should be the null plot that begins at index. + // + // What if pods up to index-1 are transmitted? it is ok, but need to have index == existingIndex + // What if pods from index + size + 1 are transmitted? it is ok, nextIndex == index + amount + function _insertInNullPlot( + address user, + uint256 index, + uint256 amount, + uint256 existingIndex + ) private { + AppStorage storage s = LibAppStorage.diamondStorage(); + // prev plot is provided by user, but it is verified in insertion. + uint256 prevAmount = s.accts[address(0)].fields[IN_FIELD].plots[existingIndex]; + uint256 nextIndex = existingIndex + prevAmount; + require(prevAmount > 0, "non null"); + require(existingIndex <= index, "existingIndex too large"); + require(nextIndex >= index + amount, "nextIndex too small"); + + // Delete existing null plot. + LibField.deletePlot(address(0), IN_FIELD, existingIndex); + + // Create preceding null plot, user plot, and following null plot. + LibField.createPlot(address(0), IN_FIELD, existingIndex, index - existingIndex); + LibField.createPlot(user, IN_FIELD, index, amount); + LibField.createPlot(address(0), IN_FIELD, index + amount, nextIndex - (index + amount)); + } + + // Assumes the latest plot is NOT null? + function _insertAfterLastPlot(address user, uint256 index, uint256 amount) private { + AppStorage storage s = LibAppStorage.diamondStorage(); + uint256 nextIndex = s.sys.fields[IN_FIELD].latestTransmittedPlotIndex + + s.accts[s.sys.fields[IN_FIELD].latestTransmittedPlotOwner].fields[IN_FIELD].plots[ + s.sys.fields[IN_FIELD].latestTransmittedPlotIndex + ]; + + // Create preceding null plot and user plot. + LibField.createPlot(address(0), IN_FIELD, nextIndex, index - nextIndex); + LibField.createPlot(user, IN_FIELD, index, amount); + + s.sys.fields[IN_FIELD].latestTransmittedPlotIndex = index; + s.sys.fields[IN_FIELD].latestTransmittedPlotOwner = user; + } +} diff --git a/protocol/contracts/libraries/ForkSystem/LibTransmitOut.sol b/protocol/contracts/libraries/ForkSystem/LibTransmitOut.sol new file mode 100644 index 0000000000..7a3faee774 --- /dev/null +++ b/protocol/contracts/libraries/ForkSystem/LibTransmitOut.sol @@ -0,0 +1,237 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.20; + +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + +import {C} from "contracts/C.sol"; +import {AppStorage} from "contracts/libraries/LibAppStorage.sol"; +import {LibUnripe} from "contracts/libraries/LibUnripe.sol"; +import {LibSilo} from "contracts/libraries/Silo/LibSilo.sol"; +import {LibTokenSilo} from "contracts/libraries/Silo/LibTokenSilo.sol"; +import {LibWell} from "contracts/libraries/Well/LibWell.sol"; +import {IWell} from "contracts/interfaces/basin/IWell.sol"; +import {LibAppStorage} from "contracts/libraries/LibAppStorage.sol"; +import {LibTransfer} from "contracts/libraries/Token/LibTransfer.sol"; +import {LibFertilizer} from "contracts/libraries/LibFertilizer.sol"; +import {LibMarket} from "contracts/libraries/LibMarket.sol"; +import {LibField} from "contracts/libraries/LibField.sol"; + +/** + * @title LibTransmitOut + * @author funderbrker + * @notice Library handling outbound migration and burning of protocol assets. + */ +library LibTransmitOut { + event DepositTransmittedOut(address indexed user, SourceDeposit deposit); + + event FertilizerTransmittedOut(address indexed user, SourceFertilizer fertilizer); + + event PlotTransmittedOut(address indexed user, SourcePlot sourcePlot); + + // Need to define structs locally to contain additional migration information. + struct SourceDeposit { + address token; + uint256 amount; + int96 stem; + uint256[] sourceMinTokenAmountsOut; // LP only + uint256 destMinLpOut; // LP only + uint256 _grownStalk; // not stalk // need to change logic + uint256 _burnedBeans; + address _transferredToken; // NOTE what if LP type is not supported at destination? + uint256 _transferredTokenAmount; + } + + struct SourcePlot { + uint256 fieldId; + uint256 index; + uint256 amount; + uint256 existingIndex; + } + + struct SourceFertilizer { + uint128 id; + uint256 amount; + uint128 _remainingBpf; + } + + // Current system does not offer a clean way to supply all deposits across all tokens. + // Could instead (for deposits + fert + pods) have the external function arguments all be arrays of structs + // where the structs are the locally defined definitions. But this is a little sloppy, bc the structs are + // partially populate during the execution. + // Could use two structs: a selector struct and a migration definition struct. Seems very bloated. + // Could mark fields that do not need to be populated with a "_" prefix <- this is a bit ugly, but i think best + + // TODO what to do with LP tokens? The destination does not want a token with the source Bean underlying it. Should unwind the LP, burn the bean, and send the non-Bean token. + // TODO swapping / exiting LP will be difficult to implement with a proper minimum out. + // remove in balanced ratio? with UI setting minimum out of each asset... + /** + * @notice Withdraw deposits and sends underlying ERC20 of asset. Burns Beans. + * @return depositsOut The set of deposits to transmit, encoded as bytes. + */ + function transmitOutDeposits( + address user, + address destination, + SourceDeposit[] memory deposits + ) internal returns (bytes[] memory depositsOut) { + if (deposits.length == 0) return depositsOut; + AppStorage storage s = LibAppStorage.diamondStorage(); + depositsOut = new bytes[](deposits.length); + + address[] memory tokensMown = new address[](s.sys.silo.whitelistStatuses.length); + for (uint256 i; i < deposits.length; i++) { + require(!LibUnripe.isUnripe(deposits[i].token), "Unripe not supported"); + + // Mow each migrating token once. + for (uint256 j; j < tokensMown.length; j++) { + if (tokensMown[j] == deposits[i].token) { + break; + } + if (tokensMown[j] == address(0)) { + tokensMown[j] = deposits[i].token; + LibSilo._mow(user, deposits[i].token); + break; + } + } + + // Withdraw deposit from Silo. + (, deposits[i]._grownStalk, , ) = LibSilo._withdrawDeposit( + user, + deposits[i].token, + deposits[i].stem, + deposits[i].amount + ); + + if (deposits[i].token == C.BEAN) { + // Burn Bean. + deposits[i]._burnedBeans = deposits[i].amount; + C.bean().burn(deposits[i].amount); + } + // If Well LP token. Only supports Wells with Bean:Token. + else if (LibWell.isWell(deposits[i].token)) { + // Withdraw LP token. + uint256[] memory tokenAmountsOut = IWell(deposits[i].token).removeLiquidity( + deposits[i].amount, + deposits[i].sourceMinTokenAmountsOut, + address(this), + block.number + ); + + // Burn Bean. + deposits[i]._burnedBeans = tokenAmountsOut[ + LibWell.getBeanIndexFromWell(deposits[i].token) + ]; + C.bean().burn(deposits[i]._burnedBeans); + + // Send non-Bean token. + IERC20 nonBeanToken = LibWell.getNonBeanTokenFromWell(deposits[i].token); + uint256 tokenAmount = tokenAmountsOut[ + LibWell.getNonBeanIndexFromWell(deposits[i].token) + ]; + LibTransfer.sendToken( + nonBeanToken, + tokenAmount, + destination, + LibTransfer.To.EXTERNAL + ); + deposits[i]._transferredToken = address(nonBeanToken); + deposits[i]._transferredTokenAmount = tokenAmount; + } else { + // Must be Bean or a whitelisted Well token. + revert("Invalid token"); + } + + depositsOut[i] = abi.encode(deposits[i]); + emit DepositTransmittedOut(user, deposits[i]); + } + } + + /** + * @notice Slashes plots, which can later be burned. Populates and encodes migration data. + * @return plotsOut The plots to transmit, encoded as bytes. + * @dev Slashes the Pods by setting the owner to 0x0. They remain in Pod line until they + * become harvestable and can be Burned. + */ + function transmitOutPlots( + address account, + SourcePlot[] memory plots + ) internal returns (bytes[] memory plotsOut) { + if (plots.length == 0) return plotsOut; + AppStorage storage s = LibAppStorage.diamondStorage(); + plotsOut = new bytes[](plots.length); + for (uint256 i; i < plots.length; i++) { + SourcePlot memory plot = plots[i]; + uint256 pods = s.accts[account].fields[plot.fieldId].plots[plot.index]; + require(plot.amount > 0, "No Pods to transmit"); + require(pods >= plot.amount, "Insufficient Pods"); + + // Remove Plots from user. + LibMarket._cancelPodListing(account, plot.fieldId, plot.index); + LibField.deletePlot(account, plot.fieldId, plot.index); + + // Add new plots to null address. + LibField.createPlot(address(0), plot.fieldId, plot.index, plot.amount); + + // Add partial plots back to user. + uint256 remainingPods = pods - plot.amount; + if (remainingPods > 0) { + uint256 newIndex = plot.index + plot.amount; + s.accts[account].fields[s.sys.activeField].plots[newIndex] = remainingPods; + s.accts[account].fields[s.sys.activeField].plotIndexes.push(newIndex); + } + + // Update Field counters. + s.sys.fields[plot.fieldId].pods -= plot.amount; + + plotsOut[i] = abi.encode(plot); + emit PlotTransmittedOut(account, plot); + } + } + + /** + * @notice Burns Fertilizer. Populates and encodes migration data. + * @return fertilizerOut The Fertilizer to transmit, encoded as bytes. + */ + function transmitOutFertilizer( + address account, + SourceFertilizer[] memory fertilizer + ) internal returns (bytes[] memory fertilizerOut) { + if (fertilizer.length == 0) return fertilizerOut; + AppStorage storage s = LibAppStorage.diamondStorage(); + fertilizerOut = new bytes[](fertilizer.length); + + /* + 0. Update user. + 1. Decrement each fert individually. + 2. Check leftoverBeans. + */ + uint256[] memory ids = new uint256[](fertilizer.length); + for (uint256 i; i < fertilizer.length; i++) { + ids[i] = fertilizer[i].id; + + // Do not allow duplicate fertilizer IDs. + for (uint256 j; j < i; j++) { + require(ids[j] != fertilizer[i].id, "Duplicate Fertilizer ID"); + } + + uint128 remainingBpf = fertilizer[i].id - s.sys.fert.bpf; + C.fertilizer().beanstalkBurn( + account, + s.sys.fert.bpf + remainingBpf, + uint128(fertilizer[i].amount), + s.sys.fert.bpf + ); + LibFertilizer.decrementFertState(fertilizer[i].amount, remainingBpf); + + fertilizer[i]._remainingBpf = remainingBpf; + fertilizerOut[i] = abi.encode(fertilizer[i]); + emit FertilizerTransmittedOut(account, fertilizer[i]); + } + + // If leftover beans are greater than obligations, drop excess leftovers. Rounding loss. + uint256 unfertilizedBeans = s.sys.fert.unfertilizedIndex - s.sys.fert.fertilizedIndex; + if (unfertilizedBeans < s.sys.fert.leftoverBeans) { + s.sys.fert.leftoverBeans = 0; + } + } +} diff --git a/protocol/contracts/libraries/LibDibbler.sol b/protocol/contracts/libraries/LibDibbler.sol index 85fab5e1d1..781b51405b 100644 --- a/protocol/contracts/libraries/LibDibbler.sol +++ b/protocol/contracts/libraries/LibDibbler.sol @@ -6,10 +6,11 @@ import {Math} from "@openzeppelin/contracts/utils/math/Math.sol"; import {PRBMath} from "@prb/math/contracts/PRBMath.sol"; import {LibPRBMathRoundable} from "contracts/libraries/LibPRBMathRoundable.sol"; import {LibAppStorage, AppStorage} from "./LibAppStorage.sol"; -import {Account, Field} from "contracts/beanstalk/storage/Account.sol"; +import {Account} from "contracts/beanstalk/storage/Account.sol"; import {LibRedundantMath128} from "./LibRedundantMath128.sol"; import {LibRedundantMath32} from "./LibRedundantMath32.sol"; import {LibRedundantMath256} from "contracts/libraries/LibRedundantMath256.sol"; +import {LibField} from "contracts/libraries/LibField.sol"; /** * @title LibDibbler @@ -96,8 +97,7 @@ library LibDibbler { uint256 index = s.sys.fields[s.sys.activeField].pods; - s.accts[account].fields[s.sys.activeField].plots[index] = pods; - s.accts[account].fields[s.sys.activeField].plotIndexes.push(index); + LibField.createPlot(account, s.sys.activeField, index, pods); emit Sow(account, s.sys.activeField, index, beans, pods); s.sys.fields[s.sys.activeField].pods += pods; @@ -381,40 +381,4 @@ library LibDibbler { ); } } - - /** - * @notice removes a plot index from an accounts plotIndex list. - */ - function removePlotIndexFromAccount( - address account, - uint256 fieldId, - uint256 plotIndex - ) internal { - AppStorage storage s = LibAppStorage.diamondStorage(); - uint256 i = findPlotIndexForAccount(account, fieldId, plotIndex); - Field storage field = s.accts[account].fields[fieldId]; - field.plotIndexes[i] = field.plotIndexes[field.plotIndexes.length - 1]; - field.plotIndexes.pop(); - } - - /** - * @notice finds the index of a plot in an accounts plotIndex list. - */ - function findPlotIndexForAccount( - address account, - uint256 fieldId, - uint256 plotIndex - ) internal view returns (uint256 i) { - AppStorage storage s = LibAppStorage.diamondStorage(); - Field storage field = s.accts[account].fields[fieldId]; - uint256[] memory plotIndexes = field.plotIndexes; - uint256 length = plotIndexes.length; - while (plotIndexes[i] != plotIndex) { - i++; - if (i >= length) { - revert("Id not found"); - } - } - return i; - } } diff --git a/protocol/contracts/libraries/LibFertilizer.sol b/protocol/contracts/libraries/LibFertilizer.sol index fdea584ab1..e9e03a40b1 100644 --- a/protocol/contracts/libraries/LibFertilizer.sol +++ b/protocol/contracts/libraries/LibFertilizer.sol @@ -18,6 +18,7 @@ import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol import {LibWell} from "contracts/libraries/Well/LibWell.sol"; import {LibUsdOracle} from "contracts/libraries/Oracle/LibUsdOracle.sol"; import {LibTractor} from "contracts/libraries/LibTractor.sol"; +import {LibTransfer} from "contracts/libraries/Token/LibTransfer.sol"; /** * @author Publius @@ -46,25 +47,52 @@ library LibFertilizer { uint256 fertilizerAmount, uint256 minLP ) internal returns (uint128 id) { - AppStorage storage s = LibAppStorage.diamondStorage(); - - uint128 fertilizerAmount128 = fertilizerAmount.toUint128(); - // Calculate Beans Per Fertilizer and add to total owed uint128 bpf = getBpf(season); - s.sys.fert.unfertilizedIndex = s.sys.fert.unfertilizedIndex.add(fertilizerAmount.mul(bpf)); - // Get id - id = s.sys.fert.bpf.add(bpf); - // Update Total and Season supply - s.sys.fert.fertilizer[id] = s.sys.fert.fertilizer[id].add(fertilizerAmount128); - s.sys.fert.activeFertilizer = s.sys.fert.activeFertilizer.add(fertilizerAmount); + id = IncrementFertState(fertilizerAmount, bpf); // Add underlying to Unripe Beans and Unripe LP addUnderlying(tokenAmountIn, fertilizerAmount.mul(DECIMALS), minLP); + } + + function IncrementFertState( + uint256 fertilizerAmount, + uint128 bpf + ) internal returns (uint128 id) { + AppStorage storage s = LibAppStorage.diamondStorage(); + uint128 fertilizerAmount128 = fertilizerAmount.toUint128(); + s.sys.fert.unfertilizedIndex += fertilizerAmount * bpf; + id = s.sys.fert.bpf + bpf; + s.sys.fert.fertilizer[id] += fertilizerAmount128; + s.sys.fert.activeFertilizer += fertilizerAmount; + // If not first time adding Fertilizer with this id, return if (s.sys.fert.fertilizer[id] > fertilizerAmount128) return id; // If first time, log end Beans Per Fertilizer and add to Season queue. push(id); emit SetFertilizer(id, bpf); + return id; + } + + function decrementFertState( + uint256 fertilizerAmount, + uint128 remainingBpf + ) internal returns (uint128 id) { + AppStorage storage s = LibAppStorage.diamondStorage(); + uint128 fertilizerAmount128 = fertilizerAmount.toUint128(); + + s.sys.fert.unfertilizedIndex -= fertilizerAmount * remainingBpf; + id = s.sys.fert.bpf + remainingBpf; + s.sys.fert.fertilizer[id] -= fertilizerAmount128; + s.sys.fert.activeFertilizer -= fertilizerAmount; + + return id; + } + + function claimFertilized(uint256[] memory ids, LibTransfer.To mode) internal { + AppStorage storage s = LibAppStorage.diamondStorage(); + uint256 amount = C.fertilizer().beanstalkUpdate(LibTractor._user(), ids, s.sys.fert.bpf); + s.sys.fert.fertilizedPaidIndex += amount; + LibTransfer.sendToken(C.bean(), amount, LibTractor._user(), mode); } function getBpf(uint128 id) internal pure returns (uint128 bpf) { diff --git a/protocol/contracts/libraries/LibField.sol b/protocol/contracts/libraries/LibField.sol new file mode 100644 index 0000000000..8d8bb83d1f --- /dev/null +++ b/protocol/contracts/libraries/LibField.sol @@ -0,0 +1,71 @@ +/* + SPDX-License-Identifier: MIT +*/ + +pragma solidity ^0.8.20; + +import {AppStorage, LibAppStorage} from "./LibAppStorage.sol"; +import {Field} from "contracts/beanstalk/storage/Account.sol"; + +/** + * @author funderbrker + * @title LibField + **/ + +library LibField { + /** + * @dev Does not check for existence of index in plotIndexes. + * @dev Plot indexes not tracked for null address. + */ + function createPlot(address account, uint256 fieldId, uint256 index, uint256 amount) internal { + AppStorage storage s = LibAppStorage.diamondStorage(); + if (amount == 0) return; + s.accts[account].fields[fieldId].plots[index] = amount; + if (account != address(0)) s.accts[account].fields[fieldId].plotIndexes.push(index); + } + + /** + * @dev Plot indexes are not tracked for null address. + */ + function deletePlot(address account, uint256 fieldId, uint256 index) internal { + AppStorage storage s = LibAppStorage.diamondStorage(); + delete s.accts[account].fields[fieldId].plots[index]; + if (account != address(0)) removePlotIndexFromAccount(account, fieldId, index); + } + + /** + * @notice removes a plot index from an accounts plotIndex list. + */ + function removePlotIndexFromAccount( + address account, + uint256 fieldId, + uint256 plotIndex + ) internal { + AppStorage storage s = LibAppStorage.diamondStorage(); + uint256 i = findPlotIndexForAccount(account, fieldId, plotIndex); + Field storage field = s.accts[account].fields[fieldId]; + field.plotIndexes[i] = field.plotIndexes[field.plotIndexes.length - 1]; + field.plotIndexes.pop(); + } + + /** + * @notice finds the index of a plot in an accounts plotIndex list. + */ + function findPlotIndexForAccount( + address account, + uint256 fieldId, + uint256 plotIndex + ) internal view returns (uint256 i) { + AppStorage storage s = LibAppStorage.diamondStorage(); + Field storage field = s.accts[account].fields[fieldId]; + uint256[] memory plotIndexes = field.plotIndexes; + uint256 length = plotIndexes.length; + while (plotIndexes[i] != plotIndex) { + i++; + if (i >= length) { + revert("Id not found"); + } + } + return i; + } +} diff --git a/protocol/contracts/libraries/Silo/LibSilo.sol b/protocol/contracts/libraries/Silo/LibSilo.sol index 9285d00806..4366e2c46f 100644 --- a/protocol/contracts/libraries/Silo/LibSilo.sol +++ b/protocol/contracts/libraries/Silo/LibSilo.sol @@ -156,6 +156,184 @@ library LibSilo { uint256[] values ); + //////////////////////// WITHDRAW //////////////////////// + + /** + * @notice Handles withdraw accounting. + * + * - {_removeDepositFromAccount} calculates the stalk + * assoicated with a given deposit, and removes the deposit from the account. + * emits `RemoveDeposit` and `TransferSingle` events. + * + * - {_withdraw} updates the total value deposited in the silo, and burns + * the stalk assoicated with the deposits. + * + */ + function _withdrawDeposit( + address account, + address token, + int96 stem, + uint256 amount + ) + internal + returns ( + uint256 initialStalkRemoved, + uint256 grownStalkRemoved, + uint256 bdvRemoved, + GerminationSide side + ) + { + // Remove the Deposit from `account`. + (initialStalkRemoved, grownStalkRemoved, bdvRemoved, side) = _removeDepositFromAccount( + account, + token, + stem, + amount, + LibTokenSilo.Transfer.emitTransferSingle + ); + if (side == GerminationSide.NOT_GERMINATING) { + // remove the deposit from totals + _withdraw( + account, + token, + amount, + bdvRemoved, + initialStalkRemoved.add(grownStalkRemoved) + ); + } else { + // remove deposit from germination, and burn the grown stalk. + // grown stalk does not germinate and is not counted in germinating totals. + _withdrawGerminating(account, token, amount, bdvRemoved, initialStalkRemoved, side); + + if (grownStalkRemoved > 0) { + burnActiveStalk(account, grownStalkRemoved); + } + } + } + + /** + * @notice Handles withdraw accounting for multiple deposits. + * + * - {_removeDepositsFromAccount} removes the deposits from the account, + * and returns the total tokens, stalk, and bdv removed from the account. + * + * - {_withdraw} updates the total value deposited in the silo, and burns + * the stalk assoicated with the deposits. + * + */ + function _withdrawDeposits( + address account, + address token, + int96[] calldata stems, + uint256[] calldata amounts + ) internal returns (uint256) { + require(stems.length == amounts.length, "Silo: Crates, amounts are diff lengths."); + + AssetsRemoved memory ar = _removeDepositsFromAccount( + account, + token, + stems, + amounts, + ERC1155Event.EMIT_BATCH_EVENT + ); + + // withdraw deposits that are not germinating. + if (ar.active.tokens > 0) { + _withdraw(account, token, ar.active.tokens, ar.active.bdv, ar.active.stalk); + } + + // withdraw Germinating deposits from odd seasons + if (ar.odd.tokens > 0) { + _withdrawGerminating( + account, + token, + ar.odd.tokens, + ar.odd.bdv, + ar.odd.stalk, + GerminationSide.ODD + ); + } + + // withdraw Germinating deposits from even seasons + if (ar.even.tokens > 0) { + _withdrawGerminating( + account, + token, + ar.even.tokens, + ar.even.bdv, + ar.even.stalk, + GerminationSide.EVEN + ); + } + + if (ar.grownStalkFromGermDeposits > 0) { + burnActiveStalk(account, ar.grownStalkFromGermDeposits); + } + + // we return the summation of all tokens removed from the silo. + // to be used in {SiloFacet.withdrawDeposits}. + return ar.active.tokens.add(ar.odd.tokens).add(ar.even.tokens); + } + + /** + * @dev internal helper function for withdraw accounting. + */ + function _withdraw( + address account, + address token, + uint256 amount, + uint256 bdv, + uint256 stalk + ) internal { + // Decrement total deposited in the silo. + LibTokenSilo.decrementTotalDeposited(token, amount, bdv); + // Burn stalk and roots associated with the stalk. + burnActiveStalk(account, stalk); + } + + /** + * @dev internal helper function for withdraw accounting with germination. + * @param side determines whether to withdraw from odd or even germination. + */ + function _withdrawGerminating( + address account, + address token, + uint256 amount, + uint256 bdv, + uint256 stalk, + GerminationSide side + ) internal { + // Deposited Earned Beans do not germinate. Thus, when withdrawing a Bean Deposit + // with a Germinating Stem, Beanstalk needs to determine how many of the Beans + // were Planted vs Deposited from a Circulating/Farm balance. + // If a Farmer's Germinating Stalk for a given Season is less than the number of + // Deposited Beans in that Season, then it is assumed that the excess Beans were + // Planted. + if (token == C.BEAN) { + (uint256 germinatingStalk, uint256 earnedBeansStalk) = checkForEarnedBeans( + account, + stalk, + side + ); + // set the bdv and amount accordingly to the stalk. + stalk = germinatingStalk; + uint256 earnedBeans = earnedBeansStalk.div(C.STALK_PER_BEAN); + amount = amount - earnedBeans; + // note: the 1 Bean = 1 BDV assumption cannot be made here for input `bdv`, + // as a user converting a germinating LP deposit into bean may have an inflated bdv. + // thus, amount and bdv are decremented by the earnedBeans (where the 1 Bean = 1 BDV assumption can be made). + bdv = bdv - earnedBeans; + + // burn the earned bean stalk (which is active). + burnActiveStalk(account, earnedBeansStalk); + // calculate earnedBeans and decrement totalDeposited. + LibTokenSilo.decrementTotalDeposited(C.BEAN, earnedBeans, earnedBeans); + } + // Decrement from total germinating. + LibTokenSilo.decrementTotalGerminating(token, amount, bdv, side); // Decrement total Germinating in the silo. + burnGerminatingStalk(account, uint128(stalk), side); // Burn stalk and roots associated with the stalk. + } + //////////////////////// MINT //////////////////////// /** @@ -260,7 +438,7 @@ library LibSilo { * @notice Burns germinating stalk. * @dev Germinating stalk does not have any roots assoicated with it. */ - function burnGerminatingStalk(address account, uint128 stalk, GerminationSide side) external { + function burnGerminatingStalk(address account, uint128 stalk, GerminationSide side) public { AppStorage storage s = LibAppStorage.diamondStorage(); s.accts[account].germinatingStalk[side] -= stalk; diff --git a/protocol/contracts/libraries/Well/LibWell.sol b/protocol/contracts/libraries/Well/LibWell.sol index 35e35c5ca6..fb84fe9284 100644 --- a/protocol/contracts/libraries/Well/LibWell.sol +++ b/protocol/contracts/libraries/Well/LibWell.sol @@ -88,6 +88,14 @@ library LibWell { beanIndex = getBeanIndex(tokens); } + /** + * @dev Returns the index of the first ERC20 given a Well. + */ + function getNonBeanIndexFromWell(address well) internal view returns (uint beanIndex) { + IERC20[] memory tokens = IWell(well).tokens(); + beanIndex = getNonBeanIndex(tokens); + } + function getNonBeanTokenFromWell(address well) internal view returns (IERC20 nonBeanToken) { IERC20[] memory tokens = IWell(well).tokens(); return tokens[getNonBeanIndex(tokens)]; diff --git a/protocol/contracts/mocks/MockInitDiamond.sol b/protocol/contracts/mocks/MockInitDiamond.sol deleted file mode 100644 index d2e331f0e6..0000000000 --- a/protocol/contracts/mocks/MockInitDiamond.sol +++ /dev/null @@ -1,70 +0,0 @@ -/* - SPDX-License-Identifier: MIT -*/ - -pragma solidity ^0.8.20; - -import {IBean} from "../interfaces/IBean.sol"; -import {IWETH} from "../interfaces/IWETH.sol"; -import {MockToken} from "../mocks/MockToken.sol"; -import {AppStorage} from "../beanstalk/storage/AppStorage.sol"; -import {C} from "../C.sol"; -import {InitWhitelist} from "contracts/beanstalk/init/InitWhitelist.sol"; -import {InitWhitelistStatuses} from "contracts/beanstalk/init/InitWhitelistStatuses.sol"; -import {LibDiamond} from "../libraries/LibDiamond.sol"; -import {LibCases} from "../libraries/LibCases.sol"; -import {LibGauge} from "contracts/libraries/LibGauge.sol"; -import {LibTractor} from "contracts/libraries/LibTractor.sol"; -import {Weather} from "contracts/beanstalk/sun/SeasonFacet/Weather.sol"; - -/** - * @author Publius - * @title Mock Init Diamond - **/ -contract MockInitDiamond is InitWhitelist, InitWhitelistStatuses, Weather { - function init() external { - LibDiamond.DiamondStorage storage ds = LibDiamond.diamondStorage(); - - ds.supportedInterfaces[0xd9b67a26] = true; // ERC1155 - ds.supportedInterfaces[0x0e89341c] = true; // ERC1155Metadata - - LibCases.setCasesV2(); - s.sys.weather.temp = 1; - - s.sys.weather.thisSowTime = type(uint32).max; - s.sys.weather.lastSowTime = type(uint32).max; - - s.sys.season.current = 1; - s.sys.season.withdrawSeasons = 25; - s.sys.season.period = C.getSeasonPeriod(); - s.sys.season.timestamp = block.timestamp; - s.sys.season.start = s.sys.season.period > 0 - ? (block.timestamp / s.sys.season.period) * s.sys.season.period - : block.timestamp; - s.sys.isFarm = 1; - s.sys.usdTokenPrice[C.BEAN_ETH_WELL] = 1; - s.sys.twaReserves[C.BEAN_ETH_WELL].reserve0 = 1; - s.sys.twaReserves[C.BEAN_ETH_WELL].reserve1 = 1; - - s.sys.season.stemStartSeason = uint16(s.sys.season.current); - s.sys.season.stemScaleSeason = uint16(s.sys.season.current); - s.sys.seedGauge.beanToMaxLpGpPerBdvRatio = 50e18; // 50% - s.sys.seedGauge.averageGrownStalkPerBdvPerSeason = 3e6; - - LibTractor._resetPublisher(); - - s.sys.silo.unripeSettings[C.UNRIPE_LP].underlyingToken = C.BEAN_WSTETH_WELL; - - emit BeanToMaxLpGpPerBdvRatioChange( - s.sys.season.current, - type(uint256).max, - int80(int128(s.sys.seedGauge.beanToMaxLpGpPerBdvRatio)) - ); - emit LibGauge.UpdateAverageStalkPerBdvPerSeason( - s.sys.seedGauge.averageGrownStalkPerBdvPerSeason - ); - - whitelistPools(); - addWhitelistStatuses(false); - } -} diff --git a/protocol/contracts/mocks/mockFacets/MockConvertFacet.sol b/protocol/contracts/mocks/mockFacets/MockConvertFacet.sol index a5ba9b0926..16a5ec9f5e 100644 --- a/protocol/contracts/mocks/mockFacets/MockConvertFacet.sol +++ b/protocol/contracts/mocks/mockFacets/MockConvertFacet.sol @@ -43,7 +43,7 @@ contract MockConvertFacet is ConvertFacet { uint256 grownStalk ) external { LibSilo._mow(LibTractor._user(), token); - LibConvert._depositTokensForConvert(token, amount, bdv, grownStalk); + LibConvert._depositTokensForConvert(LibTractor._user(), token, amount, bdv, grownStalk); } function convertInternalE( diff --git a/protocol/contracts/mocks/mockFacets/MockSeasonFacet.sol b/protocol/contracts/mocks/mockFacets/MockSeasonFacet.sol index ddc36dc5e2..a71dbf654a 100644 --- a/protocol/contracts/mocks/mockFacets/MockSeasonFacet.sol +++ b/protocol/contracts/mocks/mockFacets/MockSeasonFacet.sol @@ -254,7 +254,7 @@ contract MockSeasonFacet is SeasonFacet { function resetState() public { for (uint256 i; i < s.sys.fieldCount; i++) { s.sys.fields[i].pods = 0; - s.sys.fields[i].harvested = 0; + s.sys.fields[i].processed = 0; s.sys.fields[i].harvestable = 0; } delete s.sys.silo; @@ -511,7 +511,7 @@ contract MockSeasonFacet is SeasonFacet { return currentGaugePoints; } - function mockInitalizeGaugeForToken( + function mockInitializeGaugeForToken( address token, bytes4 gaugePointSelector, bytes4 liquidityWeightSelector, @@ -593,7 +593,7 @@ contract MockSeasonFacet is SeasonFacet { * @dev 0 = below peg, 1 = above peg, 2 = significantly above peg. */ function setPrice(uint256 price, address targetWell) public returns (int256 deltaB) { - // initalize beanTknPrice, and reserves. + // initialize beanTknPrice, and reserves. uint256 ethPrice = 1000e6; s.sys.usdTokenPrice[targetWell] = 1e24 / ethPrice; uint256[] memory reserves = IWell(targetWell).getReserves(); diff --git a/protocol/contracts/mocks/newMockInitDiamond.sol b/protocol/contracts/mocks/newMockInitDiamond.sol index 02aac12b7a..37ea57f3de 100644 --- a/protocol/contracts/mocks/newMockInitDiamond.sol +++ b/protocol/contracts/mocks/newMockInitDiamond.sol @@ -6,7 +6,7 @@ pragma solidity ^0.8.20; import {AppStorage} from "contracts/beanstalk/storage/AppStorage.sol"; import {AssetSettings} from "contracts/beanstalk/storage/System.sol"; -import "contracts/beanstalk/init/InitalizeDiamond.sol"; +import "contracts/beanstalk/init/InitializeDiamond.sol"; import {LibWhitelistedTokens} from "contracts/libraries/Silo/LibWhitelistedTokens.sol"; import {LibWhitelist} from "contracts/libraries/Silo/LibWhitelist.sol"; import {LibUnripe} from "contracts/libraries/LibUnripe.sol"; @@ -22,7 +22,7 @@ import {C} from "contracts/C.sol"; * - Whitelists the bean:wsteth well. * - Whitelists unripe assets. **/ -contract MockInitDiamond is InitalizeDiamond { +contract MockInitDiamond is InitializeDiamond { // min 1micro stalk earned per season due to germination. uint32 constant INIT_UR_BEAN_STALK_EARNED_PER_SEASON = 1; uint32 constant INIT_BEAN_WSTETH_WELL_STALK_EARNED_PER_SEASON = 4e6; @@ -30,18 +30,21 @@ contract MockInitDiamond is InitalizeDiamond { uint32 constant INIT_BEAN_WURLP_PERCENT_TARGET = 50e6; function init() external { - // initalize the default state of the diamond. - // {see. InitalizeDiamond.initalizeDiamond()} - initalizeDiamond(C.BEAN, C.BEAN_ETH_WELL); + // initialize the default state of the diamond. + // {see. InitializeDiamond.initializeDiamond()} + initializeDiamond(C.BEAN, C.BEAN_ETH_WELL); - // initalizes unripe assets. + // initializes unripe assets. // sets the underlying LP token of unripeLP to the Bean:wstETH well. address underlyingUrLPWell = C.BEAN_WSTETH_WELL; whitelistUnderlyingUrLPWell(underlyingUrLPWell); - initalizeUnripeAssets(underlyingUrLPWell); + initializeUnripeAssets(underlyingUrLPWell); + + // Set accepted source (ie parent) to be original Beanstalk. + s.sys.supportedSourceForks[0xC1E088fC1323b20BCBee9bd1B9fC9546db5624C5] = true; } - function initalizeUnripeAssets(address well) internal { + function initializeUnripeAssets(address well) internal { ( address[] memory unripeTokens, address[] memory underlyingTokens @@ -97,7 +100,7 @@ contract MockInitDiamond is InitalizeDiamond { } /** - * @notice initalizes the unripe silo settings. + * @notice initializes the unripe silo settings. * @dev unripe bean and unrpe lp has the same settings, * other than the BDV calculation. */ diff --git a/protocol/contracts/tokens/Fertilizer/Fertilizer.sol b/protocol/contracts/tokens/Fertilizer/Fertilizer.sol index a401752a16..dfc695d8da 100644 --- a/protocol/contracts/tokens/Fertilizer/Fertilizer.sol +++ b/protocol/contracts/tokens/Fertilizer/Fertilizer.sol @@ -59,6 +59,15 @@ contract Fertilizer is Internalizer { _safeMint(account, id, amount, bytes("0")); } + function beanstalkBurn(address account, uint256 id, uint128 amount, uint128 bpf) external onlyOwner { + require(_balances[id][account].amount >= amount); + uint256[] memory ids = new uint256[](1); + ids[0] = id; + _update(account, ids, bpf); + _balances[id][account].lastBpf = bpf; + _safeBurn(account, id, amount, bytes("0")); + } + function _beforeTokenTransfer( address, // operator, address from, diff --git a/protocol/contracts/tokens/Fertilizer/Fertilizer1155.sol b/protocol/contracts/tokens/Fertilizer/Fertilizer1155.sol index 2953cc2121..a76a386ce9 100644 --- a/protocol/contracts/tokens/Fertilizer/Fertilizer1155.sol +++ b/protocol/contracts/tokens/Fertilizer/Fertilizer1155.sol @@ -88,6 +88,18 @@ contract Fertilizer1155 is ERC1155Upgradeable { __doSafeTransferAcceptanceCheck(operator, address(0), to, id, amount, data); } + function _safeBurn(address from, uint256 id, uint256 amount, bytes memory data) internal virtual { + require(from != address(0), "ERC1155: burn from the zero address"); + + address operator = _msgSender(); + + _transfer(from, address(0), id, amount); + + emit TransferSingle(operator, from, address(0), id, amount); + + __doSafeTransferAcceptanceCheck(operator, from, address(0), id, amount, data); + } + // The 3 functions below are copied from: // OpenZeppelin Contracts (last updated v4.6.0) (token/ERC1155/ERC1155.sol) // as they are private functions. diff --git a/protocol/test/foundry/Invariable.t.sol b/protocol/test/foundry/Invariable.t.sol index 3033dab43c..e6d144e895 100644 --- a/protocol/test/foundry/Invariable.t.sol +++ b/protocol/test/foundry/Invariable.t.sol @@ -17,16 +17,16 @@ contract InvariableTest is TestHelper { address[] siloUsers = new address[](3); function setUp() public { - initializeBeanstalkTestState(true, true); + initializeBeanstalkTestState(true, false, true); MockToken(C.WETH).mint(BEANSTALK, 100_000); - + siloUsers = createUsers(3); initializeUnripeTokens(siloUsers[0], 100e6, 100e18); mintTokensToUsers(siloUsers, C.BEAN, 100_000e6); - setUpSiloDepositTest(10_000e6, siloUsers); + setUpSiloDeposits(10_000e6, siloUsers); addFertilizerBasedOnSprouts(0, 100e6); - sowAmountForFarmer(siloUsers[0], 1_000e6); + sowForUser(siloUsers[0], 1_000e6); } /** @@ -48,7 +48,6 @@ contract InvariableTest is TestHelper { vm.prank(siloUsers[1]); bs.advancedFarm(advancedFarmCalls); - // Manipulate user internal balance. advancedFarmCalls[0] = IMockFBeanstalk.AdvancedFarmCall( abi.encodeWithSelector(MockAttackFacet.exploitUserInternalTokenBalance.selector), @@ -75,7 +74,7 @@ contract InvariableTest is TestHelper { vm.expectRevert("INV: Insufficient token balance"); vm.prank(siloUsers[1]); bs.advancedFarm(advancedFarmCalls); - + // Exploit Flood plenty. advancedFarmCalls[0] = IMockFBeanstalk.AdvancedFarmCall( abi.encodeWithSelector(MockAttackFacet.exploitSop.selector), diff --git a/protocol/test/foundry/Migration/L2ContractMigration.t.sol b/protocol/test/foundry/Migration/L2ContractMigration.t.sol index 7837345142..af34c039fd 100644 --- a/protocol/test/foundry/Migration/L2ContractMigration.t.sol +++ b/protocol/test/foundry/Migration/L2ContractMigration.t.sol @@ -15,7 +15,7 @@ contract L2ContractMigrationTest is TestHelper { uint256 SIG_TEST_ACCOUNT_PK = 123456789; function setUp() public { - initializeBeanstalkTestState(true, false); + initializeBeanstalkTestState(true, false, false); } /** diff --git a/protocol/test/foundry/convert/convert.t.sol b/protocol/test/foundry/convert/convert.t.sol index 1b4188cb36..7d73344239 100644 --- a/protocol/test/foundry/convert/convert.t.sol +++ b/protocol/test/foundry/convert/convert.t.sol @@ -44,7 +44,7 @@ contract ConvertTest is TestHelper { address well; function setUp() public { - initializeBeanstalkTestState(true, false); + initializeBeanstalkTestState(true, false, false); well = C.BEAN_ETH_WELL; // init user. farmers.push(users[1]); diff --git a/protocol/test/foundry/farm/AdvancedFarm.t.sol b/protocol/test/foundry/farm/AdvancedFarm.t.sol index c932ea5e99..4391b88895 100644 --- a/protocol/test/foundry/farm/AdvancedFarm.t.sol +++ b/protocol/test/foundry/farm/AdvancedFarm.t.sol @@ -22,7 +22,7 @@ contract AdvancedFarmTest is TestHelper { address[] farmers; function setUp() public { - initializeBeanstalkTestState(true, true); + initializeBeanstalkTestState(true, false, true); MockToken(C.WETH).mint(BEANSTALK, 100_000); farmers = createUsers(2); diff --git a/protocol/test/foundry/farm/Farm.t.sol b/protocol/test/foundry/farm/Farm.t.sol index b881261b68..44caba67e6 100644 --- a/protocol/test/foundry/farm/Farm.t.sol +++ b/protocol/test/foundry/farm/Farm.t.sol @@ -20,7 +20,7 @@ contract FarmTest is TestHelper { address[] farmers; function setUp() public { - initializeBeanstalkTestState(true, true); + initializeBeanstalkTestState(true, false, true); MockToken(C.WETH).mint(BEANSTALK, 100_000); farmers = createUsers(2); diff --git a/protocol/test/foundry/farm/PipelineConvert.t.sol b/protocol/test/foundry/farm/PipelineConvert.t.sol index ddedc605df..e836bc76bc 100644 --- a/protocol/test/foundry/farm/PipelineConvert.t.sol +++ b/protocol/test/foundry/farm/PipelineConvert.t.sol @@ -124,9 +124,9 @@ contract PipelineConvertTest is TestHelper { ); function setUp() public { - initializeBeanstalkTestState(true, false); + initializeBeanstalkTestState(true, false, false); - // initalize farmers. + // initialize farmers. farmers.push(users[1]); farmers.push(users[2]); @@ -226,7 +226,7 @@ contract PipelineConvertTest is TestHelper { function testBasicConvertLPToBean(uint256 amount) public { vm.pauseGasMetering(); - // well is initalized with 10000 beans. cap add liquidity + // well is initialized with 10000 beans. cap add liquidity // to reasonable amounts. amount = bound(amount, 1e6, 10000e6); @@ -257,7 +257,7 @@ contract PipelineConvertTest is TestHelper { function testConvertLPToLP(uint256 amount, uint256 inputIndex, uint256 outputIndex) public { vm.pauseGasMetering(); - // well is initalized with 10000 beans. cap add liquidity + // well is initialized with 10000 beans. cap add liquidity // to reasonable amounts. amount = bound(amount, 10e6, 5000e6); @@ -1641,13 +1641,12 @@ contract PipelineConvertTest is TestHelper { ) public returns (int96 stem) { vm.pauseGasMetering(); // amount = bound(amount, 1e6, 5000e6); - bean.mint(user, amount); // setup array of addresses with user address[] memory users = new address[](1); users[0] = user; - (amount, stem) = setUpSiloDepositTest(amount, users); + (amount, stem) = setUpSiloDeposits(amount, users); passGermination(); } diff --git a/protocol/test/foundry/field/Field.t.sol b/protocol/test/foundry/field/Field.t.sol index ea61ea3814..ef9634f5b9 100644 --- a/protocol/test/foundry/field/Field.t.sol +++ b/protocol/test/foundry/field/Field.t.sol @@ -20,9 +20,9 @@ contract FieldTest is TestHelper { address[] farmers; function setUp() public { - initializeBeanstalkTestState(true, false); + initializeBeanstalkTestState(true, false, false); - // initalize farmers from farmers (farmer0 == diamond deployer) + // initialize farmers from farmers (farmer0 == diamond deployer) farmers.push(users[1]); farmers.push(users[2]); @@ -387,7 +387,7 @@ contract FieldTest is TestHelper { uint256 pods = (sowAmount * 101) / 100; portion = bound(portion, 1, pods - 1); field.incrementTotalHarvestableE(activeField, portion); - sowAmountForFarmer(farmers[0], sowAmount); + sowForUser(farmers[0], sowAmount); plotIndexes = field.getPlotIndexesFromAccount(farmers[0], activeField); plots = field.getPlotsFromAccount(farmers[0], activeField); @@ -448,7 +448,7 @@ contract FieldTest is TestHelper { uint256 sowAmount = rand(0, 10e6); uint256 sows = rand(1, 1000); for (uint256 i; i < sows; i++) { - sowAmountForFarmer(farmers[0], sowAmount); + sowForUser(farmers[0], sowAmount); } verifyPlotIndexAndPlotLengths(farmers[0], activeField, sows); uint256 pods = (sowAmount * 101) / 100; @@ -534,7 +534,7 @@ contract FieldTest is TestHelper { field.setActiveField(j, 101); uint256 activeField = field.activeField(); for (uint256 i; i < sowsPerField; i++) { - sowAmountForFarmer(farmers[0], sowAmount); + sowForUser(farmers[0], sowAmount); } } diff --git a/protocol/test/foundry/forkSystem/TransmitToFork.t.sol b/protocol/test/foundry/forkSystem/TransmitToFork.t.sol new file mode 100644 index 0000000000..93bb0c1d7e --- /dev/null +++ b/protocol/test/foundry/forkSystem/TransmitToFork.t.sol @@ -0,0 +1,290 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.6.0 <0.9.0; +pragma abicoder v2; + +import {C} from "contracts/C.sol"; +import {LibAltC} from "test/foundry/utils/LibAltC.sol"; +import {LibConstant} from "test/foundry/utils/LibConstant.sol"; +import {TestHelper} from "test/foundry/utils/TestHelper.sol"; +import {LibTransmitOut} from "contracts/libraries/ForkSystem/LibTransmitOut.sol"; +import {LibTransfer} from "contracts/libraries/Token/LibTransfer.sol"; +import {LibBytes} from "contracts/libraries/LibBytes.sol"; +import {IMockFBeanstalk} from "contracts/interfaces/IMockFBeanstalk.sol"; +import {BeanstalkDeployer} from "test/foundry/utils/BeanstalkDeployer.sol"; +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + +/** + * @notice Tests migrations to a child fork from og Beanstalk. + */ +contract TransmitToForkTest is TestHelper { + IMockFBeanstalk newBs; + address NEW_BEANSTALK; + + uint256 SRC_FIELD = 0; + uint256 DEST_FIELD = 1; + + uint32 constant REPLANT_SEASON = 6074; + + address ZERO_ADDR = LibConstant.ZERO_ADDRESS; + + // test accounts + address[] farmers; + address payable newBeanstalk; + + function setUp() public { + initializeBeanstalkTestState(true, false, false); + + farmers = createUsers(3); + + // max approve. + maxApproveBeanstalk(farmers); + + // Initialize well to balances. (1000 BEAN/ETH) + addLiquidityToWell( + C.BEAN_ETH_WELL, + 10000e6, // 10,000 Beans + 10 ether // 10 ether. + ); + + addLiquidityToWell( + C.BEAN_WSTETH_WELL, + 10000e6, // 10,000 Beans + 10 ether // 10 ether. + ); + initializeUnripeTokens(farmers[0], 100e6, 100e18); + bs.fastForward(REPLANT_SEASON); + + setUpSiloDeposits(10_000e6, farmers); + passGermination(); + + addFertilizerBasedOnSprouts(REPLANT_SEASON, 100e6); + + // Deploy new Beanstalk fork. + TestHelper altEcosystem = new TestHelper(); + altEcosystem.initializeBeanstalkTestState(true, true, false); + altEcosystem.addLiquidityToWell( + C.BEAN_ETH_WELL, + 10000e6, // 10,000 Beans + 10 ether // 10 ether. + ); + altEcosystem.addLiquidityToWell( + C.BEAN_WSTETH_WELL, + 10000e6, // 10,000 Beans + 10 ether // 10 ether. + ); + + newBs = IMockFBeanstalk(altEcosystem.bs()); + NEW_BEANSTALK = address(newBs); + vm.prank(deployer); + newBs.addField(); + newBs.fastForward(100); + } + + /** + * @notice Performs migrations that should revert. + * @dev Can revert at source if assets do not exist or destination if assets not compatible. + */ + // function test_transmitRevert() public {} + + /** + * @notice Performs a migration of all asset types from a Source to Destination fork. + */ + function test_transmitDeposits(uint256 depositAmount) public { + depositAmount = bound(depositAmount, 100, 10_000_000e6); + address user = farmers[2]; + + depositForUser(user, C.BEAN, depositAmount); + int96 stem = bs.stemTipForToken(C.BEAN); + passGermination(); + + // Capture Source state. + (uint256 depositAmount, uint256 depositBdv) = bs.getDeposit(user, C.BEAN, stem); + + IMockFBeanstalk.SourceDeposit[] memory deposits = new IMockFBeanstalk.SourceDeposit[](2); + { + uint256 firstMigrationAmount = depositAmount / 10; + uint256 secondMigrationAmount = depositAmount - firstMigrationAmount; + deposits[0] = IMockFBeanstalk.SourceDeposit( + C.BEAN, + firstMigrationAmount, + stem, + new uint256[](2), // Not used for Bean deposits. + 0, // Not used for Bean deposits. + 0, // populated by source + 0, // populated by source + address(0), // populated by source + 0 // populated by source + ); + deposits[1] = IMockFBeanstalk.SourceDeposit( + C.BEAN, + secondMigrationAmount, + stem, + new uint256[](2), // Not used for Bean deposits. + 0, // Not used for Bean deposits. + 0, // populated by source + 0, // populated by source + address(0), // populated by source + 0 // populated by source + ); + // Check events for burning and minting of bean token. + vm.expectEmit(); + emit IERC20.Transfer(BEANSTALK, ZERO_ADDR, firstMigrationAmount); + vm.expectEmit(); + emit IERC20.Transfer(BEANSTALK, ZERO_ADDR, secondMigrationAmount); + vm.expectEmit(); + emit IERC20.Transfer(ZERO_ADDR, NEW_BEANSTALK, firstMigrationAmount); + vm.expectEmit(); + emit IERC20.Transfer(ZERO_ADDR, NEW_BEANSTALK, secondMigrationAmount); + } + + vm.prank(user); + bs.transmitOut( + NEW_BEANSTALK, + deposits, + new IMockFBeanstalk.SourcePlot[](0), + new IMockFBeanstalk.SourceFertilizer[](0), + abi.encode("") + ); + + // Verify Source deposits. + { + (uint256 sourceDepositAmount, uint256 sourceDepositBdv) = bs.getDeposit( + user, + C.BEAN, + stem + ); + require(sourceDepositAmount == 0, "Source deposit amount mismatch"); + require(sourceDepositBdv == 0, "Source deposit bdv mismatch"); + } + // Verify Destination deposits. + { + uint256[] memory destDepositIds = newBs + .getTokenDepositsForAccount(user, C.BEAN) + .depositIds; + require(destDepositIds.length == 1, "Dest deposit count mismatch"); + (address token, int96 stem) = LibBytes.unpackAddressAndStem(destDepositIds[0]); + (uint256 destinationDepositAmount, uint256 destinationDepositBdv) = newBs.getDeposit( + user, + C.BEAN, // TODO: Can this be made into a new Bean token? + stem + ); + require(stem < newBs.stemTipForToken(token), "Dest deposit stem too high"); + require(depositAmount == destinationDepositAmount, "Dest deposit amount mismatch"); + require(depositBdv == destinationDepositBdv, "Dest deposit bdv mismatch"); + } + } + + /** + * @notice Performs a migration of all asset types from a Source to Destination Beanstalk. + */ + function test_transmitPlots(uint256 sowAmount) public { + sowAmount = bound(sowAmount, 100, 1000e6); + address user = farmers[2]; + + uint256 podsPerSow = sowForUser(user, sowAmount); + uint256 partialAmt = podsPerSow / 3; + uint256 remainingAmt = podsPerSow - partialAmt; + sowForUser(user, sowAmount); + + IMockFBeanstalk.SourcePlot[] memory plots = new IMockFBeanstalk.SourcePlot[](2); + { + // Initial Migration of a Plot. With index != 0. + plots[0] = IMockFBeanstalk.SourcePlot( + SRC_FIELD, // fieldId + podsPerSow, // plotId + podsPerSow, // amount + 0 // prevDestIndex + ); + // Migration of a plot prior to latest transmitted plot. + plots[1] = IMockFBeanstalk.SourcePlot( + SRC_FIELD, // fieldId + 0, // plotId + partialAmt, // amount + 0 // prevDestIndex + ); + } + + vm.prank(user); + bs.transmitOut( + NEW_BEANSTALK, + new IMockFBeanstalk.SourceDeposit[](0), + plots, + new IMockFBeanstalk.SourceFertilizer[](0), + abi.encode("") + ); + + // Verify Source plots. + { + require(bs.plot(user, SRC_FIELD, 0) == 0, "1st src amt"); + require(bs.plot(user, SRC_FIELD, partialAmt) == remainingAmt, "1.5 src amt"); + require(bs.plot(user, SRC_FIELD, podsPerSow) == 0, "2nd src amt"); + require(bs.plot(ZERO_ADDR, SRC_FIELD, 0) == partialAmt, "1st src null amt"); + require(bs.plot(ZERO_ADDR, SRC_FIELD, partialAmt) == 0, "1.5 src null amt"); + require(bs.plot(ZERO_ADDR, SRC_FIELD, podsPerSow) == podsPerSow, "2nd src null amt"); + } + // Verify Destination plots. + { + require(newBs.plot(user, DEST_FIELD, 0) == partialAmt, "1st dest amt"); + require(newBs.plot(ZERO_ADDR, DEST_FIELD, partialAmt) == remainingAmt, "1.5 dest amt"); + require(newBs.plot(user, DEST_FIELD, podsPerSow) == podsPerSow, "2nd dest amt"); + } + } + + /** + * @notice Performs a migration of all asset types from a Source to Destination fork. + */ + function test_transmitFertilizer(uint256 ethAmount) public { + address user = farmers[2]; + + uint128 firstId = bs.getEndBpf(); + uint256 firstFertAmount = buyFertForUser(user, ethAmount); + passGermination(); + uint128 secondId = bs.getEndBpf(); + uint256 secondFertAmount = buyFertForUser(user, ethAmount); + + IMockFBeanstalk.SourceFertilizer[] memory ferts = new IMockFBeanstalk.SourceFertilizer[](2); + { + firstFertAmount /= 3; + ferts[0] = IMockFBeanstalk.SourceFertilizer( + firstId, // id + firstFertAmount, // amount + 0 // _remainingBpf + ); + ferts[1] = IMockFBeanstalk.SourceFertilizer( + secondId, // id + secondFertAmount, // amount + 0 // _remainingBpf + ); + } + + vm.prank(user); + bs.transmitOut( + // TODO change to not transmit into self, but this requires significant changes to initialization system. + BEANSTALK, // Transmit into self + new IMockFBeanstalk.SourceDeposit[](0), + new IMockFBeanstalk.SourcePlot[](0), + ferts, + abi.encode("") + ); + + // NOTE: Cannot do this yet, bc source == destination. + // // Verify Source fertilizer. + // { + // require(bs.plot(user, 0, 0) == 0, "First source plot amount mismatch"); + // require(bs.plot(user, 0, podsPerSow) == 0, "Second source plot amount mismatch"); + // } + // Verify Destination fertilizer. + { + // require(fertilizer.balanceOf(user, firstId) == firstFertAmount, "Dest fert amount mismatch"); + require( + fertilizer.balanceOf(user, secondId) == secondFertAmount, + "Dest fert amount mismatch" + ); + } + } + + /** + * @notice Performs a migration of all asset types from a Source to Destination Beanstalk. + */ + // function test_transmitAll() public {} +} diff --git a/protocol/test/foundry/invariantTesting/BeanstalkHandler.sol b/protocol/test/foundry/invariantTesting/BeanstalkHandler.sol index ba3f8e3bb6..44bd5f63f2 100644 --- a/protocol/test/foundry/invariantTesting/BeanstalkHandler.sol +++ b/protocol/test/foundry/invariantTesting/BeanstalkHandler.sol @@ -12,7 +12,7 @@ import {MockToken} from "contracts/mocks/MockToken.sol"; import "forge-std/Test.sol"; contract BeanstalkHandler is Test { - address constant BEANSTALK = LibConstant.BEANSTALK; + address BEANSTALK; address[] public depositors; IERC20 public bean = IERC20(C.BEAN); @@ -43,13 +43,17 @@ contract BeanstalkHandler is Test { // tokens.push(C.BEAN_WSTETH_WELL); // is not mock erc20 } - function deposit(uint16 userSeed, uint16 tokenSeed, uint256 amount) public useUser(userSeed) useToken(tokenSeed) { + function deposit( + uint16 userSeed, + uint16 tokenSeed, + uint256 amount + ) public useUser(userSeed) useToken(tokenSeed) { amount = bound(amount, 1, type(uint96).max); MockToken(token).mint(user, amount); bean.approve(address(bs), type(uint256).max); - (,, int96 stem) = bs.deposit(token, amount, 0); + (, , int96 stem) = bs.deposit(token, amount, 0); depositSumsTotal[token] += amount; depositSumsUser[user][token] += amount; @@ -61,18 +65,25 @@ contract BeanstalkHandler is Test { userDeposits[user][token].push(stem); } - function withdraw(uint16 userSeed, uint16 tokenSeed, uint256 stemSeed, uint256 amount) public useUser(userSeed) useToken(tokenSeed) { + function withdraw( + uint16 userSeed, + uint16 tokenSeed, + uint256 stemSeed, + uint256 amount + ) public useUser(userSeed) useToken(tokenSeed) { // vm.assume(userDeposits[user][token].length > 0); if (userDeposits[user][token].length == 0) return; uint256 stemIndex = bound(stemSeed, 0, userDeposits[user][token].length - 1); int96 stem = userDeposits[user][token][stemIndex]; - (uint256 depositAmount,) = bs.getDeposit(user, token, stem); + (uint256 depositAmount, ) = bs.getDeposit(user, token, stem); amount = bound(amount, 1, depositAmount); bs.withdrawDeposit(token, stem, amount, 0); if (amount == depositAmount) { - userDeposits[user][token][stemIndex] = userDeposits[user][token][userDeposits[user][token].length - 1]; + userDeposits[user][token][stemIndex] = userDeposits[user][token][ + userDeposits[user][token].length - 1 + ]; userDeposits[user][token].pop(); } diff --git a/protocol/test/foundry/invariantTesting/BeanstalkInvariants.t.sol b/protocol/test/foundry/invariantTesting/BeanstalkInvariants.t.sol index e7eb91643e..46a876ac23 100644 --- a/protocol/test/foundry/invariantTesting/BeanstalkInvariants.t.sol +++ b/protocol/test/foundry/invariantTesting/BeanstalkInvariants.t.sol @@ -25,7 +25,7 @@ contract BeanstalkInvariants is TestHelper { function setUp() public { // vm.createSelectFork(vm.envString("FORKING_RPC"), 19976370); - initializeBeanstalkTestState(true, false); + initializeBeanstalkTestState(true, false, false); // Initialize well to balances. (1000 BEAN/ETH) addLiquidityToWell( diff --git a/protocol/test/foundry/silo/Oracle.t.sol b/protocol/test/foundry/silo/Oracle.t.sol index 62022f511d..729eb6ac02 100644 --- a/protocol/test/foundry/silo/Oracle.t.sol +++ b/protocol/test/foundry/silo/Oracle.t.sol @@ -10,7 +10,7 @@ import {OracleFacet} from "contracts/beanstalk/sun/OracleFacet.sol"; */ contract OracleTest is TestHelper { function setUp() public { - initializeBeanstalkTestState(true, false); + initializeBeanstalkTestState(true, false, false); } function test_getUsdPrice() public { diff --git a/protocol/test/foundry/silo/Silo.t.sol b/protocol/test/foundry/silo/Silo.t.sol index 959c3ad62c..d36a816c08 100644 --- a/protocol/test/foundry/silo/Silo.t.sol +++ b/protocol/test/foundry/silo/Silo.t.sol @@ -18,9 +18,9 @@ contract SiloTest is TestHelper { address[] farmers; function setUp() public { - initializeBeanstalkTestState(true, false); + initializeBeanstalkTestState(true, false, false); - // initalize farmers from farmers (farmer0 == diamond deployer) + // initialize farmers from farmers (farmer0 == diamond deployer) farmers.push(users[1]); farmers.push(users[2]); diff --git a/protocol/test/foundry/silo/Whitelist.t.sol b/protocol/test/foundry/silo/Whitelist.t.sol index 005aa27881..2c26f9e8e7 100644 --- a/protocol/test/foundry/silo/Whitelist.t.sol +++ b/protocol/test/foundry/silo/Whitelist.t.sol @@ -37,7 +37,7 @@ contract WhitelistTest is TestHelper { event DewhitelistToken(address indexed token); function setUp() public { - initializeBeanstalkTestState(true, false); + initializeBeanstalkTestState(true, false, false); } // reverts if not owner. diff --git a/protocol/test/foundry/sun/Cases.t.sol b/protocol/test/foundry/sun/Cases.t.sol index 927c91b5f2..ecacea8aab 100644 --- a/protocol/test/foundry/sun/Cases.t.sol +++ b/protocol/test/foundry/sun/Cases.t.sol @@ -37,12 +37,12 @@ contract CasesTest is TestHelper { int256 deltaB; function setUp() public { - initializeBeanstalkTestState(true, false); + initializeBeanstalkTestState(true, false, false); // Initialize well to balances. (1000 BEAN/ETH) addLiquidityToWell(well, 10000e6, 10 ether); - // call well to wsteth/bean to initalize the well. + // call well to wsteth/bean to initialize the well. // avoids errors due to gas limits. addLiquidityToWell(C.BEAN_WSTETH_WELL, 10e6, .01 ether); } diff --git a/protocol/test/foundry/sun/Flood.t.sol b/protocol/test/foundry/sun/Flood.t.sol index 7357574af8..24b38b85df 100644 --- a/protocol/test/foundry/sun/Flood.t.sol +++ b/protocol/test/foundry/sun/Flood.t.sol @@ -33,7 +33,7 @@ contract FloodTest is TestHelper { event SeasonOfPlentyField(uint256 toField); function setUp() public { - initializeBeanstalkTestState(true, false); + initializeBeanstalkTestState(true, false, false); // init user. farmers.push(users[1]); vm.prank(farmers[0]); diff --git a/protocol/test/foundry/sun/Gauge.t.sol b/protocol/test/foundry/sun/Gauge.t.sol index def0355215..278f05807c 100644 --- a/protocol/test/foundry/sun/Gauge.t.sol +++ b/protocol/test/foundry/sun/Gauge.t.sol @@ -22,7 +22,7 @@ contract GaugeTest is TestHelper { MockLiquidityWeight lw = MockLiquidityWeight(BEANSTALK); function setUp() public { - initializeBeanstalkTestState(true, false); + initializeBeanstalkTestState(true, false, false); // deploy mockLiquidityWeight contract for testing. lw = new MockLiquidityWeight(0.5e18); diff --git a/protocol/test/foundry/sun/Germination.t.sol b/protocol/test/foundry/sun/Germination.t.sol index a2a46c7935..fa8696e311 100644 --- a/protocol/test/foundry/sun/Germination.t.sol +++ b/protocol/test/foundry/sun/Germination.t.sol @@ -22,7 +22,7 @@ contract GerminationTest is TestHelper { address[] farmers; function setUp() public { - initializeBeanstalkTestState(true, false); + initializeBeanstalkTestState(true, false, false); // mint 1000 beans to user 1 and user 2 (user 0 is the beanstalk deployer). farmers.push(users[1]); @@ -38,7 +38,7 @@ contract GerminationTest is TestHelper { */ function test_depositGerminates(uint256 amount) public { // deposits bean into the silo. - (amount, ) = setUpSiloDepositTest(amount, farmers); + (amount, ) = setUpSiloDeposits(amount, farmers); // verify new state of silo. checkSiloAndUser(users[1], 0, amount); @@ -50,7 +50,7 @@ contract GerminationTest is TestHelper { */ function test_depositsContGerminating(uint256 amount) public { // deposits bean into the silo. - (amount, ) = setUpSiloDepositTest(amount, farmers); + (amount, ) = setUpSiloDeposits(amount, farmers); // call sunrise. season.siloSunrise(0); @@ -65,7 +65,7 @@ contract GerminationTest is TestHelper { */ function test_depositsEndGermination(uint256 amount) public { // deposits bean into the silo. - (amount, ) = setUpSiloDepositTest(amount, farmers); + (amount, ) = setUpSiloDeposits(amount, farmers); // call sunrise twice. season.siloSunrise(0); @@ -83,7 +83,7 @@ contract GerminationTest is TestHelper { function test_withdrawGerminating(uint256 amount) public { // deposits bean into the silo. int96 stem; - (amount, stem) = setUpSiloDepositTest(amount, farmers); + (amount, stem) = setUpSiloDeposits(amount, farmers); // withdraw beans from silo from user 1 and 2. withdrawDepositForUsers(farmers, C.BEAN, stem, amount, LibTransfer.To.EXTERNAL); @@ -100,7 +100,7 @@ contract GerminationTest is TestHelper { function test_withdrawGerminatingCont(uint256 amount) public { // deposits bean into the silo. int96 stem; - (amount, stem) = setUpSiloDepositTest(amount, farmers); + (amount, stem) = setUpSiloDeposits(amount, farmers); // call sunrise. season.siloSunrise(0); @@ -121,7 +121,7 @@ contract GerminationTest is TestHelper { function test_transferGerminating(uint256 amount) public { // deposits bean into the silo. int96 stem; - (amount, stem) = setUpSiloDepositTest(amount, farmers); + (amount, stem) = setUpSiloDeposits(amount, farmers); uint256 grownStalk = bs.balanceOfGrownStalk(users[1], C.BEAN); farmers.push(users[3]); @@ -141,7 +141,7 @@ contract GerminationTest is TestHelper { function test_transferGerminatingCont(uint256 amount) public { // deposits bean into the silo. int96 stem; - (amount, stem) = setUpSiloDepositTest(amount, farmers); + (amount, stem) = setUpSiloDeposits(amount, farmers); season.siloSunrise(0); farmers.push(users[3]); farmers.push(users[4]); @@ -265,7 +265,7 @@ contract GerminationTest is TestHelper { address newFarmer ) public returns (uint256 _amount) { // deposit 'amount' beans to the silo. - (_amount, ) = setUpSiloDepositTest(amount, initalFarmers); + (_amount, ) = setUpSiloDeposits(amount, initalFarmers); // call sunrise twice to finish the germination process. season.siloSunrise(0); @@ -277,7 +277,7 @@ contract GerminationTest is TestHelper { mintTokensToUsers(farmer, C.BEAN, MAX_DEPOSIT_BOUND); // deposit into the silo. - setUpSiloDepositTest(amount, farmer); + setUpSiloDeposits(amount, farmer); } ////// ASSERTIONS ////// diff --git a/protocol/test/foundry/sun/Oracle.t.sol b/protocol/test/foundry/sun/Oracle.t.sol index 94898ea9ed..c05b5e5ce4 100644 --- a/protocol/test/foundry/sun/Oracle.t.sol +++ b/protocol/test/foundry/sun/Oracle.t.sol @@ -38,7 +38,7 @@ contract OracleTest is TestHelper { address[] lps; function setUp() public { - initializeBeanstalkTestState(true, false); + initializeBeanstalkTestState(true, false, false); farmers.push(users[1]); // add liquidity for the bean weth well, and bean wsteth well. diff --git a/protocol/test/foundry/sun/Sun.t.sol b/protocol/test/foundry/sun/Sun.t.sol index 04e5b3e990..9bae069c24 100644 --- a/protocol/test/foundry/sun/Sun.t.sol +++ b/protocol/test/foundry/sun/Sun.t.sol @@ -19,7 +19,7 @@ contract SunTest is TestHelper { MockSeasonFacet season = MockSeasonFacet(BEANSTALK); function setUp() public { - initializeBeanstalkTestState(true, true); + initializeBeanstalkTestState(true, false, true); } /** diff --git a/protocol/test/foundry/sun/Sunrise.t.sol b/protocol/test/foundry/sun/Sunrise.t.sol index 10346e884e..d489b350ab 100644 --- a/protocol/test/foundry/sun/Sunrise.t.sol +++ b/protocol/test/foundry/sun/Sunrise.t.sol @@ -48,7 +48,7 @@ contract SunriseTest is TestHelper { uint256 constant SEASON_DURATION = 3600; function setUp() public { - initializeBeanstalkTestState(true, false); + initializeBeanstalkTestState(true, false, false); farmers.push(users[1]); // add liquidity for the bean weth well, and bean wsteth well. diff --git a/protocol/test/foundry/upgrade.t.sol b/protocol/test/foundry/upgrade.t.sol index 44765f1ce2..a90a16e93a 100644 --- a/protocol/test/foundry/upgrade.t.sol +++ b/protocol/test/foundry/upgrade.t.sol @@ -50,7 +50,7 @@ contract UpgradeDiamond is TestHelper { ); } - function testWoohoo() public pure { + function testWoohoo() public view { // verify facet is added (call woohoo()). assertEq(MockUpgradeFacet(BEANSTALK).woohoo(), 1); } diff --git a/protocol/test/foundry/utils/BasinDeployer.sol b/protocol/test/foundry/utils/BasinDeployer.sol index 800991370c..e996dcc7c0 100644 --- a/protocol/test/foundry/utils/BasinDeployer.sol +++ b/protocol/test/foundry/utils/BasinDeployer.sol @@ -49,6 +49,9 @@ contract BasinDeployer is Utils { string constant BEAN_WETH_WELL_NAME = "BEAN:WETH Constant Product 2 Well"; string constant BEAN_WETH_WELL_SYMBOL = "BEANWETHCP2w"; + address BEAN_ETH_WELL; + address BEAN_WSTETH_WELL; + // a list of well functions, pumps, and well implementations. address public aquifer; @@ -63,6 +66,7 @@ contract BasinDeployer is Utils { * at current mainnet addresses. */ function initBasin(bool mock, bool verbose) internal { + if (verbose) console.log("deploying Basin..."); deployBasin(verbose); @@ -174,14 +178,14 @@ contract BasinDeployer is Utils { } // deploy bean eth well: - wells.push(deployBeanCp2Well([C.BEAN_ETH_WELL, C.WETH], _pump)); + wells.push(deployBeanCp2Well([BEAN_ETH_WELL, C.WETH], _pump)); if (verbose) console.log("Bean Eth well deployed at:", wells[0]); - vm.label(C.BEAN_ETH_WELL, "BEAN/ETH Well"); + vm.label(BEAN_ETH_WELL, "BEAN/ETH Well"); // deploy bean wsteth well: - wells.push(deployBeanCp2Well([C.BEAN_WSTETH_WELL, C.WSTETH], _pump)); + wells.push(deployBeanCp2Well([BEAN_WSTETH_WELL, C.WSTETH], _pump)); if (verbose) console.log("Bean wstEth well deployed at:", wells[1]); - vm.label(C.BEAN_WSTETH_WELL, "BEAN/WSTETH Well"); + vm.label(BEAN_WSTETH_WELL, "BEAN/WSTETH Well"); } function deployExtraWells(bool mock, bool verbose) internal { diff --git a/protocol/test/foundry/utils/BeanstalkDeployer.sol b/protocol/test/foundry/utils/BeanstalkDeployer.sol index 6914bfa992..96aef8c6d0 100644 --- a/protocol/test/foundry/utils/BeanstalkDeployer.sol +++ b/protocol/test/foundry/utils/BeanstalkDeployer.sol @@ -10,7 +10,7 @@ import {Utils, console} from "test/foundry/utils/Utils.sol"; import {Diamond} from "contracts/beanstalk/Diamond.sol"; import {IDiamondCut} from "contracts/interfaces/IDiamondCut.sol"; import {MockInitDiamond} from "contracts/mocks/newMockInitDiamond.sol"; -import {InitDiamond} from "contracts/beanstalk/init/newInitDiamond.sol"; +import {InitDiamond} from "contracts/beanstalk/init/InitDiamond.sol"; import {DiamondLoupeFacet} from "contracts/beanstalk/diamond/DiamondLoupeFacet.sol"; /// Beanstalk Contracts w/external libraries. @@ -46,7 +46,9 @@ contract BeanstalkDeployer is Utils { "PipelineConvertFacet", "ClaimFacet", "OracleFacet", - "L2ContractMigrationFacet" + "L2ContractMigrationFacet", + "TransmitOutFacet", + "TransmitInFacet" ]; // Facets that have a mock counter part should be appended here. @@ -67,11 +69,14 @@ contract BeanstalkDeployer is Utils { * @notice deploys the beanstalk diamond contract. * @param mock if true, deploys all mocks and sets the diamond address to the canonical beanstalk address. */ - function setupDiamond(bool mock, bool verbose) internal returns (Diamond d) { + function setupDiamond( + address payable diamondAddr, + bool mock, + bool verbose + ) public returns (Diamond d) { users = createUsers(6); deployer = users[0]; vm.label(deployer, "Deployer"); - vm.label(BEANSTALK, "Beanstalk"); // Create cuts. @@ -143,7 +148,7 @@ contract BeanstalkDeployer is Utils { cutActions.push(IDiamondCut.FacetCutAction.Add); } IDiamondCut.FacetCut[] memory cut = _multiCut(facets, facetAddresses, cutActions); - d = deployDiamondAtAddress(deployer, BEANSTALK); + d = deployDiamondAtAddress(deployer, diamondAddr); // if mocking, set the diamond address to // the canonical beanstalk address. diff --git a/protocol/test/foundry/utils/FertilizerDeployer.sol b/protocol/test/foundry/utils/FertilizerDeployer.sol index 0e9a5f074c..a83de3fefc 100644 --- a/protocol/test/foundry/utils/FertilizerDeployer.sol +++ b/protocol/test/foundry/utils/FertilizerDeployer.sol @@ -6,7 +6,7 @@ pragma abicoder v2; import {Utils, console} from "test/foundry/utils/Utils.sol"; import {Fertilizer} from "contracts/tokens/Fertilizer/Fertilizer.sol"; -import {C} from "contracts/C.sol"; +import {IFertilizer} from "contracts/interfaces/IFertilizer.sol"; interface IOwner { function transferOwnership(address newOwner) external; @@ -19,16 +19,18 @@ interface IOwner { * @notice Test helper contract to deploy Fertilizer. */ contract FertilizerDeployer is Utils { + address FERTILIZER; + IFertilizer fertilizer; + function initFertilizer(bool verbose) internal { - address fertilizerAddress = C.fertilizerAddress(); - deployCodeTo("Fertilizer", fertilizerAddress); - if (verbose) console.log("Fertilizer deployed at: ", fertilizerAddress); + deployCodeTo("Fertilizer", FERTILIZER); + if (verbose) console.log("Fertilizer deployed at: ", FERTILIZER); + fertilizer = IFertilizer(FERTILIZER); } function transferFertilizerOwnership(address newOwner) internal { - address fertilizer = C.fertilizerAddress(); - vm.prank(IOwner(fertilizer).owner()); - IOwner(fertilizer).transferOwnership(newOwner); + vm.prank(IOwner(FERTILIZER).owner()); + IOwner(FERTILIZER).transferOwnership(newOwner); } function mintFertilizer() internal {} // TODO diff --git a/protocol/test/foundry/utils/LibAltC.sol b/protocol/test/foundry/utils/LibAltC.sol new file mode 100644 index 0000000000..a8e3fd40d6 --- /dev/null +++ b/protocol/test/foundry/utils/LibAltC.sol @@ -0,0 +1,24 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.20; + +/** + * @title LibAltC + * @notice Contains alternative Beanstalk constants for use with a second Beanstalk ecosystem in testing. + * Only contains address constants that diverge from actual Beanstalk. + */ +library LibAltC { + + address constant BEANSTALK = 0x00F84c1cF4Ca7fa8A8b0Dc923DA91ACA148B865C; + address internal constant BEAN = 0x006DD9acC7cDf83128C4aDF46847c301f94406ab; + + address internal constant FERTILIZER = 0x00d180156a2680F1e776b165080200cffaDa463d; + // address private constant FERTILIZER_ADMIN = ; + + address internal constant BEAN_ETH_WELL = 0x00c09d34D248ad13373193aA5Bc31876AfD577B5; + address internal constant BEAN_WSTETH_WELL = 0x00Ba43411e3Bd86a49500778021a1b101A18cB94; + + // address constant PRICE_DEPLOYER = ; + // address constant PRICE = ; + +} diff --git a/protocol/test/foundry/utils/OracleDeployer.sol b/protocol/test/foundry/utils/OracleDeployer.sol index 17d2dde6f3..c156f1a8d2 100644 --- a/protocol/test/foundry/utils/OracleDeployer.sol +++ b/protocol/test/foundry/utils/OracleDeployer.sol @@ -59,7 +59,7 @@ contract OracleDeployer is Utils { [WBTC_USDC_03_POOL, WBTC, C.USDC] // WBTC/USDC ]; - // oracles must be initalized at some price. Assumes index matching with pools. + // oracles must be initialized at some price. Assumes index matching with pools. uint256[][] public priceData = [[uint256(1e18), 18], [uint256(50000e2), 6]]; /** diff --git a/protocol/test/foundry/utils/TestHelper.sol b/protocol/test/foundry/utils/TestHelper.sol index a2cee015f0..d291b2859c 100644 --- a/protocol/test/foundry/utils/TestHelper.sol +++ b/protocol/test/foundry/utils/TestHelper.sol @@ -17,6 +17,8 @@ import {DepotDeployer} from "test/foundry/utils/DepotDeployer.sol"; import {OracleDeployer} from "test/foundry/utils/OracleDeployer.sol"; import {FertilizerDeployer} from "test/foundry/utils/FertilizerDeployer.sol"; import {ShipmentDeployer} from "test/foundry/utils/ShipmentDeployer.sol"; +import {LibAltC} from "test/foundry/utils/LibAltC.sol"; +import {LibConstant} from "test/foundry/utils/LibConstant.sol"; import {LibWell, IWell, IERC20} from "contracts/libraries/Well/LibWell.sol"; import {C} from "contracts/C.sol"; import {LibAppStorage} from "contracts/libraries/LibAppStorage.sol"; @@ -34,6 +36,9 @@ import {Pipeline} from "contracts/pipeline/Pipeline.sol"; * @title TestHelper * @author Brean * @notice Test helper contract for Beanstalk tests. + * + * This contract represents the initialization of a fresh Beanstalk system for testing. + * All deployment addresses for Beanstalk ecosystem contracts are set in this contract. */ contract TestHelper is Test, @@ -49,7 +54,7 @@ contract TestHelper is Pipeline pipeline; - MockToken bean = MockToken(C.BEAN); + MockToken bean; // ideally, timestamp should be set to 1_000_000. // however, beanstalk rounds down to the nearest hour. @@ -73,7 +78,24 @@ contract TestHelper is /** * @notice initializes the state of the beanstalk contracts for testing. */ - function initializeBeanstalkTestState(bool mock, bool verbose) public { + function initializeBeanstalkTestState(bool mock, bool alt, bool verbose) public { + // Set the deployment addresses of various Beanstalk components. + if (!alt) { + BEANSTALK = payable(LibConstant.BEANSTALK); + bean = MockToken(C.BEAN); + FERTILIZER = C.fertilizerAddress(); + BEAN_ETH_WELL = C.BEAN_ETH_WELL; + BEAN_WSTETH_WELL = C.BEAN_WSTETH_WELL; + vm.label(BEANSTALK, "Beanstalk_"); + } else { + BEANSTALK = payable(LibAltC.BEANSTALK); + bean = MockToken(LibAltC.BEAN); + FERTILIZER = LibAltC.FERTILIZER; + BEAN_ETH_WELL = LibAltC.BEAN_ETH_WELL; + BEAN_WSTETH_WELL = LibAltC.BEAN_WSTETH_WELL; + vm.label(BEANSTALK, "Alt_Beanstalk"); + } + // general mock interface for beanstalk. bs = IMockFBeanstalk(BEANSTALK); @@ -84,7 +106,7 @@ contract TestHelper is // as starting from an timestamp of 0 can cause issues. vm.warp(INITIAL_TIMESTAMP); - // initalize mock tokens. + // initialize mock tokens. initMockTokens(verbose); // initialize Depot: @@ -104,8 +126,8 @@ contract TestHelper is initFertilizer(verbose); transferFertilizerOwnership(BEANSTALK); - // initialize Diamond, initalize users: - setupDiamond(mock, verbose); + // initialize Diamond, initialize users: + setupDiamond(BEANSTALK, mock, verbose); // Initialize Shipment Routes and Plans. initShipping(verbose); @@ -144,7 +166,7 @@ contract TestHelper is address user, uint256 unripeBeanAmount, uint256 unripeLpAmount - ) internal { + ) public { // mint tokens to users. mintTokensToUser(user, C.UNRIPE_BEAN, unripeBeanAmount); mintTokensToUser(user, C.UNRIPE_LP, unripeLpAmount); @@ -221,7 +243,7 @@ contract TestHelper is address well, uint256 beanAmount, uint256 nonBeanTokenAmount - ) internal returns (uint256) { + ) public returns (uint256) { return addLiquidityToWell(users[0], well, beanAmount, nonBeanTokenAmount); } @@ -233,7 +255,7 @@ contract TestHelper is address well, uint256 beanAmount, uint256 nonBeanTokenAmount - ) internal returns (uint256 lpOut) { + ) public returns (uint256 lpOut) { (address nonBeanToken, ) = LibWell.getNonBeanTokenAndIndexFromWell(well); // mint and sync. @@ -517,17 +539,17 @@ contract TestHelper is /** * @notice Set up the silo deposit test by depositing beans to the silo from multiple users. * @param amount The amount of beans to deposit. - * @return _amount The actual amount of beans deposited. + * @return amount The actual amount of beans deposited. * @return stem The stem tip for the deposited beans. */ - function setUpSiloDepositTest( + function setUpSiloDeposits( uint256 amount, - address[] memory _farmers - ) public returns (uint256 _amount, int96 stem) { - _amount = bound(amount, 1, MAX_DEPOSIT_BOUND); - - depositForUsers(_farmers, C.BEAN, _amount, LibTransfer.From.EXTERNAL); - stem = bs.stemTipForToken(C.BEAN); + address[] memory users + ) public returns (uint256, int96) { + amount = bound(amount, 1, MAX_DEPOSIT_BOUND); + depositForUsers(users, C.BEAN, amount, LibTransfer.From.EXTERNAL); + int96 stem = bs.stemTipForToken(C.BEAN); + return (amount, stem); } /** @@ -544,20 +566,29 @@ contract TestHelper is LibTransfer.From mode ) public { for (uint256 i = 0; i < users.length; i++) { + mintTokensToUser(users[i], C.BEAN, amount); vm.prank(users[i]); - bs.deposit(token, amount, uint8(mode)); // switching from silo.deposit to bs.deposit, but bs does not have a From enum, so casting to uint8. + bs.deposit(token, amount, uint8(mode)); } } /** - * @notice mints `sowAmount` beans for farmer, - * issues `sowAmount` of beans to farmer. - * sows `sowAmount` of beans. + * @notice Sows beans for a user. */ - function sowAmountForFarmer(address farmer, uint256 sowAmount) internal { - bs.setSoilE(sowAmount); - mintTokensToUser(farmer, C.BEAN, sowAmount); - vm.prank(farmer); - bs.sow(sowAmount, 0, uint8(LibTransfer.From.EXTERNAL)); + function sowForUser(address user, uint256 beans) internal returns (uint256 pods) { + bs.setSoilE(beans); + mintTokensToUser(user, C.BEAN, beans); + vm.prank(user); + return bs.sow(beans, 0, uint8(LibTransfer.From.EXTERNAL)); + } + + function buyFertForUser( + address user, + uint256 ethAmount + ) internal returns (uint256 fertilizerAmountOut) { + ethAmount = bound(ethAmount, 1e18 / 1000, 100e18); + mintTokensToUser(user, bs.getBarnRaiseToken(), ethAmount); + vm.prank(user); + return bs.mintFertilizer(ethAmount, 0, 0); } } diff --git a/protocol/test/foundry/utils/Utils.sol b/protocol/test/foundry/utils/Utils.sol index c29dba5e54..66de924327 100644 --- a/protocol/test/foundry/utils/Utils.sol +++ b/protocol/test/foundry/utils/Utils.sol @@ -11,9 +11,8 @@ import {IMockFBeanstalk} from "contracts/interfaces/IMockFBeanstalk.sol"; */ contract Utils is Test { // beanstalk - address payable constant BEANSTALK = - payable(address(0xC1E088fC1323b20BCBee9bd1B9fC9546db5624C5)); - IMockFBeanstalk bs; + address payable public BEANSTALK; + IMockFBeanstalk public bs; address internal deployer; using Strings for uint256;