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: 22/189
Findings: 6
Award: $954.93
🌟 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/core/RdpxV2Core.sol#L764 https://github.com/code-423n4/2023-08-dopex/blob/main/contracts/perp-vault/PerpetualAtlanticVault.sol#L315 https://github.com/code-423n4/2023-08-dopex/blob/main/contracts/perp-vault/PerpetualAtlanticVaultLP.sol#L199
Attacker can deny the protocol from settle()
put options. dpxETH/ETH will de-peg.
settle()
is an admin only function, called when the price of rPDX falls below certain level.
settle()
from RdpxV2Core calls settle()
on PerpetualAtlanticVault. The losses are compensated with WETH from PerpetualAtlanticVaultLP. rPDx is transferred to PerpetualAtlanticVaultLP. And internal accounting will be updated on PerpetualAtlanticVaultLP.
collateralToken.safeTransferFrom( addresses.perpetualAtlanticVaultLP, addresses.rdpxV2Core, ethAmount ); // Transfer rdpx from rdpx rdpxV2Core to perpetual vault IERC20WithBurn(addresses.rdpx).safeTransferFrom( addresses.rdpxV2Core, addresses.perpetualAtlanticVaultLP, rdpxAmount ); IPerpetualAtlanticVaultLP(addresses.perpetualAtlanticVaultLP).subtractLoss( ethAmount ); IPerpetualAtlanticVaultLP(addresses.perpetualAtlanticVaultLP) .unlockLiquidity(ethAmount); IPerpetualAtlanticVaultLP(addresses.perpetualAtlanticVaultLP).addRdpx( rdpxAmount
Let's take a look at subtractLoss()
, it requires actual balance to be exactly _totalCollateral
- loss
. Or else the entire function call would revert.
function subtractLoss(uint256 loss) public onlyPerpVault { require( collateral.balanceOf(address(this)) == _totalCollateral - loss, "Not enough collateral was sent out" ); _totalCollateral -= loss; }
Malicious user can send PerpetualAtlanticVaultLP as little as 1 wei to create a conflict between actual balance and internal accounting. None of the put options can be settled and deny the protocol from increasing proper amount of backing in WETH and lead to de-peg.
Manual Review
It is almost impossible for the actual balance to exactly match up with internal accounting.
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-09T10:00:15Z
bytes032 marked the issue as duplicate of #619
#1 - c4-pre-sort
2023-09-11T16:15:08Z
bytes032 marked the issue as sufficient quality report
#2 - c4-judge
2023-10-20T19:35:24Z
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
https://github.com/code-423n4/2023-08-dopex/blob/main/contracts/perp-vault/PerpetualAtlanticVaultLP.sol#L118 https://github.com/code-423n4/2023-08-dopex/blob/main/contracts/perp-vault/PerpetualAtlanticVaultLP.sol#L269 https://github.com/code-423n4/2023-08-dopex/blob/main/contracts/perp-vault/PerpetualAtlanticVaultLP.sol#L274 https://github.com/code-423n4/2023-08-dopex/blob/main/contracts/perp-vault/PerpetualAtlanticVaultLP.sol#L190 https://github.com/code-423n4/2023-08-dopex/blob/main/contracts/perp-vault/PerpetualAtlanticVault.sol#L462
Deposit()
in PerpetualAtlanticVaultLP will mint additional shares to users.
Let's take a look at deposit()
. Sanity checks shares
has to be greater than 0 by previewDeposit()
.
require((shares = previewDeposit(assets)) != 0, "ZERO_SHARES");
The function will then trigger perpetualAtlanticVault.updateFunding()
, which updates the states in PerpetualAtlanticVaultLP, specifically _totalCollateral
is added with new proceeds.
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 ); }
function addProceeds(uint256 proceeds) public onlyPerpVault { require( collateral.balanceOf(address(this)) >= _totalCollateral + proceeds, "Not enough collateral token was sent" ); _totalCollateral += proceeds; }
However, _totalCollateral
was used to calculate shares in convertToShares()
before the state changes.
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); }
Function proceeds to mint shares
calculated before state changes. This will lead to users receiving extra shares.
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); }
Manual Review
Update all state changes before previewDeposit()
function deposit( uint256 assets, address receiver ) public virtual returns (uint256 shares) { // Check for rounding error since we round down in previewDeposit. + perpetualAtlanticVault.updateFunding(); - require((shares = previewDeposit(assets)) != 0, "ZERO_SHARES"); - perpetualAtlanticVault.updateFunding(); + require((shares = previewDeposit(assets)) != 0, "ZERO_SHARES"); //@audit check return value // 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); }
Invalid Validation
#0 - c4-pre-sort
2023-09-07T13:48:24Z
bytes032 marked the issue as duplicate of #867
#1 - c4-pre-sort
2023-09-11T09:06:45Z
bytes032 marked the issue as sufficient quality report
#2 - c4-judge
2023-10-20T19:26:40Z
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/main/contracts/core/RdpxV2Core.sol#L941 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#L819 https://github.com/code-423n4/2023-08-dopex/blob/main/contracts/core/RdpxV2Core.sol#L995 https://github.com/code-423n4/2023-08-dopex/blob/main/contracts/core/RdpxV2Core.sol#L790 https://github.com/code-423n4/2023-08-dopex/blob/main/contracts/core/RdpxV2Core.sol#L1080
bondWithDelegate()
, provideFunding()
and lowerDepeg()
can be denied by malicious users.
All the WETH in Rdpxv2core belong in the 2 following categories: reserveAsset[reservesIndex["WETH"]].tokenBalance
and totalWethDelegated
.
Users who do not have both WETH and rDPX can provide WETH by addToDelegate()
. toalWethDelegated
is updated here:
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;
However, when withdraw()
sends back un-delegated WETH, totalWethDelegated
is not properly deducted.
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; IERC20WithBurn(weth).safeTransfer(msg.sender, amountWithdrawn); emit LogDelegateWithdraw(delegateId, amountWithdrawn); }
This creates an issue whether malicious users or not. One can repeatedly flash loan addToDelegate()
and withdraw()
in one transaction, so totalWethDelegated = type(uint256).max
. No other user can provide WETH to delegate. The protocol will be left with empty delegatePosition
.
A different attack vector would be for the attacker to call sync()
after flash loan, so reserveAsset[reservesIndex["WETH"]].tokenBalance
can be reduced to 0. This would disable 2 admin only functions, lowerDepeg()
and provideFunding()
which can break the entire system. Considering the low gas fee on Arbitrum, this attack can be achieved with little to no cost.
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(); }
Add 2 following PoC to unit test:
address user1 = makeAddr("user1"); address user2 = makeAddr("user2"); function testAddToDelegatesDoS() public{ //@Note Mock token does not allow minting type(uint256).max //@Note totalWethDelegated on RdpxV2Core needs to be changed to uint128 to demonstrate this attack (same idea applies). //Fund the users weth.mint(user1, type(uint128).max); weth.mint(user2, 100e18); //check total WETH delegated before == 0 uint256 totalWethDelegatedBefore = rdpxV2Core.totalWethDelegated(); console.logUint(totalWethDelegatedBefore); //user1 flash loan add and withdraw in one transaction vm.startPrank(user1); weth.approve(address(rdpxV2Core), type(uint128).max); uint256 delegateId = rdpxV2Core.addToDelegate(type(uint128).max, 1e8); rdpxV2Core.withdraw(delegateId); vm.stopPrank(); //check totalWethDelegated after uint256 totalWethDelegatedAfter = rdpxV2Core.totalWethDelegated(); console.logUint(totalWethDelegatedAfter); //user2 tries to add WETH to delegate vm.startPrank(user2); weth.approve(address(rdpxV2Core), 100e18); vm.expectRevert(); rdpxV2Core.addToDelegate(100e18, 1e8); } function testDosProvideFunding() public { //@note add "address admin = makeAddr("admin");" in Setup.t.sol //@note add "rdpxV2Core.grantRole(rdpxV2Core.DEFAULT_ADMIN_ROLE(), admin);" in Setup.t.sol //Fund users weth.mint(user1, type(uint128).max); weth.mint(user2, type(uint128).max); rdpx.mint(user1, 100e18); //User1 calls bond so there is some WETH reserve vm.startPrank(user1); weth.approve(address(rdpxV2Core), 10e18); rdpx.approve(address(rdpxV2Core), 10e18); rdpxV2Core.bond(5e18, 0, user1); vm.stopPrank(); //Check WETH reserve ( ,uint256 wethBalance, ) = rdpxV2Core.getReserveTokenInfo("WETH"); console.logUint(wethBalance); //User2 flash loans and calls sync vm.startPrank(user2); weth.approve(address(rdpxV2Core), type(uint128).max); uint256 delegateId = rdpxV2Core.addToDelegate(1225000000000000000, 1e8); rdpxV2Core.withdraw(delegateId); rdpxV2Core.sync(); vm.stopPrank(); //Check WETH reserve after sync() ( ,uint256 wethBalanceAfterSync, ) = rdpxV2Core.getReserveTokenInfo("WETH"); console.logUint(wethBalanceAfterSync); //Admin tries to provide funding but should revert since WETH reserve is 0 vm.startPrank(admin); weth.approve(address(vault), type(uint128).max); vm.expectRevert(); rdpxV2Core.provideFunding(); vm.stopPrank(); }
Manual Review and Foundry
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); }
DoS
#0 - c4-pre-sort
2023-09-08T14:13:50Z
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-20T17:57:01Z
GalloDaSballo marked the issue as satisfactory
#3 - c4-judge
2023-10-21T07:38:54Z
GalloDaSballo changed the severity to 3 (High Risk)
673.8793 USDC - $673.88
https://github.com/code-423n4/2023-08-dopex/blob/main/contracts/core/RdpxV2Core.sol#L1156 https://github.com/code-423n4/2023-08-dopex/blob/main/contracts/core/RdpxV2Core.sol#L471 https://github.com/code-423n4/2023-08-dopex/blob/main/contracts/perp-vault/PerpetualAtlanticVault.sol#L255 https://github.com/code-423n4/2023-08-dopex/blob/main/contracts/perp-vault/PerpetualAtlanticVault.sol#L502 https://github.com/code-423n4/2023-08-dopex/blob/main/contracts/perp-vault/PerpetualAtlanticVault.sol#L462
Inconsistent premium
calculations in bond()
and bondWithDelegate()
. This can cause discrepancy in assets required for bonding.
When putOptionsRequired
is toggled to true, premium
is calculated in bond()
and bondWithDelegate()
.
Let's take a look at bond()
(same issue in bondWithDelegate()
):
bond()
calls calculateBondCost()
in order to calculate rdpxRequired
and wethRequired
. premium
is calculated from strike
and timeToExpiry
on PerpetualAtlanticVault. We want to focus specifically on timeToExpiry
, since the state on PerpetualAtlanticVault has not changed yet.
uint256 strike = IPerpetualAtlanticVault(addresses.perpetualAtlanticVault) .roundUp(rdpxPrice - (rdpxPrice / 4)); // 25% below the current price uint256 timeToExpiry = IPerpetualAtlanticVault( addresses.perpetualAtlanticVault ).nextFundingPaymentTimestamp() - block.timestamp; if (putOptionsRequired) { wethRequired += IPerpetualAtlanticVault(addresses.perpetualAtlanticVault) .calculatePremium(strike, rdpxRequired, timeToExpiry, 0); }
User will then send in wethRequired
. bond()
will then trigger _purchaseOptions()
which calls IPerpetualAtlanticVault.purchase()
. updateFunding()
is called:
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 ); }
updateFundingPaymentPointer()
will update latestFundingPaymentPointer
and this value is used to calculate timeToExpiry
.
while (block.timestamp >= nextFundingPaymentTimestamp()) { if (lastUpdateTime < nextFundingPaymentTimestamp()) { uint256 currentFundingRate = fundingRates[latestFundingPaymentPointer]; uint256 startTime = lastUpdateTime == 0 ? (nextFundingPaymentTimestamp() - fundingDuration) : lastUpdateTime; lastUpdateTime = nextFundingPaymentTimestamp(); collateralToken.safeTransfer( addresses.perpetualAtlanticVaultLP, (currentFundingRate * (nextFundingPaymentTimestamp() - startTime)) / 1e18 ); IPerpetualAtlanticVaultLP(addresses.perpetualAtlanticVaultLP) .addProceeds( (currentFundingRate * (nextFundingPaymentTimestamp() - startTime)) / 1e18 ); emit FundingPaid( msg.sender, ((currentFundingRate * (nextFundingPaymentTimestamp() - startTime)) / 1e18), latestFundingPaymentPointer ); } latestFundingPaymentPointer += 1; emit FundingPaymentPointerUpdated(latestFundingPaymentPointer); }
premium
calculated before latestFundingPaymentPointer
state change will be different from after due to 2 different timeToExpiry
values. This can result in discrepancy for both wethRequired
and rdpxRequired
.
Manual Review
Update all changes on PerpetualAtlanticVault before calculating premium
.
function bond( uint256 _amount, uint256 rdpxBondId, address _to ) public returns (uint256 receiptTokenAmount) { _whenNotPaused(); // Validate amount _validate(_amount > 0, 4); + IPerpetualAtlanticVault( + addresses.perpetualAtlanticVault + ).updateFunding(); // Compute the bond cost (uint256 rdpxRequired, uint256 wethRequired) = calculateBondCost( _amount, rdpxBondId ); ... }
function bondWithDelegate( address _to, uint256[] memory _amounts, uint256[] memory _delegateIds, uint256 rdpxBondId ) public returns (uint256 receiptTokenAmount, uint256[] memory) { _whenNotPaused(); // Validate amount _validate(_amounts.length == _delegateIds.length, 3); + IPerpetualAtlanticVault( + addresses.perpetualAtlanticVault + ).updateFunding(); ... }
Context
#0 - c4-pre-sort
2023-09-09T06:13:04Z
bytes032 marked the issue as primary issue
#1 - c4-pre-sort
2023-09-12T06:33:35Z
bytes032 marked the issue as duplicate of #237
#2 - c4-pre-sort
2023-09-14T09:29:48Z
bytes032 marked the issue as not a duplicate
#3 - c4-pre-sort
2023-09-15T08:51:43Z
bytes032 marked the issue as duplicate of #761
#4 - c4-judge
2023-10-20T12:02:57Z
GalloDaSballo marked the issue as not a duplicate
#5 - GalloDaSballo
2023-10-20T12:03:07Z
TODO: Dups of Wrong Math for "normal" vs "with delegate"
#6 - c4-judge
2023-10-20T19:13:44Z
GalloDaSballo marked the issue as duplicate of #2187
#7 - c4-judge
2023-10-20T19:13:51Z
GalloDaSballo marked the issue as partial-50
#8 - GalloDaSballo
2023-10-20T19:13:56Z
Harder to understand than the main finding
#9 - c4-judge
2023-10-21T07:51:40Z
GalloDaSballo changed the severity to 2 (Med Risk)
238.7514 USDC - $238.75
https://github.com/code-423n4/2023-08-dopex/blob/main/contracts/perp-vault/PerpetualAtlanticVaultLP.sol#L145 https://github.com/code-423n4/2023-08-dopex/blob/main/contracts/perp-vault/PerpetualAtlanticVaultLP.sol#L218 https://github.com/code-423n4/2023-08-dopex/blob/main/contracts/perp-vault/PerpetualAtlanticVault.sol#L315
In a black swan event where price of rPDX drops significantly. The protocol will settle()
put options and increase WETH backing from PerpetualAtlanticVaultLP. Users who hold shares PerpetaulAtlanticVault LP Token will not be able to redeem()
.
Let's take a look at settle()
. When rPDX price falls significantly, protocol increases backing by sending WETH to RdpxV2Core from PerpetaulAtlanticVault. In turn, rDPX is sent to PerpetaulAtlanticVault from RdpxV2Core.
// Transfer collateral token from perpetual vault to rdpx rdpxV2Core collateralToken.safeTransferFrom( addresses.perpetualAtlanticVaultLP, addresses.rdpxV2Core, ethAmount ); // Transfer rdpx from rdpx rdpxV2Core to perpetual vault IERC20WithBurn(addresses.rdpx).safeTransferFrom( addresses.rdpxV2Core, addresses.perpetualAtlanticVaultLP, rdpxAmount ); IPerpetualAtlanticVaultLP(addresses.perpetualAtlanticVaultLP).subtractLoss( ethAmount ); IPerpetualAtlanticVaultLP(addresses.perpetualAtlanticVaultLP) .unlockLiquidity(ethAmount); IPerpetualAtlanticVaultLP(addresses.perpetualAtlanticVaultLP).addRdpx( rdpxAmount );
This can create a scenario where PerpetaulAtlanticVault only holds rPDX and no WETH. Users who try to redeem LP Token will be denied. Require statement checks assets
received cannot be 0. However, _convertToAssets()
will return 0 for assets
for two reasons:
assets
rounds down to 0.function redeem( uint256 shares, address receiver, address owner ) public returns (uint256 assets, uint256 rdpxAmount) { perpetualAtlanticVault.updateFunding(); if (msg.sender != owner) { uint256 allowed = allowance[owner][msg.sender]; // Saves gas for limited approvals. if (allowed != type(uint256).max) { allowance[owner][msg.sender] = allowed - shares; } } (assets, rdpxAmount) = redeemPreview(shares); // Check for rounding error since we round down in previewRedeem. require(assets != 0, "ZERO_ASSETS"); _rdpxCollateral -= rdpxAmount; beforeWithdraw(assets, shares); _burn(owner, shares); collateral.transfer(receiver, assets); IERC20WithBurn(rdpx).safeTransfer(receiver, rdpxAmount); emit Withdraw(msg.sender, receiver, owner, assets, shares); }
Manual Review
Consider living with some insignificant rounding error.
function redeem( uint256 shares, address receiver, address owner ) public returns (uint256 assets, uint256 rdpxAmount) { perpetualAtlanticVault.updateFunding(); if (msg.sender != owner) { uint256 allowed = allowance[owner][msg.sender]; // Saves gas for limited approvals. if (allowed != type(uint256).max) { allowance[owner][msg.sender] = allowed - shares; } } (assets, rdpxAmount) = redeemPreview(shares); // Check for rounding error since we round down in previewRedeem. - require(assets != 0, "ZERO_ASSETS"); + require(assets != 0 || rdpxAmount != 0, "ZERO_ASSETS/ZERO_RDPXAMOUNT"); _rdpxCollateral -= rdpxAmount; beforeWithdraw(assets, shares); _burn(owner, shares); collateral.transfer(receiver, assets); IERC20WithBurn(rdpx).safeTransfer(receiver, rdpxAmount); emit Withdraw(msg.sender, receiver, owner, assets, shares); }
Invalid Validation
#0 - c4-pre-sort
2023-09-15T05:47:42Z
bytes032 marked the issue as duplicate of #750
#1 - c4-judge
2023-10-15T18:02:58Z
GalloDaSballo marked the issue as satisfactory
🌟 Selected for report: degensec
Also found by: 0x3b, 0xnev, HChang26, KmanOfficial, QiuhaoLi, T1MOH, WoolCentaur, Yanchuan, ayden, bart1e, jasonxiale, kutugu, mert_eren, nirlin, peakbolt, peanuts, pep7siup, qpzm, tapir, ubermensch, wintermute
24.8267 USDC - $24.83
https://github.com/code-423n4/2023-08-dopex/blob/main/contracts/reLP/ReLPContract.sol#L202
Dust amount of tokenB
will be locked in ReLPContract.
reLP()
can only be called by protocol admin only. Based on sponsor:
reLP basically remove a portion of the LP tokens owned by the AMO and swaps half the eth to rdpx and adds liquidity using the new ratio. After the swap the remaining assets gets sent back to core contract.
After adding liquidity back into UniswapV2, LP token and tokenA
are sent back to rdpxV2Core
. However, there is potentially unused tokenB
. Based on current implementation, there is no way to retrieve unused tokenB
in ReLPContract.
(, , uint256 lp) = IUniswapV2Router(addresses.ammRouter).addLiquidity( addresses.tokenA, addresses.tokenB, tokenAAmountOut, amountB / 2, 0, 0, address(this), block.timestamp + 10 ); // transfer the lp to the amo IERC20WithBurn(addresses.pair).safeTransfer(addresses.amo, lp); IUniV2LiquidityAmo(addresses.amo).sync(); // transfer rdpx to rdpxV2Core IERC20WithBurn(addresses.tokenA).safeTransfer( addresses.rdpxV2Core, IERC20WithBurn(addresses.tokenA).balanceOf(address(this)) ); IRdpxV2Core(addresses.rdpxV2Core).sync();
Manual Review
(, , uint256 lp) = IUniswapV2Router(addresses.ammRouter).addLiquidity( addresses.tokenA, addresses.tokenB, tokenAAmountOut, amountB / 2, 0, 0, address(this), block.timestamp + 10 ); // transfer the lp to the amo IERC20WithBurn(addresses.pair).safeTransfer(addresses.amo, lp); IUniV2LiquidityAmo(addresses.amo).sync(); // transfer rdpx to rdpxV2Core IERC20WithBurn(addresses.tokenA).safeTransfer( addresses.rdpxV2Core, IERC20WithBurn(addresses.tokenA).balanceOf(address(this)) ); + IERC20WithBurn(addresses.tokenB).safeTransfer( + addresses.rdpxV2Core, + IERC20WithBurn(addresses.tokenB).balanceOf(address(this)) + ); IRdpxV2Core(addresses.rdpxV2Core).sync();
Uniswap
#0 - c4-pre-sort
2023-09-09T17:30:50Z
bytes032 marked the issue as duplicate of #1286
#1 - c4-pre-sort
2023-09-11T15:38:19Z
bytes032 marked the issue as sufficient quality report
#2 - c4-judge
2023-10-18T12:14:29Z
GalloDaSballo marked the issue as satisfactory