Platform: Code4rena
Start Date: 21/08/2023
Pot Size: $125,000 USDC
Total HM: 26
Participants: 189
Period: 16 days
Judge: GalloDaSballo
Total Solo HM: 3
Id: 278
League: ETH
Rank: 53/189
Findings: 6
Award: $233.83
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 Selected for report: klau5
Also found by: 0x3b, 0xCiphky, 0xDING99YA, 0xWaitress, 0xbranded, 0xc0ffEE, 0xklh, 0xsurena, 0xvj, ABA, AkshaySrivastav, Anirruth, Aymen0909, Baki, Blockian, BugzyVonBuggernaut, DanielArmstrong, Evo, GangsOfBrahmin, HChang26, Inspex, Jiamin, Juntao, Kow, Krace, KrisApostolov, LFGSecurity, LokiThe5th, Mike_Bello90, Norah, Nyx, QiuhaoLi, RED-LOTUS-REACH, SBSecurity, Snow24, SpicyMeatball, T1MOH, Tendency, Toshii, Udsen, Yanchuan, __141345__, ak1, asui, auditsea, ayden, bart1e, bin2chen, blutorque, carrotsmuggler, chaduke, chainsnake, circlelooper, clash, codegpt, crunch, degensec, dirk_y, ge6a, gjaldon, grearlake, jasonxiale, juancito, ke1caM, kodyvim, kutugu, ladboy233, lanrebayode77, mahdikarimi, max10afternoon, mert_eren, nirlin, nobody2018, oakcobalt, parsely, peakbolt, pks_, pontifex, ravikiranweb3, rokinot, rvierdiiev, said, savi0ur, sces60107, sh1v, sl1, spidy730, tapir, tnquanghuy0512, ubermensch, visualbits, volodya, wintermute
0.0098 USDC - $0.01
Detailed description of the impact of this finding.
DOS attack can be launched to PerpetualAtlanticVaultLP.subtractLoss(). This is because the condition collateral.balanceOf(address(this)) == _totalCollateral - loss
will always fail if a malicious user donates some collateral tokens to the PerpetualAtlanticVaultLP contract.
Provide direct links to all referenced code in GitHub. Add screenshots, logs, or any other relevant proof that illustrates the concept.
PerpetualAtlanticVaultLP.subtractLoss()
allows the PerpetualAtlanticVault
contract to readjust the value of _totalCollateral
based on the input loss
. It checks that collateral.balanceOf(address(this)) == _totalCollateral - loss
However, if a malicious user donates some collateral tokens to the PerpetualAtlanticVaultLP contract, then the condition collateral.balanceOf(address(this)) == _totalCollateral - loss
will break forever, and therefore, subtractLoss() will be blocked forever.
Therefore, a user can easily launch a DOS attack to PerpetualAtlanticVaultLP.subtractLoss() and PerpetualAtlanticVault.settle() (since it calls the former) by sending free collateral tokens to the PerpetualAtlanticVaultLP contract.
VSCode
Change the condition collateral.balanceOf(address(this)) == _totalCollateral - loss
to collateral.balanceOf(address(this)) >= _totalCollateral - loss
to prevent donation-based DOS attack:
function subtractLoss(uint256 loss) public onlyPerpVault { require( - collateral.balanceOf(address(this)) == _totalCollateral - loss, + collateral.balanceOf(address(this)) >= _totalCollateral - loss, "Not enough collateral was sent out" ); _totalCollateral -= loss; }
DoS
#0 - c4-pre-sort
2023-09-09T06:40:46Z
bytes032 marked the issue as duplicate of #619
#1 - c4-pre-sort
2023-09-11T16:14:10Z
bytes032 marked the issue as sufficient quality report
#2 - c4-judge
2023-10-20T20:02:29Z
GalloDaSballo marked the issue as satisfactory
#3 - c4-judge
2023-10-21T07:26:28Z
GalloDaSballo changed the severity to 3 (High Risk)
#4 - c4-judge
2023-10-21T07:26:28Z
GalloDaSballo changed the severity to 3 (High Risk)
#5 - c4-judge
2023-10-21T07:26:28Z
GalloDaSballo changed the severity to 3 (High Risk)
🌟 Selected for report: bart1e
Also found by: 0x3b, 0xCiphky, Aymen0909, HHK, Inspex, bin2chen, chaduke, gkrastenov, jasonxiale, josephdara, kodyvim, peakbolt, pep7siup, rokinot, rvierdiiev, tapir
181.367 USDC - $181.37
Detailed description of the impact of this finding. All ERC721 tokens recovered by UniV3LiquidityAmo.recoverERC721() into rdpxV2Core will be lost in rdpxV2Core forever. The main reason is that there is no emergency withdrawl function in RdpxV2Core.sol for ERC721 and no other functions can perform any operations on those recovered ERC721 tokens.
Provide direct links to all referenced code in GitHub. Add screenshots, logs, or any other relevant proof that illustrates the concept.
UniV3LiquidityAmo.recoverERC721() allows one to recover ERC721 tokens. However, the ERC721 tokens will be sent to rdpxV2Core:
Unfortunately, once sent to rdpxV2Core, there is no emergency withdrawl function in RdpxV2Core.sol to withdraw them. No other functions exists to deal with these ERC721 tokens. Therefore, they will be lost in the contract forever.
VScode
Add emergency withdrawl functions for ERC721 tokens in RdpxV2Core.sol as well.
ERC721
#0 - c4-pre-sort
2023-09-09T06:29:19Z
bytes032 marked the issue as primary issue
#1 - c4-pre-sort
2023-09-12T06:09:38Z
bytes032 marked the issue as sufficient quality report
#2 - c4-pre-sort
2023-09-12T06:12:29Z
bytes032 marked the issue as duplicate of #935
#3 - c4-judge
2023-10-20T18:06:09Z
GalloDaSballo marked the issue as satisfactory
🌟 Selected for report: said
Also found by: 0Kage, 0xCiphky, 0xkazim, 836541, AkshaySrivastav, Evo, HChang26, HHK, KrisApostolov, Neon2835, QiuhaoLi, Tendency, Toshii, bart1e, bin2chen, carrotsmuggler, chaduke, etherhood, gjaldon, glcanvas, josephdara, lanrebayode77, mahdikarimi, max10afternoon, nobody2018, peakbolt, qpzm, rvierdiiev, sces60107, tapir, ubermensch, volodya
17.313 USDC - $17.31
Detailed description of the impact of this finding. PerpetualAtlanticVaultLP.deposit() calls previewDeposit(assets) before calling perpetualAtlanticVault.updateFunding(), as a result, the depositor will receive more shares they he deserves.
Provide direct links to all referenced code in GitHub. Add screenshots, logs, or any other relevant proof that illustrates the concept.
PerpetualAtlanticVaultLP.deposit() calls perpetualAtlanticVault.updateFunding() so that due funds can be sent from the PerpetualAtlanticVault to the PerpetualAtlanticVaultLP, which would increase the _totalCollateral
.
While the redeem() function calls perpetualAtlanticVault.updateFunding() as the first task; PerpetualAtlanticVaultLP.deposit() calls previewDeposit(assets) first before calling perpetualAtlanticVault.updateFunding(). As a result, the number of shares
of LP tokens will be larger than it should be since the due funds have not been transferred to the PerpetualAtlanticVaultLP contract yet, which would increase the _totalCollateral
value.
As a result, a depositor will receive more shares of LP tokens they he deserves, which means loss of funds to the contract and thus to some other users.
VSCode
Call perpetualAtlanticVault.updateFunding() first in deposit():
function deposit( uint256 assets, address receiver ) public virtual returns (uint256 shares) { + perpetualAtlanticVault.updateFunding(); // Check for rounding error since we round down in previewDeposit. require((shares = previewDeposit(assets)) != 0, "ZERO_SHARES"); - perpetualAtlanticVault.updateFunding(); // Need to transfer before minting or ERC777s could reenter. collateral.transferFrom(msg.sender, address(this), assets); _mint(receiver, shares); _totalCollateral += assets; emit Deposit(msg.sender, receiver, assets, shares); }
Timing
#0 - c4-pre-sort
2023-09-07T13:32:27Z
bytes032 marked the issue as duplicate of #1948
#1 - c4-pre-sort
2023-09-07T13:41:15Z
bytes032 marked the issue as duplicate of #867
#2 - c4-pre-sort
2023-09-11T09:04:05Z
bytes032 marked the issue as sufficient quality report
#3 - c4-judge
2023-10-20T19:26:48Z
GalloDaSballo marked the issue as satisfactory
🌟 Selected for report: 0xrafaelnicolau
Also found by: 0x111, 0xCiphky, 0xMosh, 0xWaitress, 0xc0ffEE, 0xkazim, 0xnev, 0xvj, ABAIKUNANBAEV, Aymen0909, Baki, ElCid, HChang26, HHK, Inspex, Jorgect, Kow, Krace, KrisApostolov, LFGSecurity, MiniGlome, Nyx, QiuhaoLi, RED-LOTUS-REACH, Talfao, Toshii, Vagner, Viktor_Cortess, Yanchuan, _eperezok, asui, atrixs6, bart1e, bin2chen, carrotsmuggler, chaduke, chainsnake, deadrxsezzz, degensec, dethera, dimulski, dirk_y, ether_sky, gizzy, glcanvas, grearlake, gumgumzum, halden, hals, kodyvim, koo, ladboy233, lanrebayode77, max10afternoon, minhtrng, mussucal, nobody2018, peakbolt, pontifex, qbs, ravikiranweb3, rvierdiiev, said, tapir, ubermensch, volodya, wintermute, yashar, zaevlad, zzebra83
0.0367 USDC - $0.04
Detailed description of the impact of this finding.
RdpxV2Core.withdraw() fails to decrease totalWethDelegated
. As a result, the state of totalWethDelegated
and reserveAsset[i].tokenBalance for Weth might be wrong.
Provide direct links to all referenced code in GitHub. Add screenshots, logs, or any other relevant proof that illustrates the concept.
The RdpxV2Core.withdraw() allows the owner of a delegate to withdraw the portion of the weth that is not in the portion of activeCollateral
.
However, the function does not decrease totalWethDelegated
. As a result, the accounting totalWethDelegated
, and thus reserveAsset[i].tokenBalance for Weth are all wrong. Such wrong contract state can lead to unpredictable future result.
VScode
Decrease totalWethDelegated
for RdpxV2Core.withdraw():
function withdraw( uint256 delegateId ) external returns (uint256 amountWithdrawn) { _whenNotPaused(); _validate(delegateId < delegates.length, 14); Delegate storage delegate = delegates[delegateId]; _validate(delegate.owner == msg.sender, 9); amountWithdrawn = delegate.amount - delegate.activeCollateral; _validate(amountWithdrawn > 0, 15); delegate.amount = delegate.activeCollateral; + totalWethDelegated -= amountWithdrawn IERC20WithBurn(weth).safeTransfer(msg.sender, amountWithdrawn); emit LogDelegateWithdraw(delegateId, amountWithdrawn); } /**
Math
#0 - c4-pre-sort
2023-09-08T13:24:16Z
bytes032 marked the issue as duplicate of #2186
#1 - c4-judge
2023-10-20T17:58:59Z
GalloDaSballo marked the issue as partial-50
#2 - c4-judge
2023-10-21T07:38:54Z
GalloDaSballo changed the severity to 3 (High Risk)
#3 - c4-judge
2023-10-21T07:49:38Z
GalloDaSballo marked the issue as partial-25
#4 - chaduke3730
2023-10-23T01:48:54Z
I wonder why it is a partial-25?
#5 - GalloDaSballo
2023-10-23T07:31:24Z
25% Because no Revert on Sync
50% if it had revert on Sync
100% if revert was weaponized to block lowerDepeg or AMO operations
#6 - chaduke3730
2023-10-27T02:54:24Z
Thanks
🌟 Selected for report: 0xTheC0der
Also found by: 0Kage, 0xDING99YA, 0xHelium, 0xbranded, 836541, ABA, Kow, QiuhaoLi, SpicyMeatball, T1MOH, __141345__, alexfilippov314, ayden, bart1e, bin2chen, chaduke, degensec, jasonxiale, joaovwfreire, nirlin, peakbolt, pep7siup, rvierdiiev, tnquanghuy0512
15.9268 USDC - $15.93
Detailed description of the impact of this finding. nextFundingPaymentTimestamp() assumes that each funding duration is the same, which is not true, since an admin can change the funding duration via updateFundingDuration(), which should not be retrospective.
Provide direct links to all referenced code in GitHub. Add screenshots, logs, or any other relevant proof that illustrates the concept.
PerpetualAtlanticVault.nextFundingPaymentTimestamp() assumes that each funding duration is the same
This assumption is not true since updateFundingDuration() can change it, but not retrospectively.
As a result, the way to calculate nextFundingPaymentTimestamp is wrong. It cannot be genesis + (latestFundingPaymentPointer * fundingDuration);
.
VCCode
Introduce a variable lastFundingPaymentTimestamp
or do not allow the change of funding duration once latestFundingPaymentPointer >= 1.
function nextFundingPaymentTimestamp() public view returns (uint256 timestamp) { - return genesis + (latestFundingPaymentPointer * fundingDuration); + return lastFundingPaymentTimestamp + fundingDuration; } ## Assessed type Math
#0 - c4-pre-sort
2023-09-08T06:31:35Z
bytes032 marked the issue as duplicate of #980
#1 - c4-pre-sort
2023-09-11T08:23:20Z
bytes032 marked the issue as sufficient quality report
#2 - c4-judge
2023-10-20T11:09:32Z
GalloDaSballo changed the severity to 2 (Med Risk)
#3 - c4-judge
2023-10-20T11:11:33Z
GalloDaSballo marked the issue as satisfactory
🌟 Selected for report: juancito
Also found by: 0xDING99YA, 0xTiwa, 0xkazim, 0xnev, ABA, ArmedGoose, Aymen0909, Bauchibred, Evo, IceBear, KrisApostolov, MohammedRizwan, Nikki, QiuhaoLi, T1MOH, Toshii, WoolCentaur, Yanchuan, __141345__, asui, bart1e, carrotsmuggler, catellatech, chaduke, codegpt, deadrxsezzz, degensec, dethera, dirk_y, erebus, ether_sky, gjaldon, glcanvas, jasonxiale, josephdara, klau5, kodyvim, ladboy233, lsaudit, minhquanym, parsely, peakbolt, pep7siup, rvierdiiev, said, savi0ur, sces60107, tapir, ubermensch, volodya, zzebra83
19.1724 USDC - $19.17
QA1. Some ERC20 tokens do not support allowance approval from non-zero to non-zero. So the following RdpxV2Core.approveContractToSpend() code will not work.
Correction: approve to zero first before approving to the designated amount
for a safe approval:
function approveContractToSpend( address _token, address _spender, uint256 _amount ) external onlyRole(DEFAULT_ADMIN_ROLE) { _validate(_token != address(0), 17); _validate(_spender != address(0), 17); _validate(_amount > 0, 17); + uint256 allowance = IERC20WithBurn(_token).allowance(msg.sender, _spender); + if(allowance != 0) IERC20WithBurn(_token).approve(_spender, 0); IERC20WithBurn(_token).approve(_spender, _amount); }
QA2. Possible divide by zero error in perpetualAtlanticVault._updateFundingRate()
due to lack of check of wether endTime == startTime
for the case of fundingRates[latestFundingPaymentPointer] == 0
.
Mitigation: we need to check endTime == startTime
for the case of fundingRates[latestFundingPaymentPointer] == 0
as well.
function _updateFundingRate(uint256 amount) private { if (fundingRates[latestFundingPaymentPointer] == 0) { uint256 startTime; if (lastUpdateTime > nextFundingPaymentTimestamp() - fundingDuration) { startTime = lastUpdateTime; } else { startTime = nextFundingPaymentTimestamp() - fundingDuration; } uint256 endTime = nextFundingPaymentTimestamp(); + if (endTime == startTime) return; fundingRates[latestFundingPaymentPointer] = (amount * 1e18) / (endTime - startTime); } else { uint256 startTime = lastUpdateTime; uint256 endTime = nextFundingPaymentTimestamp(); if (endTime == startTime) return; fundingRates[latestFundingPaymentPointer] = fundingRates[latestFundingPaymentPointer] + ((amount * 1e18) / (endTime - startTime)); } }
QA3. UniV2LiquidityAmo.addLiquidity() (removeLiquidity() and swap()) might fail for some ERC20 tokens that revert on zero-transfer. The main reason is that it calls _sendTokensToRdpxV2Core() to send back ununsed tokenA and tokenB. However, there is no unused tokenA, then _sendTokensToRdpxV2Core() will faile for tokens that revert on zero-transfer.
The following _sendTokensToRdpxV2Core() function will fail if there is zero balance for tokenA or tokenB.
Mitigation: check whether the balance if zero or not, call safeTransfer only when the balance of non-zero:
function _sendTokensToRdpxV2Core() internal { uint256 tokenABalance = IERC20WithBurn(addresses.tokenA).balanceOf( address(this) ); uint256 tokenBBalance = IERC20WithBurn(addresses.tokenB).balanceOf( address(this) ); // transfer token A and B from this contract to the rdpxV2Core + if (tokenABalance > 0) IERC20WithBurn(addresses.tokenA).safeTransfer( addresses.rdpxV2Core, tokenABalance ); + if(tokenBBalance > 0) IERC20WithBurn(addresses.tokenB).safeTransfer( addresses.rdpxV2Core, tokenBBalance ); emit LogAssetsTransfered(msg.sender, tokenABalance, tokenBBalance); }
QA4. UniV3LiquidityAmo.recoverERC20() fails to call IRdpxV2Core(rdpxV2Core).sync(), as a result, the balance of some reserve tokens in rdpxV2Core might not be synced after the calling of recoverERC20()
.
Mitigation: We need to call IRdpxV2Core(rdpxV2Core).sync():
function recoverERC20( address tokenAddress, uint256 tokenAmount ) external onlyRole(DEFAULT_ADMIN_ROLE) { // Can only be triggered by owner or governance, not custodian // Tokens are sent to the custodian, as a sort of safeguard TransferHelper.safeTransfer(tokenAddress, rdpxV2Core, tokenAmount); + IRdpxV2Core(rdpxV2Core).sync(); emit RecoveredERC20(tokenAddress, tokenAmount); }
QA5. The ReLPContract.reLP() function has the following problems.
First, it lacks a slippage control when calling IUniswapV2Router(addresses.ammRouter).addLiquidity()
:
Second, it fails to send the dust tokenB to addresses.rdpxV2Core
(only tokenA is considered) when there is any:
Both of them are necessary to correct the function.
QA6. An empty symbol reserve asset is added in the constructor, but two other data structures are not revised, including reserveTokens
and reservesIndex
.
We need to revise reserveTokens
and reservesIndex
as well to be consistent when add a new asset to reserve:
constructor(address _weth) { _setupRole(DEFAULT_ADMIN_ROLE, msg.sender); weth = _weth; // add Zero asset to reserveAsset ReserveAsset memory zeroAsset = ReserveAsset({ tokenAddress: address(0), tokenBalance: 0, tokenSymbol: "ZERO" }); reserveAsset.push(zeroAsset); putOptionsRequired = true; + reserveTokens.push(_assetSymbol); + reservesIndex[_assetSymbol] = reserveAsset.length - 1; }
QA7. The emergency withdraw for RdpxDecayingBonds is supposed to send the tokens to To
rather than msg.sender
according to the NATSpec.
function emergencyWithdraw( address[] calldata tokens, bool transferNative, address payable to, uint256 amount, uint256 gas ) external onlyRole(DEFAULT_ADMIN_ROLE) { _whenPaused(); if (transferNative) { (bool success, ) = to.call{ value: amount, gas: gas }(""); require(success, "RdpxReserve: transfer failed"); } IERC20WithBurn token; for (uint256 i = 0; i < tokens.length; i++) { token = IERC20WithBurn(tokens[i]); - token.safeTransfer(msg.sender, token.balanceOf(address(this))); + token.safeTransfer(to, token.balanceOf(address(this))); } }
QA8. The implementation of RdpxDecayingBonds.decreaseAmount() is not consistent with the name and the NatSpec. It sets the new amount of bonds[bondId].rdpxAmount
instead of using amount
as the amount to decrease.
Mitigation:
function decreaseAmount( uint256 bondId, uint256 amount ) public onlyRole(RDPXV2CORE_ROLE) { _whenNotPaused(); - bonds[bondId].rdpxAmount = amount; + bonds[bondId].rdpxAmount -= amount; }
QA9. The PerpetualAtlanticVault.payFunding() might be subject to reentrancy attack since it calls the transfer function first before setting fundingPaidFor[pointer] = true.
The mitigation should be like the following:
function provideFunding() external onlyRole(DEFAULT_ADMIN_ROLE) returns (uint256 fundingAmount) { _whenNotPaused(); uint256 pointer = IPerpetualAtlanticVault(addresses.perpetualAtlanticVault) .latestFundingPaymentPointer(); _validate(fundingPaidFor[pointer] == false, 16); + fundingPaidFor[pointer] = true; fundingAmount = IPerpetualAtlanticVault(addresses.perpetualAtlanticVault) .payFunding(); reserveAsset[reservesIndex["WETH"]].tokenBalance -= fundingAmount; - fundingPaidFor[pointer] = true; emit LogProvideFunding(pointer, fundingAmount); }
#0 - c4-pre-sort
2023-09-10T11:59:29Z
bytes032 marked the issue as sufficient quality report
#1 - GalloDaSballo
2023-10-10T11:35:09Z
1 -3
2 L
3 I
4 L
5 L
6 I
7 L
8 L
9 -3
5L -6
#2 - c4-judge
2023-10-20T10:21:55Z
GalloDaSballo marked the issue as grade-b