Skip to content

Commit

Permalink
Beanstalk-3 pizzaman remediations (#1011)
Browse files Browse the repository at this point in the history
  • Loading branch information
pizzaman1337 authored Aug 7, 2024
2 parents f76f05a + a4e89ac commit 12e589f
Show file tree
Hide file tree
Showing 14 changed files with 474 additions and 57 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,10 @@ contract Listing is PodTransfer {
s.sys.fields[podListing.fieldId].harvestable <= podListing.maxHarvestableIndex,
"Marketplace: Expired."
);
require(
podListing.minFillAmount <= podListing.podAmount,
"Marketplace: minFillAmount must be <= podAmount."
);

if (s.sys.podListings[podListing.fieldId][podListing.index] != bytes32(0))
LibMarket._cancelPodListing(podListing.lister, podListing.fieldId, podListing.index);
Expand Down
45 changes: 37 additions & 8 deletions protocol/contracts/libraries/Oracle/LibUniswapOracle.sol
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@ import {C} from "contracts/C.sol";
import {LibUniswapOracleLibrary} from "./LibUniswapOracleLibrary.sol";
import "@uniswap/v3-core/contracts/interfaces/IUniswapV3Pool.sol";

interface IERC20Decimals {
function decimals() external view returns (uint8);
}

/**
* @title Uniswap Oracle Library
* @notice Contains functionalty to read prices from Uniswap V3 pools.
Expand All @@ -19,23 +23,48 @@ import "@uniswap/v3-core/contracts/interfaces/IUniswapV3Pool.sol";
library LibUniswapOracle {
// All instantaneous queries of Uniswap Oracles should use a 15 minute lookback.
uint32 internal constant FIFTEEN_MINUTES = 900;
uint256 constant PRECISION = 1e6;

/**
* @dev Uses `pool`'s Uniswap V3 Oracle to get the TWAP price of `token1` in `token2` over the
* last `lookback` seconds.
* Return value has 6 decimal precision.
* Returns 0 if {IUniswapV3Pool.observe} reverts.
* @notice Given a tick and a token amount, calculates the amount of token received in exchange
* @param baseTokenAmount Amount of baseToken to be converted.
* @param baseToken Address of the ERC20 token contract used as the baseAmount denomination.
* @param quoteToken Address of the ERC20 token contract used as the quoteAmount denomination.
* @return price Amount of quoteToken. Value has 6 decimal precision.
*/
function getTwap(
uint32 lookback,
address pool,
address token1,
address token2,
uint128 oneToken
address baseToken,
address quoteToken,
uint128 baseTokenAmount
) internal view returns (uint256 price) {
(bool success, int24 tick) = consult(pool, lookback);
if (!success) return 0;
price = LibUniswapOracleLibrary.getQuoteAtTick(tick, oneToken, token1, token2);

price = LibUniswapOracleLibrary.getQuoteAtTick(
tick,
baseTokenAmount,
baseToken,
quoteToken
);

uint256 baseTokenDecimals = IERC20Decimals(baseToken).decimals();
uint256 quoteTokenDecimals = IERC20Decimals(quoteToken).decimals();
int256 factor = int256(baseTokenDecimals) - int256(quoteTokenDecimals);

// decimals are the same. i.e. DAI/WETH
if (factor == 0) return (price * PRECISION) / (10 ** baseTokenDecimals);

// scale decimals
if (factor > 0) {
price = price * (10 ** uint256(factor));
} else {
price = price / (10 ** uint256(-factor));
}

// set 1e6 precision
price = (price * PRECISION) / (10 ** baseTokenDecimals);
}

/**
Expand Down
1 change: 0 additions & 1 deletion protocol/contracts/libraries/Oracle/LibUsdOracle.sol
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,6 @@ library LibUsdOracle {
if (token == C.WSTETH) {
return LibWstethUsdOracle.getUsdWstethPrice(lookback);
}

// tokens that use the custom oracle implementation are called here.
return getTokenPriceFromExternal(token, IERC20Decimals(token).decimals(), lookback);
}
Expand Down
3 changes: 3 additions & 0 deletions protocol/contracts/libraries/Oracle/LibWstethEthOracle.sol
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,9 @@ library LibWstethEthOracle {
ONE
);

// uniswap price is a 1e6, need to convert to 1e18, multiply by 1e12
uniswapPrice = uniswapPrice * PRECISION_DENOMINATOR;

// Check if the uniswapPrice oracle fails.
if (uniswapPrice == 0) return 0;

Expand Down
35 changes: 25 additions & 10 deletions protocol/contracts/libraries/Silo/LibFlood.sol
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ library LibFlood {
function handleRainAndSops(
address account,
uint32 lastUpdate,
uint128 firstGerminatingRoots
uint256 firstGerminatingRoots
) internal {
AppStorage storage s = LibAppStorage.diamondStorage();

Expand Down Expand Up @@ -259,7 +259,6 @@ library LibFlood {
quickSort(wellDeltaBs, 0, int(wellDeltaBs.length - 1));
}

// Reviewer note: This works, but there's got to be a way to make this more gas efficient
function quickSort(
WellDeltaB[] memory arr,
int left,
Expand All @@ -269,13 +268,29 @@ library LibFlood {

// Choose the median of left, right, and middle as pivot (improves performance on random data)
uint mid = uint(left) + (uint(right) - uint(left)) / 2;
WellDeltaB memory pivot = arr[uint(left)].deltaB > arr[uint(mid)].deltaB
? (
arr[uint(left)].deltaB < arr[uint(right)].deltaB
? arr[uint(left)]
: arr[uint(right)]
)
: (arr[uint(mid)].deltaB < arr[uint(right)].deltaB ? arr[uint(mid)] : arr[uint(right)]);
WellDeltaB memory pivot;

if (arr[uint(left)].deltaB > arr[uint(mid)].deltaB) {
if (arr[uint(left)].deltaB < arr[uint(right)].deltaB) {
pivot = arr[uint(left)];
} else {
if (arr[uint(right)].deltaB > arr[uint(mid)].deltaB) {
pivot = arr[uint(right)];
} else {
pivot = arr[uint(mid)];
}
}
} else {
if (arr[uint(mid)].deltaB < arr[uint(right)].deltaB) {
pivot = arr[uint(mid)];
} else {
if (arr[uint(right)].deltaB > arr[uint(left)].deltaB) {
pivot = arr[uint(right)];
} else {
pivot = arr[uint(left)];
}
}
}

int i = left;
int j = right;
Expand All @@ -290,7 +305,7 @@ library LibFlood {
}

if (left < j) {
return quickSort(arr, left, j);
arr = quickSort(arr, left, j);
}
if (i < right) {
return quickSort(arr, i, right);
Expand Down
2 changes: 1 addition & 1 deletion protocol/contracts/libraries/Silo/LibGerminate.sol
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,7 @@ library LibGerminate {
address account,
uint32 lastMowedSeason,
uint32 currentSeason
) internal returns (uint128 firstGerminatingRoots) {
) internal returns (uint256 firstGerminatingRoots) {
AppStorage storage s = LibAppStorage.diamondStorage();
bool lastUpdateOdd = isSeasonOdd(lastMowedSeason);
(uint256 firstStalk, uint256 secondStalk) = getGerminatingStalk(account, lastUpdateOdd);
Expand Down
2 changes: 1 addition & 1 deletion protocol/contracts/libraries/Silo/LibSilo.sol
Original file line number Diff line number Diff line change
Expand Up @@ -449,7 +449,7 @@ library LibSilo {
uint32 currentSeason = s.sys.season.current;

// End account germination.
uint128 firstGerminatingRoots;
uint256 firstGerminatingRoots;
if (lastUpdate < currentSeason) {
firstGerminatingRoots = LibGerminate.endAccountGermination(
account,
Expand Down
1 change: 1 addition & 0 deletions protocol/scripts/impersonate.js
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ async function wsteth() {
const wsteth = await ethers.getContractAt("MockWsteth", WSTETH);
await wsteth.setSymbol("wstETH");
await wsteth.setStEthPerToken(to18("1"));
await wsteth.setDecimals(18);
}

/// Uniswap V2 Router ///
Expand Down
83 changes: 83 additions & 0 deletions protocol/test/foundry/field/Marketplace.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import "forge-std/Test.sol";
import {TestHelper, IMockFBeanstalk} from "test/foundry/utils/TestHelper.sol";
import {MockFieldFacet} from "contracts/mocks/mockFacets/MockFieldFacet.sol";
import {MockSeasonFacet} from "contracts/mocks/mockFacets/MockSeasonFacet.sol";
import {C} from "contracts/C.sol";

contract ListingTest is TestHelper {
// test accounts
address[] farmers;

MockFieldFacet field = MockFieldFacet(BEANSTALK);
MockSeasonFacet season = MockSeasonFacet(BEANSTALK);

function setUp() public {
initializeBeanstalkTestState(true, false);

season.siloSunrise(0);

// initalize farmers from farmers (farmer0 == diamond deployer)
farmers.push(users[1]);
farmers.push(users[2]);

// max approve.
maxApproveBeanstalk(farmers);

mintTokensToUsers(farmers, C.BEAN, MAX_DEPOSIT_BOUND);

field.incrementTotalSoilE(1000e18);

// mine 300 blocks
vm.roll(300);

//set temp
bs.setYieldE(0);

console.log("bs.activeField(): ", bs.activeField());

// sow 1000
vm.prank(users[1]);
uint256 pods = bs.sow(1000e6, 0, 0);
console.log("Pods: ", pods);
vm.prank(users[2]);
bs.sow(1000e6, 0, 0);
}

function testCreatePodListing_InvalidMinFillAmount() public {
IMockFBeanstalk.PodListing memory podListing = IMockFBeanstalk.PodListing({
lister: users[1],
fieldId: bs.activeField(),
index: 0,
start: 0,
podAmount: 50,
pricePerPod: 100,
maxHarvestableIndex: 100,
minFillAmount: 60, // Invalid: greater than podAmount
mode: 0
});

vm.expectRevert("Marketplace: minFillAmount must be <= podAmount.");
vm.prank(users[1]);
bs.createPodListing(podListing);
}

function testCreatePodListing_ValidMinFillAmount() public {
// no revert
IMockFBeanstalk.PodListing memory podListing = IMockFBeanstalk.PodListing({
lister: users[1],
fieldId: bs.activeField(),
index: 0,
start: 0,
podAmount: 50,
pricePerPod: 100,
maxHarvestableIndex: 100,
minFillAmount: 30, // Valid: less than or equal to podAmount
mode: 0
});
vm.prank(users[1]);
bs.createPodListing(podListing);
}
}
79 changes: 62 additions & 17 deletions protocol/test/foundry/silo/Oracle.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -16,29 +16,30 @@ contract OracleTest is TestHelper {
initializeBeanstalkTestState(true, false);
}

// 1 WBTC = 50000 USD
// 1e8 = 50000 USD
function test_getUsdPrice() public {
function testUniswapOracleImplementation() public {
// encode type 0x01
vm.prank(BEANSTALK);
bs.updateOracleImplementationForToken(
WBTC,
IMockFBeanstalk.Implementation(address(0), bytes4(0), bytes1(0x01))
);

// encode type 0x01
uint256 price = OracleFacet(BEANSTALK).getUsdTokenPrice(WBTC);
// 1e8 / 50000 = 2000
assertEq(price, 2000);

// change encode type to 0x02, with a wbtc/usdc pool.
// todo: fix once oracle fixes come in.
// vm.prank(BEANSTALK);
// bs.updateOracleImplementationForToken(
// WBTC,
// IMockFBeanstalk.Implementation(WBTC_USDC_03_POOL, bytes4(0), bytes1(0x02))
// );
// price = OracleFacet(BEANSTALK).getUsdTokenPrice(WBTC);
// assertApproxEqRel(price, 2000, 0.001e18);
assertEq(price, 0.00002e8, "price using encode type 0x01");

// change encode type to 0x02:
vm.prank(BEANSTALK);
bs.updateOracleImplementationForToken(
WBTC,
IMockFBeanstalk.Implementation(WBTC_USDC_03_POOL, bytes4(0), bytes1(0x02))
);
price = OracleFacet(BEANSTALK).getTokenUsdPrice(WBTC);
// 1 USDC will get ~500 satoshis of BTC at $50k
// 1 USDC = 1e6
// 1 wBTC = 1e8
// $50,000/100000000*1000000 = 500
// function returns uint256(1e24).div(tokenPrice);
// expected delta is 0.2004008016032064%
assertApproxEqRel(price, 50000e6, 0.001e18, "price using encode type 0x02");
}

/**
Expand Down Expand Up @@ -149,4 +150,48 @@ contract OracleTest is TestHelper {
);
return token;
}

function testGetTokenPrice() public {
// change encode type to 0x02 for wbtc:
vm.prank(BEANSTALK);
bs.updateOracleImplementationForToken(
WBTC,
IMockFBeanstalk.Implementation(address(0), bytes4(0), bytes1(0x01))
);

// token price is number of dollars per token, i.e. 50000 USD for 1 WBTC
uint256 tokenPriceEth = OracleFacet(BEANSTALK).getTokenUsdPrice(C.WETH); // 1000e6
assertEq(tokenPriceEth, 1000e6, "getTokenUsdPrice eth");

// number of tokens received per dollar
uint256 usdPriceEth = OracleFacet(BEANSTALK).getUsdTokenPrice(C.WETH); // 1e15 which is 1e18 (1 eth in wei) / 1000 (weth price 1000), you get 1/1000th of 1 eth for $1
assertEq(usdPriceEth, 1e18 / 1000, "getUsdTokenPrice eth");

uint256 tokenPriceWBTC = OracleFacet(BEANSTALK).getTokenUsdPrice(WBTC); // should be 50000e6
assertEq(tokenPriceWBTC, 50000e6, "getTokenUsdPrice wbtc");

// number of tokens received per dollar
uint256 usdPriceWBTC = OracleFacet(BEANSTALK).getUsdTokenPrice(WBTC); // $1 = 0.00002 wbtc, wbtc is 8 decimals,
assertEq(usdPriceWBTC, 0.00002e8, "getUsdTokenPrice wbtc");
}

// test provided by T1MOH
function test_getUsdTokenPrice_whenExternalToken_priceIsInvalid() public {
// pre condition: encode type 0x01
vm.prank(BEANSTALK);
bs.updateOracleImplementationForToken(
WBTC,
IMockFBeanstalk.Implementation(address(0), bytes4(0), bytes1(0x01))
);

// WETH price is 1000
uint256 priceWETH = OracleFacet(BEANSTALK).getUsdTokenPrice(C.WETH);
assertEq(priceWETH, 1e15); // 1e18/1e3 = 1e15

// WBTC price is 50000
uint256 priceWBTC = OracleFacet(BEANSTALK).getUsdTokenPrice(WBTC);
assertEq(priceWBTC, 0.00002e8); // adjusted to 8 decimals
}

// TODO: fork tests to verify the on-chain values currently returned by oracles alignes with mocks?
}
Loading

0 comments on commit 12e589f

Please sign in to comment.