diff --git a/protocol/contracts/beanstalk/sun/OracleFacet.sol b/protocol/contracts/beanstalk/sun/OracleFacet.sol index c5e70886b..ef84026c0 100644 --- a/protocol/contracts/beanstalk/sun/OracleFacet.sol +++ b/protocol/contracts/beanstalk/sun/OracleFacet.sol @@ -87,7 +87,16 @@ contract OracleFacet is Invariable, ReentrancyGuard { function getRatiosAndBeanIndex( IERC20[] memory tokens, uint256 lookback - ) internal view returns (uint[] memory ratios, uint beanIndex, bool success) { + ) external view returns (uint[] memory ratios, uint beanIndex, bool success) { (ratios, beanIndex, success) = LibWell.getRatiosAndBeanIndex(tokens, lookback); } + + /** + * @notice Fetches the amount of tokens equal to 1 Million USD for a given token. + * @param token address of the token to get the amount for. + * @param lookback the amount of time to look back in seconds. + */ + function getMillionUsdPrice(address token, uint256 lookback) external view returns (uint256) { + return LibUsdOracle.getMillionUsdPrice(token, lookback); + } } diff --git a/protocol/contracts/interfaces/IMockFBeanstalk.sol b/protocol/contracts/interfaces/IMockFBeanstalk.sol index 7e12ee3a2..add13b11f 100644 --- a/protocol/contracts/interfaces/IMockFBeanstalk.sol +++ b/protocol/contracts/interfaces/IMockFBeanstalk.sol @@ -1530,6 +1530,8 @@ interface IMockFBeanstalk { GerminationSide side ) external; + function mockInitState() external; + function mockLiquidityWeight() external pure returns (uint256); function mockSetAverageGrownStalkPerBdvPerSeason( @@ -1715,6 +1717,8 @@ interface IMockFBeanstalk { function poolCurrentDeltaB(address pool) external view returns (int256 deltaB); + function poolCurrentDeltaBMock(address pool) external view returns (int256 deltaB); + function poolDeltaB(address pool) external view returns (int256); function publishRequisition(Requisition memory requisition) external; diff --git a/protocol/contracts/libraries/Oracle/LibChainlinkOracle.sol b/protocol/contracts/libraries/Oracle/LibChainlinkOracle.sol index 686f4e9a6..5594f14a1 100644 --- a/protocol/contracts/libraries/Oracle/LibChainlinkOracle.sol +++ b/protocol/contracts/libraries/Oracle/LibChainlinkOracle.sol @@ -41,12 +41,22 @@ library LibChainlinkOracle { address priceAggregatorAddress, uint256 maxTimeout, uint256 tokenDecimals, - uint256 lookback + uint256 lookback, + bool isMillion ) internal view returns (uint256 price) { return lookback > 0 - ? getTwap(priceAggregatorAddress, maxTimeout, tokenDecimals, lookback) - : getPrice(priceAggregatorAddress, maxTimeout, tokenDecimals); + ? getTwap(priceAggregatorAddress, maxTimeout, tokenDecimals, lookback, isMillion) + : getPrice(priceAggregatorAddress, maxTimeout, tokenDecimals, isMillion); + } + + function getTokenPrice( + address priceAggregatorAddress, + uint256 maxTimeout, + uint256 tokenDecimals, + uint256 lookback + ) internal view returns (uint256 price) { + return getTokenPrice(priceAggregatorAddress, maxTimeout, tokenDecimals, lookback, false); } /** @@ -59,7 +69,8 @@ library LibChainlinkOracle { function getPrice( address priceAggregatorAddress, uint256 maxTimeout, - uint256 tokenDecimals + uint256 tokenDecimals, + bool isMillion ) internal view returns (uint256 price) { IChainlinkAggregator priceAggregator = IChainlinkAggregator(priceAggregatorAddress); // First, try to get current decimal precision: @@ -88,6 +99,9 @@ library LibChainlinkOracle { // if token decimals is greater than 0, return the TOKEN2/TOKEN1 price instead (i.e invert the price). if (tokenDecimals > 0) { + // if `isMillion` is set, return `MillionTOKEN2/TOKEN1` Price instead + // (i.e, the amount of TOKEN1 equal to a million of TOKEN2) + if (isMillion) tokenDecimals = tokenDecimals + 6; price = uint256(10 ** (tokenDecimals + decimals)).div(uint256(answer)); } else { // Adjust to 6 decimal precision. @@ -109,12 +123,12 @@ library LibChainlinkOracle { address priceAggregatorAddress, uint256 maxTimeout, uint256 tokenDecimals, - uint256 lookback + uint256 lookback, + bool isMillion ) internal view returns (uint256 price) { - IChainlinkAggregator priceAggregator = IChainlinkAggregator(priceAggregatorAddress); // First, try to get current decimal precision: uint8 decimals; - try priceAggregator.decimals() returns (uint8 _decimals) { + try IChainlinkAggregator(priceAggregatorAddress).decimals() returns (uint8 _decimals) { // If call to Chainlink succeeds, record the current decimal precision decimals = _decimals; } catch { @@ -123,7 +137,7 @@ library LibChainlinkOracle { } // Secondly, try to get latest price data: - try priceAggregator.latestRoundData() returns ( + try IChainlinkAggregator(priceAggregatorAddress).latestRoundData() returns ( uint80 roundId, int256 answer, uint256 /* startedAt */, @@ -139,6 +153,11 @@ library LibChainlinkOracle { TwapVariables memory t; t.endTimestamp = block.timestamp.sub(lookback); + + if (isMillion) { + // if `isMillion` flag is enabled, + tokenDecimals = tokenDecimals + 6; + } // Check if last round was more than `lookback` ago. if (timestamp <= t.endTimestamp) { if (tokenDecimals > 0) { @@ -161,7 +180,10 @@ library LibChainlinkOracle { ); roundId -= 1; t.lastTimestamp = timestamp; - (answer, timestamp) = getRoundData(priceAggregator, roundId); + (answer, timestamp) = getRoundData( + IChainlinkAggregator(priceAggregatorAddress), + roundId + ); if ( checkForInvalidTimestampOrAnswer( timestamp, diff --git a/protocol/contracts/libraries/Oracle/LibUniswapOracle.sol b/protocol/contracts/libraries/Oracle/LibUniswapOracle.sol index 6c0d71733..0286a9e3b 100644 --- a/protocol/contracts/libraries/Oracle/LibUniswapOracle.sol +++ b/protocol/contracts/libraries/Oracle/LibUniswapOracle.sol @@ -15,9 +15,6 @@ interface IERC20Decimals { /** * @title Uniswap Oracle Library * @notice Contains functionalty to read prices from Uniswap V3 pools. - * @dev currently supports: - * - ETH:USDC price from the ETH:USDC 0.05% pool - * - ETH:USDT price from the ETH:USDT 0.05% pool **/ library LibUniswapOracle { // All instantaneous queries of Uniswap Oracles should use a 15 minute lookback. diff --git a/protocol/contracts/libraries/Oracle/LibUsdOracle.sol b/protocol/contracts/libraries/Oracle/LibUsdOracle.sol index 5116b1fbd..3e9845054 100644 --- a/protocol/contracts/libraries/Oracle/LibUsdOracle.sol +++ b/protocol/contracts/libraries/Oracle/LibUsdOracle.sol @@ -31,7 +31,7 @@ library LibUsdOracle { } /** - * @dev Returns the price of a given token in in USD with the option of using a lookback. (Usd:token Price) + * @dev Returns the price of 1 USD in terms of `token` with the option of using a lookback. (Usd:token Price) * `lookback` should be 0 if the instantaneous price is desired. Otherwise, it should be the * TWAP lookback in seconds. * If using a non-zero lookback, it is recommended to use a substantially large `lookback` @@ -68,23 +68,44 @@ library LibUsdOracle { uint256 tokenDecimals, uint256 lookback ) internal view returns (uint256 tokenPrice) { + return getTokenPriceFromExternal(token, tokenDecimals, lookback, false); + } + + /** + * @notice returns the price of 1 Million USD in terms of `token` with the option of using a lookback. + * @dev `LibWell.getRatiosAndBeanIndex` attempts to calculate the target ratios by fetching the usdPrice of each token. + * For tokens with low decimal precision and high prices (ex. WBTC), using the usd:token price would result in a + * large amount of precision loss. For this reason, tokens with less than 8 decimals use the 1 Million USD price instead.. + */ + function getMillionUsdPrice(address token, uint256 lookback) internal view returns (uint256) { + return getTokenPriceFromExternal(token, IERC20Decimals(token).decimals(), lookback, true); + } + + /** + * @notice internal helper function for `getTokenPriceFromExternal`. + * @dev the `isMillion` flag is used in `LibChainlinkOracle.getTokenPrice` to + * return the MILLION_TOKEN2/TOKEN1 price, in cases where the price of TOKEN1 is extremely high (relative to token 2), + * and when the decimals is very low. + */ + function getTokenPriceFromExternal( + address token, + uint256 tokenDecimals, + uint256 lookback, + bool isMillion + ) private view returns (uint256 tokenPrice) { AppStorage storage s = LibAppStorage.diamondStorage(); Implementation memory oracleImpl = s.sys.oracleImplementation[token]; // If the encode type is type 1, use the default chainlink implementation instead. // `target` refers to the address of the price aggergator implmenation if (oracleImpl.encodeType == bytes1(0x01)) { - // if the address in the oracle implementation is 0, use the chainlink registry to lookup address - address chainlinkOraclePriceAddress = oracleImpl.target; - - // decode data timeout to uint256 - uint256 timeout = abi.decode(oracleImpl.data, (uint256)); return LibChainlinkOracle.getTokenPrice( - chainlinkOraclePriceAddress, - timeout, - tokenDecimals, - lookback + oracleImpl.target, // chainlink Aggergator Address + abi.decode(oracleImpl.data, (uint256)), // timeout + tokenDecimals, // token decimals + lookback, + isMillion ); } else if (oracleImpl.encodeType == bytes1(0x02)) { // if the encodeType is type 2, use a uniswap oracle implementation. @@ -119,7 +140,8 @@ library LibUsdOracle { chainlinkOracle.target, abi.decode(chainlinkOracle.data, (uint256)), // timeout tokenDecimals == 0 ? tokenDecimals : chainlinkTokenDecimals, - lookback + lookback, + false ); // if token decimals != 0, Beanstalk is attempting to query the USD/TOKEN price, and @@ -127,14 +149,18 @@ library LibUsdOracle { if (tokenDecimals != 0) { // invert tokenPrice (to get CL_TOKEN/TOKEN). // `tokenPrice` has 6 decimal precision (see {LibUniswapOracle.getTwap}). - tokenPrice = 1e12 / tokenPrice; + // `tokenPrice` is scaled up to 1 million units, if the `isMillion` flag is enabled. + if (isMillion) { + tokenPrice = (1e12 * (10 ** tokenDecimals)) / tokenPrice; + } else { + tokenPrice = (1e6 * (10 ** tokenDecimals)) / tokenPrice; + } // return the USD/TOKEN price. // 1e6 * 1e`n` / 1e`n` = 1e6 return (tokenPrice * chainlinkTokenPrice) / (10 ** chainlinkTokenDecimals); - } else { - // return the TOKEN/USD price. - return (tokenPrice * chainlinkTokenPrice) / UNISWAP_DENOMINATOR; } + + return (tokenPrice * chainlinkTokenPrice) / UNISWAP_DENOMINATOR; } // If the oracle implementation address is not set, use the current contract. diff --git a/protocol/contracts/libraries/Silo/LibWhitelist.sol b/protocol/contracts/libraries/Silo/LibWhitelist.sol index cd12a51a7..dc038657c 100644 --- a/protocol/contracts/libraries/Silo/LibWhitelist.sol +++ b/protocol/contracts/libraries/Silo/LibWhitelist.sol @@ -151,8 +151,6 @@ library LibWhitelist { s.sys.silo.assetSettings[token].stalkIssuedPerBdv = stalkIssuedPerBdv; s.sys.silo.assetSettings[token].milestoneSeason = uint32(s.sys.season.current); s.sys.silo.assetSettings[token].encodeType = encodeType; - s.sys.silo.assetSettings[token].gaugePointImplementation.selector = bytes4(0); - s.sys.silo.assetSettings[token].liquidityWeightImplementation.selector = bytes4(0); s.sys.silo.assetSettings[token].gaugePoints = gaugePoints; s.sys.silo.assetSettings[token].optimalPercentDepositedBdv = optimalPercentDepositedBdv; s.sys.silo.assetSettings[token].gaugePointImplementation = gpImplementation; diff --git a/protocol/contracts/libraries/Well/LibWell.sol b/protocol/contracts/libraries/Well/LibWell.sol index b6b87c454..f37621f90 100644 --- a/protocol/contracts/libraries/Well/LibWell.sol +++ b/protocol/contracts/libraries/Well/LibWell.sol @@ -51,17 +51,40 @@ library LibWell { success = true; ratios = new uint[](tokens.length); beanIndex = type(uint256).max; + bool isMillion; + address bean = s.sys.tokens.bean; + + // fetch the bean index and check whether the ratios precision needs to be increased. for (uint i; i < tokens.length; ++i) { - if (s.sys.tokens.bean == address(tokens[i])) { + if (address(tokens[i]) == bean) { beanIndex = i; - ratios[i] = 1e6; + } else if (IERC20Decimals(address(tokens[i])).decimals() < 8) { + // if the nonBean token in the well has a low decimal precision, + // set `isMillion` such that the ratio is set to be on a million basis. + isMillion = true; + } + } + + // get the target ratios. + for (uint i; i < tokens.length; ++i) { + if (address(tokens[i]) == bean) { + if (isMillion) { + ratios[i] = 1e12; + } else { + ratios[i] = 1e6; + } } else { - ratios[i] = LibUsdOracle.getUsdPrice(address(tokens[i]), lookback); + if (isMillion) { + ratios[i] = LibUsdOracle.getMillionUsdPrice(address(tokens[i]), lookback); + } else { + ratios[i] = LibUsdOracle.getUsdPrice(address(tokens[i]), lookback); + } if (ratios[i] == 0) { success = false; } } } + require(beanIndex != type(uint256).max, "Bean not in Well."); } @@ -168,7 +191,9 @@ library LibWell { /** * @dev Sets the price in {AppStorage.usdTokenPrice} given a set of ratios. - * It assumes that the ratios correspond to the Constant Product Well indexes. + * Assumes + * 1) Ratios correspond to the Constant Product Well indexes. + * 2) the Well is a 2 token Well. */ function setUsdTokenPriceForWell(address well, uint256[] memory ratios) internal { AppStorage storage s = LibAppStorage.diamondStorage(); @@ -180,7 +205,12 @@ library LibWell { s.sys.usdTokenPrice[well] = 0; } else { (, uint256 j) = getNonBeanTokenAndIndexFromWell(well); - s.sys.usdTokenPrice[well] = ratios[j]; + uint256 i = j == 0 ? 1 : 0; + // usdTokenPrice is scaled down to USD/TOKEN, in the cases where + // Beanstalk calculated the MILLION_USD/TOKEN price instead of USD/TOKEN price. + // Beanstalk accepts the loss of precision here, as `usdTokenPrice[well]` is used for + // calculating the liquidity and excessive price. + s.sys.usdTokenPrice[well] = (ratios[j] * 1e6) / ratios[i]; } } diff --git a/protocol/contracts/mocks/mockFacets/MockSeasonGettersFacet.sol b/protocol/contracts/mocks/mockFacets/MockSeasonGettersFacet.sol new file mode 100644 index 000000000..5c7c393f2 --- /dev/null +++ b/protocol/contracts/mocks/mockFacets/MockSeasonGettersFacet.sol @@ -0,0 +1,28 @@ +/* + SPDX-License-Identifier: MIT +*/ + +pragma solidity ^0.8.20; + +import {SeasonGettersFacet} from "../../beanstalk/sun/SeasonFacet/SeasonGettersFacet.sol"; +import {LibDeltaB} from "../../libraries/Oracle/LibDeltaB.sol"; +import {LibAppStorage, AppStorage} from "../../libraries/LibAppStorage.sol"; +import "forge-std/console.sol"; + +/** + * @author pizzaman1337 + * @title Mock Season Getters Facet + **/ +contract MockSeasonGettersFacet is SeasonGettersFacet { + // this mock removes the isWell check, so that a well's deltaB can be checked without requiring whitelisting + function poolCurrentDeltaBMock(address pool) public view returns (int256 deltaB) { + console.log("poolCurrentDeltaB"); + (deltaB) = LibDeltaB.currentDeltaB(pool); + return deltaB; + } + + function mockInitState() public { + AppStorage storage s = LibAppStorage.diamondStorage(); + s.sys.tokens.bean = 0xBEA0000029AD1c77D3d5D23Ba2D8893dB9d1Efab; + } +} diff --git a/protocol/scripts/genDiamondCut.js b/protocol/scripts/genDiamondCut.js new file mode 100644 index 000000000..62265b0e8 --- /dev/null +++ b/protocol/scripts/genDiamondCut.js @@ -0,0 +1,110 @@ +const ethers = require("ethers"); + +async function generateDiamondCut(existingFacets, newFacets) { + const cuts = []; + + // console.log("Generating diamond cut..."); + + // Helper function to convert selector strings to bytes4 + const selectorToBytes4 = (selector) => { + return selector.slice(0, 10); // Assuming selectors are already in '0x' format + }; + + // Process existing facets + const existingSelectors = new Map(); + existingFacets.forEach(facet => { + facet.selectors.forEach(selector => { + existingSelectors.set(selectorToBytes4(selector), facet.facetAddress); + }); + }); + + // console.log(`Found ${existingSelectors.size} existing selectors`); + // console.log("existing selectors: ", Array.from(existingSelectors.keys())); + + // Process new facets + for (const newFacet of newFacets) { + const addSelectors = []; + const replaceSelectors = []; + + newFacet.selectors.forEach(selector => { + const bytes4Selector = selectorToBytes4(selector); + if (existingSelectors.has(bytes4Selector)) { + replaceSelectors.push(bytes4Selector); + } else { + addSelectors.push(bytes4Selector); + } + existingSelectors.delete(bytes4Selector); + }); + + if (addSelectors.length > 0) { + cuts.push({ + facetAddress: newFacet.facetAddress, + action: 0, // Add + functionSelectors: addSelectors + }); + } + + if (replaceSelectors.length > 0) { + cuts.push({ + facetAddress: newFacet.facetAddress, + action: 1, // Replace + functionSelectors: replaceSelectors + }); + } + } + + // console.log(`Found ${existingSelectors.size} removed selectors`); + + // Handle removed selectors + if (existingSelectors.size > 0) { + cuts.push({ + facetAddress: '0x0000000000000000000000000000000000000000', + action: 2, // Remove + functionSelectors: Array.from(existingSelectors.keys()) + }); + } + + // console.log(`Generated ${cuts.length} cuts`); + // console.log("final cuts: ", cuts); + + return cuts; +} + +async function processDiamondCut(existingFacetsJson, newFacetsJson) { + try { + const existingFacets = JSON.parse(existingFacetsJson); + const newFacets = JSON.parse(newFacetsJson); + const diamondCut = await generateDiamondCut(existingFacets, newFacets); + + // Compact encoding + let encoded = ethers.utils.hexlify(ethers.utils.pack(['uint256'], [diamondCut.length])); + + for (const cut of diamondCut) { + encoded += ethers.utils.hexlify(cut.facetAddress).slice(2); + encoded += ethers.utils.hexZeroPad(ethers.utils.hexlify(cut.action), 1).slice(2); + encoded += ethers.utils.hexZeroPad(ethers.utils.hexlify(cut.functionSelectors.length), 2).slice(2); + for (const selector of cut.functionSelectors) { + encoded += selector.slice(2); + } + } + + process.stdout.write(encoded); + } catch (error) { + console.error(error); + process.exit(1); + } +} + +// Get command line arguments +const args = process.argv.slice(2); +if (args.length !== 2) { + console.error("Usage: node genDiamondCut.js "); + process.exit(1); +} + +processDiamondCut(args[0], args[1]) + .then(() => process.exit(0)) + .catch((error) => { + console.error(error); + process.exit(1); + }); \ No newline at end of file diff --git a/protocol/scripts/genSelectors.js b/protocol/scripts/genSelectors.js index 2caf11d99..1c492817e 100644 --- a/protocol/scripts/genSelectors.js +++ b/protocol/scripts/genSelectors.js @@ -1,31 +1,91 @@ const ethers = require("ethers"); const path = require("path/posix"); +const fs = require("fs"); const args = process.argv.slice(2); -async function printSelectors(contractName, artifactFolderPath = "../out") { - const contractFilePath = path.join(artifactFolderPath,`${contractName}.sol`,`${contractName}.json`); - const contractArtifact = require(contractFilePath); +function log(message) { + // turn logging on or off by commenting out this following line + // fs.appendFileSync('genSelectors.log', message + '\n'); +} + +async function printSelectors(contractName, artifactFolderPath = "out") { + try { + const contractFilePath = path.join( + process.cwd(), + artifactFolderPath, + `${contractName}.sol`, + `${contractName}.json` + ); + log(`Looking for contract at: ${contractFilePath}`); + + if (!fs.existsSync(contractFilePath)) { + log(`Contract file not found: ${contractFilePath}`); + return []; + } + + const contractArtifact = JSON.parse(fs.readFileSync(contractFilePath, "utf8")); + log(`Contract artifact loaded for ${contractName}`); + + if (!contractArtifact.methodIdentifiers) { + log(`No method identifiers found for ${contractName}`); + return []; + } + + const selectors = Object.values(contractArtifact.methodIdentifiers); + log(`Found ${selectors.length} selectors for ${contractName}`); - // Use map to prepend '0x' to each element, return selectors. - return Object.values(contractArtifact.methodIdentifiers).map(element => '0x' + element); + return selectors; + } catch (error) { + log(`Error in printSelectors for ${contractName}: ${error.message}`); + return []; + } } -async function processContracts(contractNames, defaultArtifactFolderPath = "../out") { - const promises = contractNames.map(contractName => - printSelectors(contractName, defaultArtifactFolderPath) - ); +async function processContracts(contractNames, defaultArtifactFolderPath = "./out/") { + try { + log(`Current working directory: ${process.cwd()}`); + + log(`Processing contracts: ${contractNames.join(", ")}`); + + const promises = contractNames.map((contractName) => + printSelectors(contractName, defaultArtifactFolderPath) + ); + + const results = await Promise.all(promises); + log(`All selectors retrieved. Number of contracts processed: ${results.length}`); + + // Compact encoding + let encoded = ethers.utils.hexZeroPad(ethers.BigNumber.from(results.length).toHexString(), 32); + log(`Encoded number of contracts: ${encoded}`); + + for (const selectors of results) { + encoded += ethers.utils + .hexZeroPad(ethers.BigNumber.from(selectors.length).toHexString(), 2) + .slice(2); + log(`Encoded number of selectors for a contract: ${encoded.slice(-4)}`); + for (const selector of selectors) { + encoded += selector; + log(`Encoded selector: ${selector}`); + } + } - // Wait for all printSelectors calls to complete - const results = await Promise.all(promises); - const coded = ethers.utils.defaultAbiCoder.encode(["bytes4[][]"], [results]); - process.stdout.write(coded) + log(`Final encoded data: ${encoded}`); + return encoded; + } catch (error) { + log(`Error in processContracts: ${error.message}`); + return "0x"; + } } -// We recommend this pattern to be able to use async/await everywhere processContracts(args) -.then(() => process.exit(0)) -.catch((error) => { - console.error(error); - process.exit(1); -}); \ No newline at end of file + .then((encoded) => { + log(`Writing to stdout: ${encoded}`); + process.stdout.write(encoded); + process.exit(0); + }) + .catch((error) => { + log(`Fatal error: ${error.message}`); + console.error(error); + process.exit(1); + }); diff --git a/protocol/test/foundry/Migration/L1Reciever.t.sol b/protocol/test/foundry/Migration/L1Reciever.t.sol index 9b5907ce6..728faffd8 100644 --- a/protocol/test/foundry/Migration/L1Reciever.t.sol +++ b/protocol/test/foundry/Migration/L1Reciever.t.sol @@ -224,13 +224,12 @@ contract L1RecieverFacetTest is Order, TestHelper { } function test_L2MigrateInvalidPodOrder() public { - bs.setRecieverForL1Migration(OWNER, RECIEVER); - ( address owner, L1RecieverFacet.L1PodOrder[] memory podOrders, bytes32[] memory proof ) = getMockPodOrder(); + bs.setRecieverForL1Migration(owner, RECIEVER); // update pod orderer podOrders[0].podOrder.orderer = RECIEVER; @@ -244,6 +243,7 @@ contract L1RecieverFacetTest is Order, TestHelper { // test helpers function getMockDepositData() internal + pure returns (address, uint256[] memory, uint256[] memory, uint256[] memory, bytes32[] memory) { address account = address(0x000000009d3a9e5C7c620514e1f36905C4Eb91e1); diff --git a/protocol/test/foundry/silo/Oracle.t.sol b/protocol/test/foundry/silo/Oracle.t.sol index e6250e350..a402403f1 100644 --- a/protocol/test/foundry/silo/Oracle.t.sol +++ b/protocol/test/foundry/silo/Oracle.t.sol @@ -8,6 +8,9 @@ import {MockChainlinkAggregator} from "contracts/mocks/chainlink/MockChainlinkAg import {MockToken} from "contracts/mocks/MockToken.sol"; import {LSDChainlinkOracle} from "contracts/ecosystem/oracles/LSDChainlinkOracle.sol"; import {LibChainlinkOracle} from "contracts/libraries/Oracle/LibChainlinkOracle.sol"; +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import {IWell} from "contracts/interfaces/basin/IWell.sol"; +import "forge-std/console.sol"; /** * @notice Tests the functionality of the Oracles. @@ -63,7 +66,7 @@ contract OracleTest is TestHelper { WBTC, 0 ); - assertEq(usdTokenPriceFromExternal, 0.00002e6, "usdTokenPriceFromExternal"); + assertEq(usdTokenPriceFromExternal, 0.00002e8, "usdTokenPriceFromExternal"); // e8 because wbtc has 8 decimals } /** @@ -260,6 +263,71 @@ contract OracleTest is TestHelper { assertEq(priceWBTC, 0.00002e8); // adjusted to 8 decimals } + function testForkMainnetWBTCOracle() public { + forkMainnetAndUpgradeAllFacets(20641000); + + setupUniswapWBTCOracleImplementation(); + + uint256 priceWBTCmillion = OracleFacet(BEANSTALK).getMillionUsdPrice(WBTC, 0); + // 1e(8+6)/1684341342 = 59370.3885943091 + assertEq(priceWBTCmillion, 1684454192); // $1,000,000 buys 1684341342 at BTC price of 6148186669379 per USDC and USDC 99993272. + + // 1e8/1684 = 59382.4228028504 + uint256 priceWBTC = OracleFacet(BEANSTALK).getUsdTokenPrice(WBTC); + assertEq(priceWBTC, 1684); // $1 buys 1683 satoshi at BTC price of 6148186669379 per USDC and USDC 99993272. + } + + function testForkMainnetAAVEOracle() public { + forkMainnetAndUpgradeAllFacets(20666000); + + setupUniswapAaveOracleImplementation(); + + uint256 priceAAVE = OracleFacet(BEANSTALK).getUsdTokenPrice(AAVE); + assertEq(priceAAVE, 7478751606516229); + // chainlink price: 2541090000 (2541 usd per weth at 6 decimals) + // uniswap price: 52620 (0.052620 WETH per AAVE at 6 decimals) + // these multiplied together: 133712155800000 (12 decimal precision) + // but inverse is needed, so 1e12/133712155800000 = 0.007478751607 + // and 0.007478751607 at 6 decimal precision is 7479 + } + + function testForkMainnetWSTETHOracle() public { + forkMainnetAndUpgradeAllFacets(20666000); + + setupUniswapWstethOracleImplementation(); + + uint256 priceWSTETH = OracleFacet(BEANSTALK).getUsdTokenPrice(WSTETH); + assertEq(priceWSTETH, 334243752683826); + } + + function testForkMainnetWBTCDeltaB() public { + forkMainnetAndUpgradeAllFacets(20666000); + + setupUniswapWBTCOracleImplementation(); + + console.log("deploy well"); + + // deploy a wbtc:bean well + deployWBTCWellOnFork(true, true); + console.log("deployed well"); + + address WBTC_WHALE = 0x5Ee5bf7ae06D1Be5997A1A72006FE6C607eC6DE8; + // deal didn't seem to work with wbtc, so instead, transfer from a wbtc whale + + vm.prank(WBTC_WHALE); + IERC20(WBTC).transfer(BEAN_WBTC_WELL, 2e8); // 2 wbtc + deal(address(BEAN), BEAN_WBTC_WELL, 118063754426, true); // approx 2 btc worth of beans + IWell(BEAN_WBTC_WELL).sync(users[0], 0); + + // mock init state so that the bean token is defined + IMockFBeanstalk(BEANSTALK).mockInitState(); + + int256 deltaB = IMockFBeanstalk(BEANSTALK).poolCurrentDeltaBMock(BEAN_WBTC_WELL); + assertEq(deltaB, 0); + } + + //////////// Helper Functions //////////// + function setupUniswapWBTCOracleImplementation() public { vm.prank(BEANSTALK); bs.updateOracleImplementationForToken( @@ -268,7 +336,7 @@ contract OracleTest is TestHelper { WBTC_USDC_03_POOL, bytes4(0), bytes1(0x02), - abi.encode(LibChainlinkOracle.FOUR_HOUR_TIMEOUT) + abi.encode(LibChainlinkOracle.FOUR_DAY_TIMEOUT) ) ); @@ -284,4 +352,57 @@ contract OracleTest is TestHelper { ) ); } + + // AAVE:WETH is the highest volume non-memcoin/non-stablecoin pair on uniswap as of the time of writing + function setupUniswapAaveOracleImplementation() internal { + vm.prank(BEANSTALK); + bs.updateOracleImplementationForToken( + AAVE, + IMockFBeanstalk.Implementation( + AAVE_ETH_03_POOL, + bytes4(0), + bytes1(0x02), + abi.encode(LibChainlinkOracle.FOUR_DAY_TIMEOUT) + ) + ); + + // also uniswap relies on having a chainlink oracle for the token that's trading against the uniswap target token + // in the case of AAVE/ETH, eth is the token that needs to be looked up against chainlink + vm.prank(BEANSTALK); + bs.updateOracleImplementationForToken( + WETH, + IMockFBeanstalk.Implementation( + ETH_USD_CHAINLINK_PRICE_AGGREGATOR, // note this is using eth instead of weth + bytes4(0), + bytes1(0x01), + abi.encode(LibChainlinkOracle.FOUR_DAY_TIMEOUT) + ) + ); + } + + function setupUniswapWstethOracleImplementation() internal { + vm.prank(BEANSTALK); + bs.updateOracleImplementationForToken( + WSTETH, + IMockFBeanstalk.Implementation( + WSTETH_ETH_001_POOL, + bytes4(0), + bytes1(0x02), + abi.encode(LibChainlinkOracle.FOUR_DAY_TIMEOUT) + ) + ); + + // also uniswap relies on having a chainlink oracle for the token that's trading against the uniswap target token + // in the case of AAVE/ETH, eth is the token that needs to be looked up against chainlink + vm.prank(BEANSTALK); + bs.updateOracleImplementationForToken( + WETH, + IMockFBeanstalk.Implementation( + ETH_USD_CHAINLINK_PRICE_AGGREGATOR, // note this is using eth instead of weth + bytes4(0), + bytes1(0x01), + abi.encode(LibChainlinkOracle.FOUR_DAY_TIMEOUT) + ) + ); + } } diff --git a/protocol/test/foundry/utils/BasinDeployer.sol b/protocol/test/foundry/utils/BasinDeployer.sol index e72bb68ae..dc4208956 100644 --- a/protocol/test/foundry/utils/BasinDeployer.sol +++ b/protocol/test/foundry/utils/BasinDeployer.sol @@ -45,6 +45,7 @@ contract BasinDeployer is Utils { // addresses were randomly generated and are not on-chain. address constant BEAN_USDC_WELL = address(0x4444F7394455A8d1af37E8BEa52F2FCf6D39f158); address constant BEAN_USDT_WELL = address(0x55554AF7c7CEe28994c7484C364768620C726D68); + address constant BEAN_WBTC_WELL = address(0x7777F3d631f856b4738Da79E0f4c10EE25C75B31); string constant BEAN_WETH_WELL_NAME = "BEAN:WETH Constant Product 2 Well"; string constant BEAN_WETH_WELL_SYMBOL = "BEANWETHCP2w"; @@ -206,6 +207,28 @@ contract BasinDeployer is Utils { vm.label(BEAN_USDT_WELL, "BEAN/USDT Well"); } + function deployWBTCWellOnFork(bool mock, bool verbose) internal { + console.log("deploying wbtc well"); + + console.log("wellImplementations[0]:", wellImplementations[0]); + + // deploy Bean WBTC well: + // wells.push(deployBeanCp2Well([BEAN_WBTC_WELL, WBTC], _pump)); + + deployWellAtAddressNoData( + BEAN_WBTC_WELL, + BEAN, + WBTC, + wellFunctions[0], + pumps[0], // multi flow pump + wellImplementations[0] + ); + + console.log("deployed wbtc well"); + if (verbose) console.log("Bean WBTC well deployed at:", wells[0]); + vm.label(BEAN_WBTC_WELL, "BEAN/WBTC Well"); + } + /** * @notice deploys a well with a * Constant product 2 well function and pump. diff --git a/protocol/test/foundry/utils/BeanstalkDeployer.sol b/protocol/test/foundry/utils/BeanstalkDeployer.sol index 3f3c3d7bf..3a8352dd8 100644 --- a/protocol/test/foundry/utils/BeanstalkDeployer.sol +++ b/protocol/test/foundry/utils/BeanstalkDeployer.sol @@ -20,7 +20,15 @@ import {MockConvertFacet, ConvertFacet} from "contracts/mocks/mockFacets/MockCon import {MockSeasonFacet, SeasonFacet} from "contracts/mocks/mockFacets/MockSeasonFacet.sol"; import {MockSiloFacet, SiloFacet} from "contracts/mocks/mockFacets/MockSiloFacet.sol"; import {MockPipelineConvertFacet, PipelineConvertFacet} from "contracts/mocks/mockFacets/MockPipelineConvertFacet.sol"; +import {MockFertilizerFacet, FertilizerFacet} from "contracts/mocks/mockFacets/MockFertilizerFacet.sol"; +import {MockWhitelistFacet, WhitelistFacet} from "contracts/mocks/mockFacets/MockWhitelistFacet.sol"; +import {MockFieldFacet, FieldFacet} from "contracts/mocks/mockFacets/MockFieldFacet.sol"; import {SeasonGettersFacet} from "contracts/beanstalk/sun/SeasonFacet/SeasonGettersFacet.sol"; +import {DiamondCutFacet} from "contracts/beanstalk/diamond/DiamondCutFacet.sol"; +import {IDiamondLoupe} from "contracts/interfaces/IDiamondLoupe.sol"; + +import {IMockFBeanstalk} from "contracts/interfaces/IMockFBeanstalk.sol"; +import "forge-std/console.sol"; /** * @title TestHelper @@ -41,7 +49,6 @@ contract BeanstalkDeployer is Utils { "SiloGettersFacet", "ConvertGettersFacet", "MetadataFacet", - "SeasonGettersFacet", "DepotFacet", "MarketplaceFacet", "ClaimFacet", @@ -58,9 +65,13 @@ contract BeanstalkDeployer is Utils { "UnripeFacet", // MockUnripeFacet "ConvertFacet", // MockConvertFacet "SeasonFacet", // MockSeasonFacet - "PipelineConvertFacet" // MockPipelineConvertFacet + "PipelineConvertFacet", // MockPipelineConvertFacet + "SeasonGettersFacet" // MockSeasonGettersFacet ]; - address[] facetAddresses; + address[] initialDeployFacetAddresses; + string[] initialDeploFacetNames; + address[] upgradeFacetAddresses; + string[] upgradeFacetNames; IDiamondCut.FacetCutAction[] cutActions; @@ -75,6 +86,39 @@ contract BeanstalkDeployer is Utils { vm.label(BEANSTALK, "Beanstalk"); // Create cuts. + setupFacetAddresses(mock, true, false); + + IDiamondCut.FacetCut[] memory cut = _multiCut( + initialDeploFacetNames, + initialDeployFacetAddresses, + cutActions + ); + d = deployDiamondAtAddress(deployer, BEANSTALK); + + // if mocking, set the diamond address to + // the canonical beanstalk address. + address initDiamondAddress; + if (mock) { + initDiamondAddress = address(new MockInitDiamond()); + } else { + initDiamondAddress = address(new InitDiamond()); + } + + vm.prank(deployer); + IDiamondCut(address(d)).diamondCut( + cut, + initDiamondAddress, + abi.encodeWithSignature("init()") + ); + + if (verbose) console.log("Diamond deployed at: ", address(d)); + } + + function setupFacetAddresses(bool mock, bool attack, bool includeUpgradeFacetsOnly) internal { + address[] memory facetAddresses = new address[](100); + string[] memory facetNames = new string[](100); + + uint256 facetCounter; // Facets that require external libraries need to be deployed by // `address(new Facet())` @@ -83,20 +127,38 @@ contract BeanstalkDeployer is Utils { // for facets with external libraries, deploy the facet, // rather than deploying using the bytecode. string memory facetName = facets[i]; + + if ( + includeUpgradeFacetsOnly && + (keccak256(abi.encodePacked(facetName)) == + keccak256(abi.encodePacked("OwnershipFacet")) || + keccak256(abi.encodePacked(facetName)) == + keccak256(abi.encodePacked("PauseFacet")) || + keccak256(abi.encodePacked(facetName)) == + keccak256(abi.encodePacked("DiamondCutFacet")) || + keccak256(abi.encodePacked(facetName)) == + keccak256(abi.encodePacked("DiamondCutFacet"))) + ) { + continue; + } + if (keccak256(abi.encode(facetName)) == keccak256(abi.encode("SeasonGettersFacet"))) { - facetAddresses.push(address(new SeasonGettersFacet())); + facetAddresses[facetCounter++] = address(new SeasonGettersFacet()); } else { - facetAddresses.push(address(deployCode(facetName))); + facetAddresses[facetCounter++] = address(deployCode(facetName)); } cutActions.push(IDiamondCut.FacetCutAction.Add); + // facetNames.push(facetName); + facetNames[facetCounter - 1] = facetName; } // Deploy mock only facets. - if (mock) { - facetAddresses.push(address(new MockAttackFacet())); - facets.push("MockAttackFacet"); + if (mock && attack) { + // facetAddresses.push(address(new MockAttackFacet())); + facetAddresses[facetCounter++] = address(new MockAttackFacet()); cutActions.push(IDiamondCut.FacetCutAction.Add); + facetNames[facetCounter - 1] = "MockAttackFacet"; } for (uint i; i < mockFacets.length; i++) { @@ -126,6 +188,24 @@ contract BeanstalkDeployer is Utils { } else { facetAddress = address(new SeasonFacet()); } + } else if (hashedName == keccak256(abi.encode("FertilizerFacet"))) { + if (mock) { + facetAddress = address(new MockFertilizerFacet()); + } else { + facetAddress = address(new FertilizerFacet()); + } + } else if (hashedName == keccak256(abi.encode("WhitelistFacet"))) { + if (mock) { + facetAddress = address(new MockWhitelistFacet()); + } else { + facetAddress = address(new WhitelistFacet()); + } + } else if (hashedName == keccak256(abi.encode("FieldFacet"))) { + if (mock) { + facetAddress = address(new MockFieldFacet()); + } else { + facetAddress = address(new FieldFacet()); + } } else if (hashedName == keccak256(abi.encode("SiloFacet"))) { if (mock) { facetAddress = address(new MockSiloFacet()); @@ -142,33 +222,25 @@ contract BeanstalkDeployer is Utils { facetAddress = address(deployCode(facet)); } - facetAddresses.push(facetAddress); - - // append the facet name to the facets array. - facets.push(facet); + facetAddresses[facetCounter++] = facetAddress; cutActions.push(IDiamondCut.FacetCutAction.Add); + facetNames[facetCounter - 1] = facet; } - IDiamondCut.FacetCut[] memory cut = _multiCut(facets, facetAddresses, cutActions); - d = deployDiamondAtAddress(deployer, BEANSTALK); - // if mocking, set the diamond address to - // the canonical beanstalk address. - address initDiamondAddress; - if (mock) { - initDiamondAddress = address(new MockInitDiamond()); - } else { - initDiamondAddress = address(new InitDiamond()); + // update array lengths + assembly { + mstore(facetAddresses, facetCounter) + mstore(facetNames, facetCounter) } - vm.prank(deployer); - IDiamondCut(address(d)).diamondCut( - cut, - initDiamondAddress, - abi.encodeWithSignature("init()") - ); - - if (verbose) console.log("Diamond deployed at: ", address(d)); + if (includeUpgradeFacetsOnly) { + upgradeFacetAddresses = facetAddresses; + upgradeFacetNames = facetNames; + } else { + initialDeployFacetAddresses = facetAddresses; + initialDeploFacetNames = facetNames; + } } /** @@ -210,6 +282,87 @@ contract BeanstalkDeployer is Utils { vm.stopPrank(); } + // useful for debugging which facets are erroring by adding logs to LibDiamond and deploying after forking + /*function upgradeDiamondFacet() internal { + string[] memory _facetNames = new string[](1); + _facetNames[0] = "DiamondCutFacet"; + address[] memory newFacetAddresses = new address[](1); + newFacetAddresses[0] = address(new DiamondCutFacet()); + + IDiamondCut.FacetCutAction[] memory facetCutActions = new IDiamondCut.FacetCutAction[](1); + facetCutActions[0] = IDiamondCut.FacetCutAction.Replace; + + // upgrade just the diamond cut facet + upgradeWithNewFacets( + BEANSTALK, // upgrading beanstalk. + IMockFBeanstalk(BEANSTALK).owner(), // fetch beanstalk owner. + _facetNames, + newFacetAddresses, + facetCutActions, + address(new EmptyInitContract()), // deploy the ReseedL2Migration. + abi.encodeWithSignature("init()"), // call init. + new bytes4[](0) + ); + }*/ + + /** + * @notice Forks mainnet at a given block, + */ + function forkMainnetAndUpgradeAllFacets(uint256 blockNumber) internal { + vm.createSelectFork(vm.envString("FORKING_RPC"), blockNumber); + + setupFacetAddresses(true, false, true); + + // upgradeDiamondFacet(); + + // the idea is to add/upgrade all the facets/mock facets that are in the constants at the top of this file + // get the list of all current selectors + IDiamondLoupe.Facet[] memory currentFacets = IDiamondLoupe(BEANSTALK).facets(); + bytes4[] memory currentSelectors = new bytes4[](1000); + uint256 selectorsCounter = 0; + for (uint256 i = 0; i < currentFacets.length; i++) { + // loop through all selectors in the facet + bytes4[] memory selectors = IDiamondLoupe(BEANSTALK).facetFunctionSelectors( + currentFacets[i].facetAddress + ); + for (uint256 j = 0; j < selectors.length; j++) { + // add the selector to the currentSelectors array + currentSelectors[selectorsCounter++] = selectors[j]; + } + } + assembly { + mstore(currentSelectors, selectorsCounter) + } + + // generated list of all the new facets + IDiamondLoupe.Facet[] memory newFacets = new IDiamondLoupe.Facet[]( + upgradeFacetAddresses.length + ); + + uint256 facetAddressesLength = upgradeFacetAddresses.length; + + bytes4[][] memory functionSelectorsArray = _generateMultiSelectors(upgradeFacetNames); + for (uint256 i = 0; i < upgradeFacetNames.length; i++) { + IDiamondLoupe.Facet memory facet = IDiamondLoupe.Facet( + upgradeFacetAddresses[i], + functionSelectorsArray[i] + ); + newFacets[i] = facet; + } + + assembly { + mstore(newFacets, facetAddressesLength) + } + + // generate the diamond cut required to upgrade all facets + IDiamondCut.FacetCut[] memory cut = generateDiamondCut(currentFacets, newFacets); + + vm.startPrank(IMockFBeanstalk(BEANSTALK).owner()); + // perform the diamond cut (upgrades Beanstalk) + IDiamondCut(BEANSTALK).diamondCut(cut, address(0), new bytes(0)); + vm.stopPrank(); + } + //////////////////////// Deploy ///////////////////////// /** @@ -291,7 +444,271 @@ contract BeanstalkDeployer is Utils { for (uint i = 0; i < _facetNames.length; i++) { cmd[i + 2] = _facetNames[i]; } + // be aware of cases where the response may be very large, which can break the EVM bytes memory res = vm.ffi(cmd); - selectorsArray = abi.decode(res, (bytes4[][])); + + if (res.length > 0) { + selectorsArray = _decodeCompactSelectors(res); + } else { + selectorsArray = new bytes4[][](0); + } + } + + function _decodeCompactSelectors(bytes memory data) internal pure returns (bytes4[][] memory) { + uint256 pointer = 0; + uint256 numContracts = uint256(bytes32(slice(data, pointer, 32))); + pointer += 32; + + bytes4[][] memory selectorsArray = new bytes4[][](numContracts); + + for (uint256 i = 0; i < numContracts; i++) { + uint16 numSelectors = uint16(bytes2(slice(data, pointer, 2))); + pointer += 2; + + bytes4[] memory selectors = new bytes4[](numSelectors); + for (uint256 j = 0; j < numSelectors; j++) { + selectors[j] = bytes4(slice(data, pointer, 4)); + pointer += 4; + } + + selectorsArray[i] = selectors; + } + + return selectorsArray; + } + + function generateDiamondCut( + IDiamondLoupe.Facet[] memory currentFacets, + IDiamondLoupe.Facet[] memory newFacets + ) internal pure returns (IDiamondCut.FacetCut[] memory) { + // Use a dynamic array for cuts + IDiamondCut.FacetCut[] memory cuts = new IDiamondCut.FacetCut[](0); + + // Create arrays to store all selectors and their corresponding new facet addresses + bytes4[] memory allSelectors = new bytes4[](0); + address[] memory allNewFacetAddresses = new address[](0); + + // Populate the arrays with data from newFacets + for (uint256 i = 0; i < newFacets.length; i++) { + for (uint256 j = 0; j < newFacets[i].functionSelectors.length; j++) { + allSelectors = appendToBytes4Array(allSelectors, newFacets[i].functionSelectors[j]); + allNewFacetAddresses = appendToAddressArray( + allNewFacetAddresses, + newFacets[i].facetAddress + ); + } + } + + // Process removals and replacements + for (uint256 i = 0; i < currentFacets.length; i++) { + bytes4[] memory selectorsToRemove = new bytes4[]( + currentFacets[i].functionSelectors.length + ); + bytes4[] memory selectorsToReplace = new bytes4[]( + currentFacets[i].functionSelectors.length + ); + uint256 removeCount = 0; + uint256 replaceCount = 0; + + for (uint256 j = 0; j < currentFacets[i].functionSelectors.length; j++) { + bytes4 selector = currentFacets[i].functionSelectors[j]; + (bool found, address newFacetAddress) = findNewFacetAddress( + selector, + allSelectors, + allNewFacetAddresses + ); + + if (!found) { + selectorsToRemove[removeCount] = selector; + removeCount++; + } else if (newFacetAddress != currentFacets[i].facetAddress) { + selectorsToReplace[replaceCount] = selector; + replaceCount++; + } + } + + if (removeCount > 0) { + bytes4[] memory finalSelectorsToRemove = new bytes4[](removeCount); + for (uint256 j = 0; j < removeCount; j++) { + finalSelectorsToRemove[j] = selectorsToRemove[j]; + } + cuts = appendToCuts( + cuts, + IDiamondCut.FacetCut( + address(0), + IDiamondCut.FacetCutAction.Remove, + finalSelectorsToRemove + ) + ); + } + + if (replaceCount > 0) { + bytes4[] memory finalSelectorsToReplace = new bytes4[](replaceCount); + for (uint256 j = 0; j < replaceCount; j++) { + finalSelectorsToReplace[j] = selectorsToReplace[j]; + } + (, address newFacetAddress) = findNewFacetAddress( + finalSelectorsToReplace[0], + allSelectors, + allNewFacetAddresses + ); + cuts = appendToCuts( + cuts, + IDiamondCut.FacetCut( + newFacetAddress, + IDiamondCut.FacetCutAction.Replace, + finalSelectorsToReplace + ) + ); + } + } + + // Process additions + for (uint256 i = 0; i < newFacets.length; i++) { + bytes4[] memory selectorsToAdd = new bytes4[](newFacets[i].functionSelectors.length); + uint256 addCount = 0; + + for (uint256 j = 0; j < newFacets[i].functionSelectors.length; j++) { + bytes4 selector = newFacets[i].functionSelectors[j]; + bool isNewSelector = true; + for (uint256 k = 0; k < currentFacets.length; k++) { + if (contains(currentFacets[k].functionSelectors, selector)) { + isNewSelector = false; + break; + } + } + if (isNewSelector) { + selectorsToAdd[addCount] = selector; + addCount++; + } + } + + if (addCount > 0) { + bytes4[] memory finalSelectorsToAdd = new bytes4[](addCount); + for (uint256 j = 0; j < addCount; j++) { + finalSelectorsToAdd[j] = selectorsToAdd[j]; + } + cuts = appendToCuts( + cuts, + IDiamondCut.FacetCut( + newFacets[i].facetAddress, + IDiamondCut.FacetCutAction.Add, + finalSelectorsToAdd + ) + ); + } + } + + return cuts; + } + + function findNewFacetAddress( + bytes4 selector, + bytes4[] memory allSelectors, + address[] memory allNewFacetAddresses + ) internal pure returns (bool, address) { + for (uint256 i = 0; i < allSelectors.length; i++) { + if (allSelectors[i] == selector) { + return (true, allNewFacetAddresses[i]); + } + } + return (false, address(0)); + } + + // these append functions are not ideal in terms for gas/performance, but they are convenient for testing + function appendToBytes4Array( + bytes4[] memory array, + bytes4 element + ) internal pure returns (bytes4[] memory) { + bytes4[] memory newArray = new bytes4[](array.length + 1); + for (uint256 i = 0; i < array.length; i++) { + newArray[i] = array[i]; + } + newArray[array.length] = element; + return newArray; + } + + function appendToAddressArray( + address[] memory array, + address element + ) internal pure returns (address[] memory) { + address[] memory newArray = new address[](array.length + 1); + for (uint256 i = 0; i < array.length; i++) { + newArray[i] = array[i]; + } + newArray[array.length] = element; + return newArray; + } + + function appendToCuts( + IDiamondCut.FacetCut[] memory cuts, + IDiamondCut.FacetCut memory newCut + ) internal pure returns (IDiamondCut.FacetCut[] memory) { + IDiamondCut.FacetCut[] memory newCuts = new IDiamondCut.FacetCut[](cuts.length + 1); + for (uint i = 0; i < cuts.length; i++) { + newCuts[i] = cuts[i]; + } + newCuts[cuts.length] = newCut; + return newCuts; + } + + function contains(bytes4[] memory array, bytes4 value) internal pure returns (bool) { + for (uint256 i = 0; i < array.length; i++) { + if (array[i] == value) { + return true; + } + } + return false; + } + + function _decodeDiamondCut( + bytes memory data + ) internal pure returns (IDiamondCut.FacetCut[] memory) { + uint256 pointer = 0; + uint256 numCuts = uint256(bytes32(slice(data, pointer, 32))); + pointer += 32; + + IDiamondCut.FacetCut[] memory cuts = new IDiamondCut.FacetCut[](numCuts); + + for (uint256 i = 0; i < numCuts; i++) { + address facetAddress = address(bytes20(slice(data, pointer, 20))); + pointer += 20; + + uint8 action = uint8(slice(data, pointer, 1)[0]); + pointer += 1; + + uint16 numSelectors = uint16(bytes2(slice(data, pointer, 2))); + pointer += 2; + + bytes4[] memory selectors = new bytes4[](numSelectors); + for (uint256 j = 0; j < numSelectors; j++) { + selectors[j] = bytes4(slice(data, pointer, 4)); + pointer += 4; + } + + cuts[i] = IDiamondCut.FacetCut( + facetAddress, + IDiamondCut.FacetCutAction(action), + selectors + ); + } + + return cuts; } + + function slice( + bytes memory data, + uint256 start, + uint256 length + ) internal pure returns (bytes memory) { + bytes memory result = new bytes(length); + for (uint256 i = 0; i < length; i++) { + result[i] = data[start + i]; + } + return result; + } +} + +contract EmptyInitContract { + function init() external {} } diff --git a/protocol/test/foundry/utils/OracleDeployer.sol b/protocol/test/foundry/utils/OracleDeployer.sol index 61d2cab39..f52ddf0b6 100644 --- a/protocol/test/foundry/utils/OracleDeployer.sol +++ b/protocol/test/foundry/utils/OracleDeployer.sol @@ -63,7 +63,9 @@ contract OracleDeployer is Utils { ////////// UNISWAP ////////// address constant WBTC_USDC_03_POOL = address(0x99ac8cA7087fA4A2A1FB6357269965A2014ABc35); - address constant WBTC = address(0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599); + address constant AAVE_ETH_03_POOL = address(0x5aB53EE1d50eeF2C1DD3d5402789cd27bB52c1bB); + address constant WSTETH_ETH_001_POOL = address(0x109830a1AAaD605BbF02a9dFA7B0B92EC2FB7dAa); + address constant AAVE = address(0x7Fc66500c84A76Ad7e9c93437bFc5Ac33E2DDaE9); // new uniswap pools should be appended here. address[][] public pools = [ diff --git a/protocol/test/foundry/utils/TestHelper.sol b/protocol/test/foundry/utils/TestHelper.sol index 32a5aab37..6cc718063 100644 --- a/protocol/test/foundry/utils/TestHelper.sol +++ b/protocol/test/foundry/utils/TestHelper.sol @@ -210,16 +210,39 @@ contract TestHelper is ) internal returns (uint256 lpOut) { (address nonBeanToken, ) = bs.getNonBeanTokenAndIndexFromWell(well); - // mint and sync. - MockToken(BEAN).mint(well, beanAmount); - MockToken(nonBeanToken).mint(well, nonBeanTokenAmount); + if (runningOnFork()) { + console.log("dealing tokens on fork"); + deal(address(BEAN), well, beanAmount, true); + deal(address(nonBeanToken), well, nonBeanTokenAmount, true); + } else { + // mint and sync. + MockToken(BEAN).mint(well, beanAmount); + MockToken(nonBeanToken).mint(well, nonBeanTokenAmount); + } - lpOut = IWell(well).sync(user, 0); + uint256 beanWellBalance = IERC20(BEAN).balanceOf(well); + console.log("bean well balance", beanWellBalance); + uint256 wbtcWellBalance = IERC20(WBTC).balanceOf(well); + console.log("wbtc well balance", wbtcWellBalance); + console.log("syncing"); + lpOut = IWell(well).sync(user, 0); + console.log("syncing again"); // sync again to update reserves. IWell(well).sync(user, 0); } + function runningOnFork() public view returns (bool) { + bool isForked; + + try vm.activeFork() returns (uint256) { + isForked = true; + } catch { + isForked = false; + } + return isForked; + } + /** * @notice sets the reserves of a well by adding/removing liquidity. * @dev if the reserves decrease, manually remove liquidity. diff --git a/protocol/test/foundry/utils/Utils.sol b/protocol/test/foundry/utils/Utils.sol index 10c624df6..69db4fff9 100644 --- a/protocol/test/foundry/utils/Utils.sol +++ b/protocol/test/foundry/utils/Utils.sol @@ -22,6 +22,7 @@ contract Utils is Test { address internal constant WSTETH = 0x7f39C581F595B53c5cb19bD0b3f8dA6c935E2Ca0; address internal constant USDC = 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48; address internal constant USDT = 0xdAC17F958D2ee523a2206206994597C13D831ec7; + address internal constant WBTC = 0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599; address internal constant BEAN_ETH_WELL = 0xBEA0e11282e2bB5893bEcE110cF199501e872bAd; address internal constant BEAN_WSTETH_WELL = 0xBeA0000113B0d182f4064C86B71c315389E4715D; address payable internal constant PIPELINE =