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: 57/189
Findings: 4
Award: $200.62
🌟 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
https://github.com/code-423n4/2023-08-dopex/blob/main/contracts/perp-vault/PerpetualAtlanticVault.sol#L359 https://github.com/code-423n4/2023-08-dopex/blob/main/contracts/perp-vault/PerpetualAtlanticVaultLP.sol#L201
PerpetualAtlanticVaultLP.subtractLoss uses strict equal to check if enough collateral was sent out following the call to PerpetualAtlanticVault.settle.
function subtractLoss(uint256 loss) public onlyPerpVault { require( collateral.balanceOf(address(this)) == _totalCollateral - loss, "Not enough collateral was sent out" );//@audit-issue _totalCollateral -= loss; }
_totalCollateral
tracks the total collateral available in the contract but does not account for values sent directly to the contract.
when making settlement in PerpetualAtlanticVault.settle
it checks that the amount transferred from perpetual vault to rdpxV2Core is accurate by calling subtractLoss
at the end of the transaction.
function settle( uint256[] memory optionIds ) external nonReentrant onlyRole(RDPXV2CORE_ROLE) returns (uint256 ethAmount, uint256 rdpxAmount) { ...SNIP // Transfer collateral token from perpetual vault to rdpx rdpxV2Core collateralToken.safeTransferFrom( addresses.perpetualAtlanticVaultLP, addresses.rdpxV2Core, ethAmount );//@audit-info ...SNIP IPerpetualAtlanticVaultLP(addresses.perpetualAtlanticVaultLP).subtractLoss( ethAmount );//@audit-issue ...SNIP emit Settle(ethAmount, rdpxAmount, optionIds); }
if an attacker sends as little as 1 wei of collateral token to perpetualAtlanticVaultLp
_totalCollateral would be out of sync with it's actual contract balance causing the call to always revert.
Adjust the following test /tests/perp-vault/Unit.t.sol#testSettle
to:
function testSettle() public { weth.mint(address(1), 1 ether); + weth.mint(address(1337), 1 wei); deposit(1 ether, address(1)); vault.purchase(1 ether, address(this)); uint256[] memory ids = new uint256[](1); ids[0] = 0; vm.expectRevert( abi.encodeWithSelector( PerpetualAtlanticVault.PerpetualAtlanticVaultError.selector, 7 ) ); vault.settle(ids); priceOracle.updateRdpxPrice(0.2 gwei); // initial price * 10 uint256[] memory strikes = new uint256[](1); strikes[0] = 0.015 gwei; skip(86500); // expire vm.expectRevert( abi.encodeWithSelector( PerpetualAtlanticVault.PerpetualAtlanticVaultError.selector, 7 ) ); vault.settle(ids); + vm.prank(address(1337)); + weth.transfer(address(vaultLp), 1 wei);//@audit attacker sends 1 wei of collateral token to the contract priceOracle.updateRdpxPrice(0.010 gwei); // ITM uint256 wethBalanceBefore = weth.balanceOf(address(this)); uint256 rdpxBalanceBefore = rdpx.balanceOf(address(this)); + vm.expectRevert("Not enough collateral was sent out");//@audit All settlement would fail vault.settle(ids); uint256 wethBalanceAfter = weth.balanceOf(address(this)); uint256 rdpxBalanceAfter = rdpx.balanceOf(address(this)); assertEq(wethBalanceAfter - wethBalanceBefore, 0.15 ether); // asset settled; 1 rdpx worth of ether received by rdpxV2Core assertEq(rdpxBalanceBefore - rdpxBalanceAfter, 1 ether); // asset settled; 1 rdpx sent from rdpxV2Core to lp }
Manual Review
Consider removing the strict equality check in subtractLoss
because the check is redundant since token transfer from perpetual vault to rdpxV2Core uses safe transfer which would actually revert if the transfer fails.
DoS
#0 - c4-pre-sort
2023-09-09T06:18:15Z
bytes032 marked the issue as duplicate of #619
#1 - c4-pre-sort
2023-09-11T16:14:03Z
bytes032 marked the issue as sufficient quality report
#2 - c4-judge
2023-10-20T19:36:34Z
GalloDaSballo marked the issue as satisfactory
🌟 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
https://github.com/code-423n4/2023-08-dopex/blob/main/contracts/amo/UniV3LiquidityAmo.sol#L324
ERC721 tokens sent to RdpxV2Core
could be stuck
ERC721 tokens can be recovered to RdpxV2Core
from Amo
contracts.
https://github.com/code-423n4/2023-08-dopex/blob/main/contracts/amo/UniV3LiquidityAmo.sol#L324
function recoverERC721( address tokenAddress, uint256 token_id ) external onlyRole(DEFAULT_ADMIN_ROLE) { // Only the owner address can ever receive the recovery withdrawal // INonfungiblePositionManager inherits IERC721 so the latter does not need to be imported INonfungiblePositionManager(tokenAddress).safeTransferFrom( address(this), rdpxV2Core, token_id ); emit RecoveredERC721(tokenAddress, token_id); }
but there is no function available to transfer these tokens to the appropriate recipient in RdpxV2Core
.
These tokens could be stuck within the contract.
Manual Review
Consider adding a functionality to transfer ERC721 tokens from RdpxV2Core
with proper access control.
Other
#0 - c4-pre-sort
2023-09-09T10:38:32Z
bytes032 marked the issue as duplicate of #106
#1 - c4-pre-sort
2023-09-12T06:10:14Z
bytes032 marked the issue as sufficient quality report
#2 - c4-pre-sort
2023-09-12T06:12:20Z
bytes032 marked the issue as duplicate of #935
#3 - c4-judge
2023-10-20T18:05:28Z
GalloDaSballo changed the severity to 3 (High Risk)
#4 - c4-judge
2023-10-20T18:05:51Z
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.0734 USDC - $0.07
https://github.com/code-423n4/2023-08-dopex/blob/main/contracts/core/RdpxV2Core.sol#L975 https://github.com/code-423n4/2023-08-dopex/blob/main/contracts/core/RdpxV2Core.sol#L1002
syncing would fail following every withdrawal.
RdpxV2Core.sync
syncs asset reserves with the contract balance, this is used to update reserveAsset.tokenBalance
which tracks the balance of asset owned by the contract.
sync may be called after balance changes to update the state of asset balance.
The issue is that when a user addToDelegate
in RdpxV2Core
, totalWethDelegated
is incremented by the amount supplied.
https://github.com/code-423n4/2023-08-dopex/blob/main/contracts/core/RdpxV2Core.sol#L941
function addToDelegate( uint256 _amount, uint256 _fee ) external returns (uint256) { ...SNIP Delegate memory delegatePosition = Delegate({ amount: _amount, fee: _fee, owner: msg.sender, activeCollateral: 0 }); delegates.push(delegatePosition); // add amount to total weth delegated totalWethDelegated += _amount;//@audit-info emit LogAddToDelegate(_amount, _fee, delegates.length - 1); return (delegates.length - 1); }
but if the user withdraw
, totalWethDelegated
is not decremented.
This causes sync
to revert since it adjust totalWethDelegated from it's balance.
https://github.com/code-423n4/2023-08-dopex/blob/main/contracts/core/RdpxV2Core.sol#L995
function sync() external { for (uint256 i = 1; i < reserveAsset.length; i++) { uint256 balance = IERC20WithBurn(reserveAsset[i].tokenAddress).balanceOf( address(this) ); if (weth == reserveAsset[i].tokenAddress) { balance = balance - totalWethDelegated;//@audit-info } reserveAsset[i].tokenBalance = balance; } emit LogSync(); }
From the AMO contracts sync
is called when adding or removing liquidity through _sendTokensToRdpxV2Core
https://github.com/code-423n4/2023-08-dopex/blob/main/contracts/amo/UniV3LiquidityAmo.sol#L361
function _sendTokensToRdpxV2Core(address tokenA, address tokenB) internal { uint256 tokenABalance = IERC20WithBurn(tokenA).balanceOf(address(this)); uint256 tokenBBalance = IERC20WithBurn(tokenB).balanceOf(address(this)); // transfer token A and B from this contract to the rdpxV2Core IERC20WithBurn(tokenA).safeTransfer(rdpxV2Core, tokenABalance); IERC20WithBurn(tokenB).safeTransfer(rdpxV2Core, tokenBBalance); // sync token balances IRdpxV2Core(rdpxV2Core).sync();//@audit this call may fail emit LogAssetsTransfered(tokenABalance, tokenBBalance, tokenA, tokenB); }
This would prevent the addition/removal of liquidity from uniswapNonfungiblePositionManager since the call to sync could revert. this would also affect any other function calls within the protocol that relies on sync
.
Sync fails after every withdraw since totalWethDelegated
deviates from the actual total WETH delegated to the contract.
This could be triggered intentionally or unintentionally by users.
A malicious user could takeout a flashloan and call addToDelegate
and also withdraw
within the same transaction to greatly inflate the value of totalWethDelegated
.
add the following line to /tests/Unit.t.sol#testWithdraw
function testWithdraw() public { rdpxV2Core.addToDelegate(1 * 1e18, 10e8); // test withdraw with invalid delegate id vm.expectRevert( abi.encodeWithSelector(RdpxV2Core.RdpxV2CoreError.selector, 14) ); rdpxV2Core.withdraw(1); // test withdraw without ownership vm.expectRevert( abi.encodeWithSelector(RdpxV2Core.RdpxV2CoreError.selector, 9) ); vm.prank(address(1), address(1)); rdpxV2Core.withdraw(0); // test withdraw successfully uint256 userBalance = weth.balanceOf(address(this)); rdpxV2Core.withdraw(0); assertEq(weth.balanceOf(address(this)), userBalance + 1 * 1e18); (, uint256 amount, , uint256 activeCollateral) = rdpxV2Core.delegates(0); assertEq(amount, 0); assertEq(activeCollateral, 0); + //@audit test sync would fail after each withdraw + vm.expectRevert();//[FAIL. Reason: Arithmetic over/underflow] + rdpxV2Core.sync(); // test withdraw with 0 amount vm.expectRevert( abi.encodeWithSelector(RdpxV2Core.RdpxV2CoreError.selector, 15) ); rdpxV2Core.withdraw(0); // test partial amount rdpxV2Core.addToDelegate(2 * 1e18, 10e8); uint256[] memory _amounts = new uint256[](1); uint256[] memory _delegateIds = new uint256[](1); _delegateIds[0] = 1; _amounts[0] = 2 * 1e18; userBalance = weth.balanceOf(address(this)); (, amount) = rdpxV2Core.calculateBondCost(2e18, 0); rdpxV2Core.bondWithDelegate(address(this), _amounts, _delegateIds, 0); rdpxV2Core.withdraw(1); assertEq(weth.balanceOf(address(this)), userBalance + (2e18 - amount)); }
Manual Review
Adjust totalWethDelegated
when delegated weth are withdrawn.
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; + // remove amount from total weth delegated + totalWethDelegated -= amountWithdrawn; IERC20WithBurn(weth).safeTransfer(msg.sender, amountWithdrawn); emit LogDelegateWithdraw(delegateId, amountWithdrawn); }
Context
#0 - c4-pre-sort
2023-09-07T08:12:41Z
bytes032 marked the issue as duplicate of #2186
#1 - c4-judge
2023-10-20T17:55:32Z
GalloDaSballo changed the severity to 2 (Med Risk)
#2 - c4-judge
2023-10-20T18:01:03Z
GalloDaSballo marked the issue as satisfactory
#3 - c4-judge
2023-10-21T07:38:55Z
GalloDaSballo changed the severity to 3 (High Risk)
#4 - c4-judge
2023-10-21T07:48:23Z
GalloDaSballo marked the issue as partial-50
🌟 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
counters used in RdpxDecayingBonds and perpetualAtlanticVault would be removed from recent openzepplin release
sync
in UniV2LiquidityAmo#_sendTokensToRdpxV2Corebalance of reserves should be updated following any changes to the contract balances https://github.com/code-423n4/2023-08-dopex/blob/main/contracts/amo/UniV2LiquidityAmo.sol#L160
hash collision could be possible due to how bytes are packed when using encodePacked.
function getReserveTokenInfo( string memory _token ) public view returns (address, uint256, string memory) { ReserveAsset memory asset = reserveAsset[reservesIndex[_token]]; _validate( keccak256(abi.encodePacked(asset.tokenSymbol)) == keccak256(abi.encodePacked(_token)), 19 );//@audit return (asset.tokenAddress, asset.tokenBalance, asset.tokenSymbol); }
use abi.encode
instead of abi.encodePacked
When interacting with AMM deadline happens to terminate a transaction which has been pending on the mempool which might have been executed on an unfavorable condition or timing. https://github.com/code-423n4/2023-08-dopex/blob/main/contracts/amo/UniV3LiquidityAmo.sol#L295
ISwapRouter.ExactInputSingleParams memory swap_params = ISwapRouter .ExactInputSingleParams( _tokenA, _tokenB, _fee_tier, address(this), 2105300114, // Expiration: a long time from now@audit invalid deadline _amountAtoB, _amountOutMinimum, _sqrtPriceLimitX96 );
Allow the Admin to pass a deadline deemed reasonable to swap
.
Within rdpxV2 there are some redundant checks which should be refactored. https://github.com/code-423n4/2023-08-dopex/blob/main/contracts/decaying-bonds/RdpxDecayingBonds.sol#L120
function mint( address to, uint256 expiry, uint256 rdpxAmount ) external onlyRole(MINTER_ROLE) {<@ _whenNotPaused(); require(hasRole(MINTER_ROLE, msg.sender), "Caller is not a minter");//@audit this is a redundant check }
other Instances: https://github.com/code-423n4/2023-08-dopex/blob/main/contracts/core/RdpxV2Core.sol#L1055 https://github.com/code-423n4/2023-08-dopex/blob/main/contracts/core/RdpxV2Core.sol#L1086
When reLping part of tokenB is swapped for tokenA and the other part readded as Liquidity, within does swaps tokenB could have leftover from the swap with could accumulate and left stuck within the reLpContract
. These leftovers could be transferred to rdpxV2Core
as well.
https://github.com/code-423n4/2023-08-dopex/blob/main/contracts/reLP/ReLPContract.sol#L290
https://github.com/code-423n4/2023-08-dopex/blob/main/contracts/core/RdpxV2Core.sol#L441
function setBondDiscount( uint256 _bondDiscountFactor ) external onlyRole(DEFAULT_ADMIN_ROLE) { _validate(_bondDiscountFactor > 0, 3); @> bondDiscountFactor = _bondDiscountFactor; emit LogSetBondDiscountFactor(_bondDiscountFactor); }
bondDiscountFactor
could be set to a value that could DoS bonding.
function calculateBondCost( uint256 _amount, uint256 _rdpxBondId ) public view returns (uint256 rdpxRequired, uint256 wethRequired) { ...SNIP if (_rdpxBondId == 0) { @> uint256 bondDiscount = (bondDiscountFactor * Math.sqrt(IRdpxReserve(addresses.rdpxReserve).rdpxReserve()) * 1e2) / (Math.sqrt(1e18)); // 1e8 precision @> _validate(bondDiscount < 100e8, 14); ..SNIP }
#0 - c4-pre-sort
2023-09-10T11:49:28Z
bytes032 marked the issue as sufficient quality report
#1 - GalloDaSballo
2023-10-10T11:48:54Z
Not sure since the contract is so simple it has no vulnerabilities
sync
in UniV2LiquidityAmo#_sendTokensToRdpxV2CoreL, no detail
-3, collision is possible but strings will not
L
L
L
OOS
4L -3
#2 - c4-judge
2023-10-20T10:21:58Z
GalloDaSballo marked the issue as grade-b