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: 27/189
Findings: 5
Award: $791.14
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 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
Calling recoverERC721()
on the UniV3 AMO will lock the position indefinitely.
Because the position NFT is sent to the core contract but the core contract doesn't have any function to allow transfer of such NFT and or burn it. Resulting in the liquidity being lock forever.
In recoverERC721()
the NFT is sent to the core contract on line 330.
But in the core contract there is no function to interact with.
While the function is only callable by the owner, it seems unlikely that the idea behind it is to lock the NFT and liquidity forever.
Manual review.
Add a function to transfer back the NFT on the core contract or send the NFT to the msg.sender
or a to
address when calling recoverERC721()
.
ERC721
#0 - c4-pre-sort
2023-09-09T06:43:12Z
bytes032 marked the issue as duplicate of #106
#1 - c4-pre-sort
2023-09-12T06:10:12Z
bytes032 marked the issue as sufficient quality report
#2 - c4-pre-sort
2023-09-12T06:12:27Z
bytes032 marked the issue as duplicate of #935
#3 - c4-judge
2023-10-20T18:05:20Z
GalloDaSballo marked the issue as satisfactory
#4 - c4-judge
2023-10-20T18:05:30Z
GalloDaSballo changed the severity to 3 (High Risk)
🌟 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
When calling deposit()
the shares to be received are calculated before calling updateFunding()
which result in the new depositor earning funding even tho it just deposited, at the expense of older depositors.
In the deposit()
function, the shares
to be received are calculated by calling previewDeposit()
which doesn't account for the funding to be received.
So then the function calls perpetualAtlanticVault.updateFunding()
right after which will result in the shares value to increase as some WETH will be sent from the vault contract to the LP contract.
A new depositor will instantly receive the rewards like other depositors since the last updateFunding()
call even tho it just deposited.
One could monitor funding to be sent and deposit()
then redeem()
every time it's worth the gas price resulting in no increase of the LP liquidity and less rewards for actual liquidity providers.
Simple POC:
This is essentially the same as testFunding()
but we assert that new depositor would get more shares if depositing before than after updateFunding()
.
Can be copy pasted in tests/perp-vault/Unit.t.sol:Unit.
function testFundingSharesIncrease() public { // mint 1 weth to address 1, address 1 LPs 1 weth, rdpxV2Core purchases 1 option, update price to <strike, calculate funding weth.mint(address(1), 1 ether); deposit(1 ether, address(1)); vault.purchase(1 ether, address(this)); priceOracle.updateRdpxPrice(0.014 gwei); skip(86500); // skip to expiry uint256[] memory strikes = new uint256[](1); strikes[0] = 0.015 gwei; //check share price before paying funding uint sharesBefore = vaultLp.previewDeposit(1 ether); vault.payFunding(); vault.updateFunding(); //check share price after paying funding uint sharesAfter = vaultLp.previewDeposit(1 ether); //Assert that depositing before would give user more shares assertGt(sharesBefore, sharesAfter); }
Manual review.
Call updateFunding()
first in the deposit()
function so share value is updated before being calculated.
ERC4626
#0 - c4-pre-sort
2023-09-07T13:26:07Z
bytes032 marked the issue as primary issue
#1 - c4-pre-sort
2023-09-07T13:26:15Z
bytes032 marked the issue as high quality report
#2 - c4-pre-sort
2023-09-07T13:42:33Z
bytes032 marked the issue as duplicate of #867
#3 - c4-judge
2023-10-20T19:23:12Z
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.1468 USDC - $0.15
https://github.com/code-423n4/2023-08-dopex/blob/eb4d4a201b3a75dd4bddc74a34e9c42c71d0d12f/contracts/core/RdpxV2Core.sol#L975 https://github.com/code-423n4/2023-08-dopex/blob/eb4d4a201b3a75dd4bddc74a34e9c42c71d0d12f/contracts/core/RdpxV2Core.sol#L995 https://github.com/code-423n4/2023-08-dopex/blob/eb4d4a201b3a75dd4bddc74a34e9c42c71d0d12f/contracts/core/RdpxV2Core.sol#L941
totalWethDelegated
is not updated on withdraw()
but is being used to compute the WETH balance of the core contract when calling sync()
.
This can result in the internal balance being less than the real balance and revert when the WETH are used and substracted by different functions of the core contract.
When calling addToDelegate
the function adds the WETH deposited to totalWethDelegated
but not to the internal balance as these WETH are not technically available for the core contract, they're waiting for another user to pair RDPX with.
Thus when we call sync()
the totalWethDelegated
is substracted from the WETH balance to limit the core contract to only WETH that have actually been used for bonding.
But because it is not updated in withdraw
the next time someone call sync()
, the internal balance will be smaller than it actually is.
Having a smaller balance will impact the abilities of the protocol to use different functions such as:
lowerDepeg()
could revert as the _wethAmount
substracted might be bigger than the internal balance on line 1110.provideFunding()
could revert as the funding amount could be bigger than the internal balance on line 803.sync()
itself as with time or if an attacker keep delegating then withdrawing the totalWethDelegated
could become bigger than the WETH balance itself and revert on line 1002. This would impact the Uniswapv3 AMO as the function _sendTokensToRdpxV2Core() relies on it, as well as the reLP contract in the function reLP() it calls sync()
at the end. Having the relp()
revert will cause bonding to revert if the core contract has the isReLPActive
variable set to true
.POC:
We show how sync()
would revert if we delegate then withdraw WETH so the totalWethDelegated
cause an underflow.
This test can be copy pasted in tests/rdpxV2-core/Unit.t.sol:Unit.
function testTotalWethDelegated() public { //delegate tokens to the core contract uint id = rdpxV2Core.addToDelegate(10 ether, 5e8); //totalWethDelegated was updated assertEq(rdpxV2Core.totalWethDelegated(), 10 ether); //we can call sync since 10 - 10 = 0 so no revert rdpxV2Core.sync(); //Check WETH internal balance is still 0 (, uint balance, string memory symbol) = rdpxV2Core.reserveAsset(2); assertEq(symbol, "WETH"); assertEq(balance, 0); //now we withdraw our WETH rdpxV2Core.withdraw(id); //totalWethDelegated was NOT updated assertEq(rdpxV2Core.totalWethDelegated(), 10 ether); //If we try to call sync it reverts with underflow vm.expectRevert(stdError.arithmeticError); rdpxV2Core.sync(); }
Manual review.
Update the totalWethDelegated
in withdraw()
.
amountWithdrawn = delegate.amount - delegate.activeCollateral; _validate(amountWithdrawn > 0, 15); delegate.amount = delegate.activeCollateral; totalWethDelegated -= amountWithdrawn; IERC20WithBurn(weth).safeTransfer(msg.sender, amountWithdrawn);
Error
#0 - bytes032
2023-09-07T07:35:13Z
Says: But because it is not updated in withdraw the next time someone call sync(), the internal balance will be smaller than it actually is
But doesn't mention why that happens
#1 - c4-pre-sort
2023-09-07T07:35:19Z
bytes032 marked the issue as sufficient quality report
#2 - c4-pre-sort
2023-09-07T07:35:23Z
bytes032 marked the issue as duplicate of #2186
#3 - c4-judge
2023-10-20T17:52:55Z
GalloDaSballo marked the issue as satisfactory
#4 - c4-judge
2023-10-20T17:55:32Z
GalloDaSballo changed the severity to 2 (Med Risk)
#5 - c4-judge
2023-10-21T07:38:54Z
GalloDaSballo changed the severity to 3 (High Risk)
491.258 USDC - $491.26
Judge has assessed an item in Issue #2142 as 2 risk. The relevant finding follows:
LOW1: No _whenNotPaused() in redeem() Technical Details Almost all state changing functions have _whenNotPaused() in the core contract but it is not the case for redeem().
The NFT it interact with has a pause/unpause functionnality so the function will revert if the NFT is paused thus the impact seems low.
Impact User could still redeem even tho the core contract is paused.
Recommendation Add _whenNotPaused() to the function.
#0 - c4-judge
2023-10-24T07:12:44Z
GalloDaSballo marked the issue as duplicate of #6
#1 - c4-judge
2023-10-24T07:12:55Z
GalloDaSballo marked the issue as satisfactory
🌟 Selected for report: c3phas
Also found by: 0xgrbr, HHK, LeoS, QiuhaoLi, Sathish9098, __141345__, flacko, oakcobalt, pontifex
101.0486 USDC - $101.05
reserveTokens
is not neededIn the core contract, the storage array reserveTokens
is not needed as the token symbols are being stored in the reserveAsset
array.
Remove reserveTokens
and update the function removeAssetFromtokenReserves()
to use the reserveAsset
array.
minAmount
is set then no need to compute minOut
.In the function _curveSwap()
of the core contract. minOut
is always computed although it won't be use if minAmount
was set in the params.
Add an if statement to not compute minOut
if minAmount > 0
.
_calculateAmounts()
ratioIn the function _calculateAmounts()
of the core contract, the amount1
is calculated by recomputing the rdpx value and then finding the ratio of the bond amount.
But because the ratio of ETH and RDPX is a constant 75/25, we could just take 25% of the amount (rdpx part) and then apply the delegate fee.
Remove the oracle call and replace the amount1
calculation with:
// amount required for delegatee amount1 = _amount * RDPX_RATIO_PERCENTAGE / (100 * DEFAULT_PRECISION) // account for delegate fee amount1 = (amount1 * (100e8 - _delegateFee)) / 1e10;
_whenNotPaused()
not needed in mint()
In the mint()
function of the rDpxDecayingBond, the check _whenNotPaused()
is not needed as the internal mint function will call beforeTokenTransfer()
which has this check already.
Remove _whenNotPaused()
from the function.
In the UniV3 AMO contract there is positions_array
and a positions_mapping
. Because the mapping is only used to link a tokenId
to a Position
we could save gas by just saving the index of the positions_array
as a simple mapping(uint256 => uint256)
.
Use a mapping(uint256 => uint256)
instead of mapping(uint256 => Position)
.
roundingPrecision
could be constant
In the PerpetualVault contract the variable roundingPrecision
could be constant
as it's never updated.
Set the variable to constant
.
currentPrice
to calculatePremium()
instead of 0
In the function purchase()
of the perpetualVault, the call to calculatePremium()
could cost less gas by passing the variable currentPrice
instead of 0
as this will force the function to call the oracle again.
Pass currentPrice
instead of 0
.
positionId
from the OptionPosition
struct.In the perpetualVault, the OptionPosition
struct saves the optionId
.
But because it is only used with a mapping that already store the id as key, saving it in the struct is useless.
Remove positionId
from the OptionPosition
struct.
#0 - c4-pre-sort
2023-09-10T11:28:42Z
bytes032 marked the issue as sufficient quality report
#1 - GalloDaSballo
2023-10-10T18:52:08Z
reserveTokens
is not neededR
minAmount
is set then no need to compute minOut
.R
_calculateAmounts()
ratioNC
_whenNotPaused()
not needed in mint()
L
NC
roundingPrecision
could be constant
L
currentPrice
to calculatePremium()
instead of 0
L
positionId
from the OptionPosition
struct.NC
3L 2R 3NC
#2 - c4-judge
2023-10-20T10:19:33Z
GalloDaSballo marked the issue as grade-b