BigBang and Singularity both have a buyCollateral function designed to allow users to lever up by borrowing more assets and buying collateral with it. In BigBang this is a public function, and in Singularity this is a accessible through a public function that ties into the SGLLeverage module. Effectively these have the same implementation.
Due to an ineffective check on the swap's final output collateralShare, an attacker can steal any user's approvals of asset to BigBang or Singularity. An attacker can also cause any user to borrow extra assets and buy collateral with it up until the point that the user is barely still solvent. Combined with a sandwich attack this can be used to steal the new collateral.
Notably the function takes a address from parameter, which allows the caller to specify any address as the user that is borrowing and buying collateral. Two checks are attempted to prevent this from being abused:
the solvent(from) modifier
the _allowedBorrow(from, collateralShare) check
In actuality, the solvent(from) only limits the damage and the _allowedBorrow(from, collateralShare) check can be manipulated or outright bypassed.
Impact
The exploit hinges on abusing the _allowedBorrow(from, collateralShare) check. This is designed to check the allowance of msg.sender for from, but will pass in all cases if collateralShare is 0.
function _allowedBorrow(address from, uint share) internal {
if (from != msg.sender) {
if (allowanceBorrow[from][msg.sender] < share) {
revert NotApproved(from, msg.sender);
}
allowanceBorrow[from][msg.sender] -= share;
}
}
In order to get collateralShare as 0, the amountOut of the swapper.swap() call must be 0.
The attacker provides minAmountOut, and in certain instances can manipulate the swap using a sandwich to return 0.
The protocol initially has 3 Swappers listed:
UniswapV2Swapper
UniswapV3Swapper
CurveSwapper
The UniswapV2Swapper and UniswapV3Swapper are both vulnerable to this attack, though with exceptions.
For UniswapV2Swapper, with a large enough flashmint the attacker can skew the curve enough to return 0. This would require reserves to be small and the flashmint to be inexpensive, but both are possible. BigBang supports USDO flashmints and has admin-setter functions for flashmint fees and max limits that both don't have bounds. Seems reasonable to think the flashmints have potential to be free and unlimited.
For UniswapV3Swapper, the same scenario as V2 applies as well as the possibility that full-range liquidity is not supplied. In this case the attacker needs less capital to skew the curve enough to return 0.
CurveSwapper is not vulnerable to this attack, as in the implementation it is not possible to return 0.
It seems reasonable to assume that in the future new swappers could be introduced - such as support for UniswapX. In a case of a new swapper implementation where a user is directly filled for minAmountOut, this vulnerability becomes trivial to exploit.
In a much more simple case than the amountOut == 0 extreme, if the attacker has any approvals from a user to borrow, that allowance can be abused. All of the current swappers are vulnerable to attacker sandwiches, which lowers amountOut. While having amountOut == 0 is difficult, skewing any of the swappers to have amountOut < allowanceBorrow[from][msg.sender] is much easier.
Stealing Approvals
Attacker controls supplyAmount and from. This portion of the code means any user's approvals of asset to BigBang or Singularity can be sent to the swapper.
Note that the from user's solvency is checked at the end of the function via the solvent(from) modifier, so the attacker cannot cause a borrow that brings the user to insolvency.
Summary
In the maximum impact case, an attacker can steal any user's approvals of asset to BigBang or Singularity as well as force any user to borrow extra assets and buy collateral with it up until the point that the user is barely still solvent. Combined with a sandwich attack this can be used to steal the approved assets as well as the new collateral.
In the case that has reduced surface but easier replication, any user that has pre-approved another user for allowanceBorrow[from][msg.sender] falls victim to these same attacks.
Proof of Concept
The PoC hinges on the atomic sandwich of the swapper.
Flashmint USDO or flashloan other asset to get USDO via another swapper.
this calldata will have the yieldbox.transfer line move all of the user's asset approvals to the swapper.
this calldata will have the _borrow() line move extra borrowed assets to the swapper.
swapper.swap() will fetch (0, 0) due to the skewed price
_allowedBorrow() passes
solvent(from) checks for the user's solvency, which passes
Sell original tOFT into swapper for USDO at improved price
Repay flashloan
Tools Used
Manual Review
Recommended Mitigation Steps
Perform transfer allowance checks for the user's approvals before performing the yieldBox.transfer(from, address(swapper), assetId, supplyShare);
Perform the _allowedBorrow check for the borrowShare`` that takes place beforeswapper.swap. ie:if (borrowShare > 0) { _allowedBorrow(from, borrowShare); }`
Lines of code
https://github.com/Tapioca-DAO/tapioca-bar-audit/blob/2286f80f928f41c8bc189d0657d74ba83286c668/contracts/markets/singularity/SGLLeverage.sol#L147-L186 https://github.com/Tapioca-DAO/tapioca-bar-audit/blob/2286f80f928f41c8bc189d0657d74ba83286c668/contracts/markets/bigBang/BigBang.sol#L336-L375
Vulnerability details
BigBang and Singularity both have a
buyCollateral
function designed to allow users to lever up by borrowing more assets and buying collateral with it. In BigBang this is a public function, and in Singularity this is a accessible through a public function that ties into the SGLLeverage module. Effectively these have the same implementation.Due to an ineffective check on the swap's final output
collateralShare
, an attacker can steal any user's approvals ofasset
to BigBang or Singularity. An attacker can also cause any user to borrow extra assets and buy collateral with it up until the point that the user is barely still solvent. Combined with a sandwich attack this can be used to steal the new collateral.Notably the function takes a
address from
parameter, which allows the caller to specify any address as the user that is borrowing and buying collateral. Two checks are attempted to prevent this from being abused:solvent(from)
modifier_allowedBorrow(from, collateralShare)
checkIn actuality, the
solvent(from)
only limits the damage and the_allowedBorrow(from, collateralShare)
check can be manipulated or outright bypassed.Impact
The exploit hinges on abusing the
_allowedBorrow(from, collateralShare)
check. This is designed to check the allowance ofmsg.sender
forfrom
, but will pass in all cases ifcollateralShare
is 0.In order to get
collateralShare
as 0, theamountOut
of theswapper.swap()
call must be 0.The attacker provides
minAmountOut
, and in certain instances can manipulate the swap using a sandwich to return 0.The protocol initially has 3 Swappers listed:
The UniswapV2Swapper and UniswapV3Swapper are both vulnerable to this attack, though with exceptions.
For UniswapV2Swapper, with a large enough flashmint the attacker can skew the curve enough to return 0. This would require reserves to be small and the flashmint to be inexpensive, but both are possible. BigBang supports USDO flashmints and has admin-setter functions for flashmint fees and max limits that both don't have bounds. Seems reasonable to think the flashmints have potential to be free and unlimited.
For UniswapV3Swapper, the same scenario as V2 applies as well as the possibility that full-range liquidity is not supplied. In this case the attacker needs less capital to skew the curve enough to return 0.
CurveSwapper is not vulnerable to this attack, as in the implementation it is not possible to return 0.
It seems reasonable to assume that in the future new swappers could be introduced - such as support for UniswapX. In a case of a new swapper implementation where a user is directly filled for
minAmountOut
, this vulnerability becomes trivial to exploit.In a much more simple case than the
amountOut == 0
extreme, if the attacker has any approvals from a user to borrow, that allowance can be abused. All of the current swappers are vulnerable to attacker sandwiches, which lowersamountOut
. While havingamountOut == 0
is difficult, skewing any of the swappers to haveamountOut < allowanceBorrow[from][msg.sender]
is much easier.Stealing Approvals
Attacker controls
supplyAmount
andfrom
. This portion of the code means any user's approvals ofasset
to BigBang or Singularity can be sent to the swapper.Stealing Collateral
Attacker controls
borrowAmount
andfrom
. This portion of the code performs the_borrow
action for the user up toborrowAmount
.Note that the
from
user's solvency is checked at the end of the function via thesolvent(from)
modifier, so the attacker cannot cause a borrow that brings the user to insolvency.Summary
In the maximum impact case, an attacker can steal any user's approvals of
asset
to BigBang or Singularity as well as force any user to borrow extra assets and buy collateral with it up until the point that the user is barely still solvent. Combined with a sandwich attack this can be used to steal the approved assets as well as the new collateral.In the case that has reduced surface but easier replication, any user that has pre-approved another user for
allowanceBorrow[from][msg.sender]
falls victim to these same attacks.Proof of Concept
The PoC hinges on the atomic sandwich of the swapper.
buyCollateral(target, max_borrowable, full_approvals, 0, swapper, dexData)
yieldbox.transfer
line move all of the user's asset approvals to the swapper._borrow()
line move extra borrowed assets to the swapper.swapper.swap()
will fetch(0, 0)
due to the skewed price_allowedBorrow()
passessolvent(from)
checks for the user's solvency, which passesTools Used
Manual Review
Recommended Mitigation Steps
Perform transfer allowance checks for the user's approvals before performing the
yieldBox.transfer(from, address(swapper), assetId, supplyShare);
Perform the
_allowedBorrow
check for theborrowShare`` that takes place before
swapper.swap. ie:
if (borrowShare > 0) { _allowedBorrow(from, borrowShare); }`Assessed type
Other