Platform: Code4rena
Start Date: 16/12/2022
Pot Size: $60,500 USDC
Total HM: 12
Participants: 58
Period: 5 days
Judge: Trust
Total Solo HM: 4
Id: 196
League: ETH
Rank: 30/58
Findings: 2
Award: $57.64
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 Selected for report: Jeiwan
Also found by: 0x52, Franfran, HollaDieWaldfee, KingNFT, Saintcode_, bin2chen, evan, fs0c, noot, poirots, rvierdiiev, stealthyz, teawaterwire, unforgiven
16.6999 USDC - $16.70
PaprController.buyAndReduceDebt() does not validate params.swapFeeBips
, which can be set to any value by the user. This allows the user to arbitrarily manipulate the amount of underlying
token transferred to them, and thus can drain the contract.
The function buyAndRedudeDebt()
is callable by anyone, and is as follows:
function buyAndReduceDebt(address account, ERC721 collateralAsset, IPaprController.SwapParams calldata params) external override returns (uint256) { bool hasFee = params.swapFeeBips != 0; (uint256 amountOut, uint256 amountIn) = UniswapHelpers.swap( pool, account, token0IsUnderlying, params.amount, params.minOut, params.sqrtPriceLimitX96, abi.encode(msg.sender) ); if (hasFee) { underlying.transfer(params.swapFeeTo, amountIn * params.swapFeeBips / BIPS_ONE); } _reduceDebt({account: account, asset: collateralAsset, burnFrom: msg.sender, amount: amountOut}); return amountOut; }
To successfully drain the contract of underlying
via underlying.transfer()
:
params.swapFeeTo
to be their own address, and params.swapFeeBips
to be a large value such that amountIn * params.swapFeeBips / BIPS_ONE >= underlying.balanceOf(address(this))
. Note that the caller has control over amountIn
via params.amount
and params.minOut
._reduceDebt
, the attacker also needs to ensure that amountOut <= _vaultInfo[account][collateralAsset].debt
and also that amountOut <=
the Papr balance of msg.sender
. To ensure they have enough debt and also have Papr, the attacker can call increaseDebt
after depositing any valid ERC721 via addCollateral
-> _addCollateralToVault()
. Also note that collateralAsset
is not validated, so the attacker can also pass any ERC721 that they had previously deposited.uniswapV3SwapCallback()
, the attacker also needs to ensure that they have >=amountToPay
underlying
token. However, the amountToPay
is also manipulable by the caller via params.amount
in buyAndReduceDebt
; they can just set some small value to be transferred out of their account.buyAndReduceDebt
with parameters that satisfy the above constraints, which will then transfer the desired amount of underlying
to the attacker.n/a
Validate params.swapFeeBips
such that the amount transferred out of the user's account in uniswapV3SwapCallback
is proportional to it, or pass it to _reduceDebt()
and increase the user's debt proportionally to params.swapFeeBips
.
#0 - c4-judge
2022-12-25T13:22:41Z
trust1995 marked the issue as duplicate of #20
#1 - c4-judge
2022-12-25T13:22:58Z
trust1995 changed the severity to 2 (Med Risk)
#2 - c4-judge
2022-12-25T13:23:07Z
trust1995 marked the issue as partial-50
#3 - trust1995
2022-12-25T13:23:50Z
Partial because problem was spotted but it's not a drain issue but rather a contract not working as intended issue.
#4 - C4-Staff
2023-01-10T22:32:21Z
JeeberC4 marked the issue as duplicate of #196
🌟 Selected for report: IllIllI
Also found by: 0xSmartContract, 0xhacksmithh, Awesome, Aymen0909, Mukund, RaymondFam, Rolezn, TomJ, c3phas, eyexploit, noot, rbitbytes, rjs, saneryee
40.9392 USDC - $40.94
In purchaseLiquidationAuctionNFT()
, various calculations are done before _purchaseNFTAndUpdateVaultIfNeeded()
is called. However, none of these calculated values are passed to _purchaseNFTAndUpdateVaultIfNeeded()
, which may revert. The call to _purchaseNFTAndUpdateVaultIfNeeded()
should be the first thing to happen in the function to save users gas in case of reverts.
removeCollateral
should accept accept ERC721 address and array of IDs instead of array of CollateralThis function requires all collateral addresses to be the same. To remove the address check which happens on every iteration >0, this function should instead accept the collateral address and array of IDs. This can also remove the if (i == 0)
check.
startLiquidationAuction
is unneededThere is no way to end an auction once it starts (apart from purchasing the NFT), thus calling startLiquidationAuction
multiple times will revert in the _startAuction
call. The timestamp check inside the function:
if (block.timestamp - info.latestAuctionStartTime < liquidationAuctionMinSpacing) { revert IPaprController.MinAuctionSpacing(); }
effectively does nothing, as info.latestAuctionStartTime
will always be 0 on the first call to start the auction, and if it's set, the call will revert during _startAuction
.
newDebt
check should be executed earlier in _increaseDebt()
The check for newDebt
inside _increaseDebt
is as follows:
if (newDebt >= 1 << 200) revert IPaprController.DebtAmountExceedsUint200();
This line should be moved directly under the line which calculates newDebt
to save gas in case of reverts.
#0 - c4-judge
2022-12-25T16:39:28Z
trust1995 marked the issue as grade-b