Platform: Code4rena
Start Date: 28/11/2022
Pot Size: $192,500 USDC
Total HM: 33
Participants: 106
Period: 11 days
Judge: LSDan
Total Solo HM: 15
Id: 186
League: ETH
Rank: 7/106
Findings: 5
Award: $4,903.26
🌟 Selected for report: 2
🚀 Solo Findings: 1
891.6728 USDC - $891.67
https://github.com/code-423n4/2022-11-paraspace/blob/c6820a279c64a299a783955749fdc977de8f0449/paraspace-core/contracts/ui/WPunkGateway.sol#L77-L95 https://github.com/code-423n4/2022-11-paraspace/blob/c6820a279c64a299a783955749fdc977de8f0449/paraspace-core/contracts/ui/WPunkGateway.sol#L129-L155 https://github.com/code-423n4/2022-11-paraspace/blob/c6820a279c64a299a783955749fdc977de8f0449/paraspace-core/contracts/ui/WPunkGateway.sol#L167-L193
All CryptoPunk deposits can be stolen
CryptoPunks were created before the ERC721 standard. A consequence of this is that they do not possess the transferFrom
method. To approximate this a user can offerPunkForSaleToAddress
for a price of 0 to effectively approve the contract to transferFrom
.
function supplyPunk( DataTypes.ERC721SupplyParams[] calldata punkIndexes, address onBehalfOf, uint16 referralCode ) external nonReentrant { for (uint256 i = 0; i < punkIndexes.length; i++) { Punk.buyPunk(punkIndexes[i].tokenId); Punk.transferPunk(proxy, punkIndexes[i].tokenId); // gatewayProxy is the sender of this function, not the original gateway WPunk.mint(punkIndexes[i].tokenId); } Pool.supplyERC721( address(WPunk), punkIndexes, onBehalfOf, referralCode ); }
The current implementation of WPunkGateway#supplyPunk
allows anyone to execute and determine where the nTokens are minted to. To complete the flow supply flow a user would need to offerPunkForSaleToAddress
for a price of 0 to WPunkGateway
. After they have done this, anyone can call the function to deposit the punk and mint the nTokens to themselves, effectively stealing it.
Example:
User A
owns tokenID
of 1. They want to deposit it so they call offerPunkForSaleToAddress
with an amount of 0, effectively approving the WPunkGateway
to transfer their CryptoPunk. User B
monitors the transactions and immediately calls supplyPunk
with themselves as onBehalfOf
. This completes the transfer of the CryptoPunk and deposits it into the pool but mints the nTokens
to User B
, allowing them to effectively steal the CryptoPunk
The same fundamental issue exists with acceptBidWithCredit
and batchAcceptBidWithCredit
Manual Review
Query the punkIndexToAddress to find the owner and only allow owner to deposit:
for (uint256 i = 0; i < punkIndexes.length; i++) { + address owner = Punk.punkIndexToAddress(punkIndexes[i].tokenId); + require(owner == msg.sender); Punk.buyPunk(punkIndexes[i].tokenId); Punk.transferPunk(proxy, punkIndexes[i].tokenId); // gatewayProxy is the sender of this function, not the original gateway WPunk.mint(punkIndexes[i].tokenId); }
#0 - c4-sponsor
2022-12-06T01:25:07Z
yubo-ruan marked the issue as sponsor confirmed
#1 - c4-judge
2022-12-20T18:11:08Z
dmvt marked the issue as duplicate of #71
#2 - c4-judge
2022-12-20T18:12:33Z
dmvt marked the issue as selected for report
#3 - c4-judge
2023-01-23T19:59:44Z
dmvt marked the issue as satisfactory
#4 - c4-judge
2023-01-26T15:07:51Z
Simon-Busch marked the issue as selected for report
🌟 Selected for report: 0x52
3523.4633 USDC - $3,523.46
Adversary can DOS user and make them pay more gas by sending them collateral
if (fromConfig.isUsingAsCollateral(reserveId)) { if (fromConfig.isBorrowingAny()) { ValidationLogic.validateHFAndLtvERC20( reservesData, reservesList, usersConfig[params.from], params.asset, params.from, params.reservesCount, params.oracle ); } if (params.balanceFromBefore == params.amount) { fromConfig.setUsingAsCollateral(reserveId, false); emit ReserveUsedAsCollateralDisabled( params.asset, params.from ); } //@audit collateral is automatically turned on for receiver if (params.balanceToBefore == 0) { DataTypes.UserConfigurationMap storage toConfig = usersConfig[params.to]; toConfig.setUsingAsCollateral(reserveId, true); emit ReserveUsedAsCollateralEnabled( params.asset, params.to ); } }
The above lines are executed when a user transfer collateral to another user. If the sending user currently has the collateral enabled and the receiving user doesn't have a balance already, the collateral will automatically be enabled for the receiver. Since the collateral is enabled, it will now be factored into the health check calculation. This increases gas for the receiver every time the user does anything that requires a health check (which ironically includes turning off a collateral). If enough different kinds of collateral are added to the platform it may even be enough gas to DOS the users.
Manual Review
Don't automatically enable the collateral for the receiver.
#0 - c4-judge
2022-12-20T14:19:36Z
dmvt marked the issue as primary issue
#1 - c4-judge
2023-01-09T23:41:48Z
dmvt marked the issue as selected for report
🌟 Selected for report: IllIllI
Also found by: 0x52, 0xNazgul, Franfran, IllIllI, Jeiwan, Lambda, RaymondFam, Rolezn, Trust, __141345__, codecustard, erictee, gzeon, hansfriese, imare, rbserver, rvierdiiev, seyni, skinz, ujamal_
18.3064 USDC - $18.31
Outdated or incorrect asset prices
Chainlink has officially deprecated the latestAnswer method, recommending to use latestRoundData instead. This offers much better data validation to ensure that the price data is current and correct.
Manual Review
Use latestRoundData instead. Validate outputs and pass to the fallback oracle if data is stale or incorrect.
#0 - c4-judge
2022-12-20T17:44:48Z
dmvt marked the issue as duplicate of #5
#1 - c4-judge
2023-01-09T16:32:58Z
dmvt marked the issue as partial-50
#2 - c4-judge
2023-01-23T15:57:06Z
dmvt marked the issue as satisfactory
365.8981 USDC - $365.90
Liquidation of assets when account isn't actually under-collateralized
function validateSetUseERC20AsCollateral( DataTypes.ReserveCache memory reserveCache, uint256 userBalance ) internal pure { require(userBalance != 0, Errors.UNDERLYING_BALANCE_ZERO); IXTokenType xToken = IXTokenType(reserveCache.xTokenAddress); //@audit user is unable to change status of sApe manually require( xToken.getXTokenType() != XTokenType.PTokenSApe, Errors.SAPE_NOT_ALLOWED ); ( bool isActive, , , bool isPaused, DataTypes.AssetType reserveAssetType ) = reserveCache.reserveConfiguration.getFlags(); require( reserveAssetType == DataTypes.AssetType.ERC20, Errors.INVALID_ASSET_TYPE ); require(isActive, Errors.RESERVE_INACTIVE); require(!isPaused, Errors.RESERVE_PAUSED); }
When depositing a BAYC, MAYC and BAKC sAPE is not immediately enabled as collateral. It is only enabled when a user calls PoolApeStaking#borrowApeAndStake. Otherwise if the user tries to enable manually enable it as a collateral if will revert in ValidationLogic#validateSetUseERC20AsCollateral. The result is that if a user deposits an NFT already staked with APE, then it won't be accounted for in their health factor. This is possible because when APE is staked it is locked in the staking contract but the NFT is free to be transferred link. This can lead to users being unfairly liquidated and their collateral being sold for a large loss. They will also be force to pay a liquidation incentive even though their position isn't actually under-collateralized
Manual Review
When a user deposits a BAYC, MAYC or BAKC the contract should check if the NFT already has sAPE. If it does then the contract should automatically enable the sAPE as collateral.
#0 - c4-judge
2022-12-20T17:33:48Z
dmvt marked the issue as duplicate of #421
#1 - c4-judge
2023-01-09T15:33:27Z
dmvt changed the severity to 2 (Med Risk)
#2 - c4-judge
2023-01-23T20:50:11Z
dmvt marked the issue as partial-50
🌟 Selected for report: IllIllI
Also found by: 0x4non, 0x52, 0xAgro, 0xNazgul, 0xSmartContract, 0xackermann, 9svR6w, Awesome, Aymen0909, B2, BRONZEDISC, Bnke0x0, Deekshith99, Deivitto, Diana, Dravee, HE1M, Jeiwan, Kaiziron, KingNFT, Lambda, Mukund, PaludoX0, RaymondFam, Rolezn, Sathish9098, Secureverse, SmartSek, __141345__, ahmedov, ayeslick, brgltd, cccz, ch0bu, chrisdior4, cryptonue, cryptostellar5, csanuragjain, datapunk, delfin454000, erictee, gz627, gzeon, helios, i_got_hacked, ignacio, imare, jadezti, jayphbee, joestakey, kankodu, ksk2345, ladboy233, martin, nadin, nicobevi, oyc_109, pashov, pavankv, pedr02b2, pzeus, rbserver, ronnyx2017, rvierdiiev, shark, unforgiven, xiaoming90, yjrwkk
103.9175 USDC - $103.92
User is forced to pay too high of an incentive if part of their staked apecoin isn't borrowed and they are forced to unstake
uint256 unstakedAmount = _apeCoin.balanceOf(address(this)) - balanceBefore; if (unstakedAmount == 0) { return; } //2 send incentive to caller if (params.incentiveReceiver != address(0)) { uint256 unstakeIncentive = stakingParameter.unstakeIncentive; if (unstakeIncentive > 0) { uint256 incentiveAmount = unstakedAmount.percentMul( unstakeIncentive ); _apeCoin.safeTransfer( params.incentiveReceiver, incentiveAmount ); unstakedAmount = unstakedAmount - incentiveAmount; } }
Paraspace allows users to deposit their MAYC, BAYC and BAKC then take out a loan against their NFT to borrow apecoin and stake it. The staked coins don't have to be borrowed. A user can stake ApeCoin before they deposit their NFT to ParaSpace. If their collateralization drops too low they will be forced to unstake and will be force to pay an incentive to the caller. This is where the issues is because when forced to unstake they will pay an incentive on ApeCoin that wasn't borrowed.
Manual Review
Rewrite the lines so that the user only pays incentive on the amount that was liquidated so that user is not unfairly penalized:
//2 repay DataTypes.ReserveData memory apeCoinData = params.POOL.getReserveData( address(_apeCoin) ); uint256 repayAmount = IERC20(apeCoinData.variableDebtTokenAddress) .balanceOf(positionOwner); if (repayAmount > 0) { repayAmount = Math.min(repayAmount, unstakedAmount); } uint256 supplyAmount = unstakedAmount - repayAmount; //3 send incentive to caller and supply if (params.incentiveReceiver != address(0)) { uint256 unstakeIncentive = stakingParameter.unstakeIncentive; if (unstakeIncentive > 0) { uint256 incentiveAmount = repayAmount.percentMul( unstakeIncentive ); _apeCoin.safeTransfer( params.incentiveReceiver, incentiveAmount ); supplyAmount = supplyAmount - incentiveAmount; } }
#0 - c4-judge
2023-01-09T22:43:37Z
dmvt changed the severity to QA (Quality Assurance)
#1 - c4-judge
2023-01-25T16:30:47Z
dmvt marked the issue as grade-b