Analysis of the Curio Exploit
Learn how Curio was exploited, which resulted in a loss of approximately $16 million.
Youtube Video
Playing the video that you've selected below in an iframe
Learn how Palmswap was exploited, resulting in a loss of approximately $901,000.
On July 24, 2023, the LP Vault contract of Palmswap was exploited due to a price manipulation of the underlying assets, resulting in a loss of approximately $901,000.
Palmswap provides a liquidity-efficient, powerful, and user-friendly decentralized leverage trading platform.
The root cause of the exploit is the price manipulation of the underlying PLP token, which is determined by the number of USDT in the Vault contract and the USDP supply.
Step 1:
We attempt to analyze the attack transaction executed by the exploiter.
Step 2:
When adding or removing liquidity, the PlpManager contract calculates the price of the PLP token by calling the `getAum` function. However, purchasing USDP with the `buyUSDP` function also increases its price.
function getAumInUsdp(bool maximise)
public
view
override
returns (uint256)
{
uint256 aum = getAum(maximise);
return (aum * (10**USDP_DECIMALS)) / PRICE_PRECISION;
}
function getAum(bool maximise) public view returns (uint256) {
uint256 length = vault.allWhitelistedTokensLength();
uint256 aum = aumAddition;
IVault _vault = vault;
uint256 collateralTokenPrice = maximise
? _vault.getMaxPrice(collateralToken)
: _vault.getMinPrice(collateralToken);
uint256 collateralDecimals = _vault.tokenDecimals(collateralToken);
uint256 currentAmmDeduction = (vault.permanentPoolAmount() *
collateralTokenPrice) / (10**collateralDecimals);
aum +=
(vault.poolAmount() * collateralTokenPrice) /
(10**collateralDecimals);
bool _maximise = maximise;
for (uint256 i; i < length; i++) {
address token = vault.allWhitelistedTokens(i);
bool isWhitelisted = vault.whitelistedTokens(token);
if (!isWhitelisted || token == collateralToken) {
continue;
}
uint256 maxPrice = _vault.getMaxPrice(token);
uint256 minPrice = _vault.getMinPrice(token);
// add global profit / loss
uint256 shortSize = _vault.globalShortSizes(token);
uint256 longSize = _vault.globalLongSizes(token);
if (longSize > 0) {
(uint256 delta, bool hasProfit) = getGlobalDelta(
token,
_maximise ? minPrice : maxPrice,
longSize,
true
);
if (!hasProfit) {
// add lossees from longs
aum += delta;
} else {
currentAmmDeduction += delta;
}
}
if (shortSize > 0) {
(uint256 delta, bool hasProfit) = getGlobalDelta(
token,
_maximise ? maxPrice : minPrice,
shortSize,
false
);
if (!hasProfit) {
// add losses from shorts
aum += delta;
} else {
currentAmmDeduction += delta;
}
}
}
aum = currentAmmDeduction > aum ? 0 : aum - currentAmmDeduction;
return aumDeduction > aum ? 0 : aum - aumDeduction;
}
Step 3:
The attacker was able to manipulate the amount of USDT in the Vault by calling the `buyUSDP` function in the Vault contract, which would then invoke a call to the `_increaseUsdpAmount(mintAmount)` and `_increasePoolAmount(tokenAmount)` sub-routines.
function buyUSDP(address _receiver) external override nonReentrant returns (uint256) {
_validateManager();
_validateAddr(_receiver);
address _collateralToken = collateralToken;
useSwapPricing = true;
uint256 tokenAmount = _transferIn(_collateralToken);
_validate(tokenAmount > 0, 13);
uint256 price = getMinPrice(_collateralToken);
uint256 _usdpAmount = (tokenAmount * price) / PRICE_PRECISION;
_usdpAmount = adjustForDecimals(_usdpAmount, _collateralToken, usdp);
_validate(_usdpAmount > 0, 14);
uint256 feeBasisPoints = vaultUtils.getBuyUsdpFeeBasisPoints(_collateralToken, _usdpAmount);
uint256 amountAfterFees = _collectSwapFees(_collateralToken, tokenAmount, feeBasisPoints);
uint256 mintAmount = (amountAfterFees * price) / PRICE_PRECISION;
mintAmount = adjustForDecimals(mintAmount, _collateralToken, usdp);
_increaseUsdpAmount(mintAmount);
_increasePoolAmount(tokenAmount);
IUSDP(usdp).mint(_receiver, mintAmount);
emit BuyUSDP(_receiver, tokenAmount, mintAmount, feeBasisPoints);
useSwapPricing = false;
return mintAmount;
}
Step 4:
Specifically, when the `poolAmount` value is increased, the `aum` value calculated inside the `getAum` function is also increased, leading to the PLP price manipulation.
Step 5:
The attacker initially staked 1,000,000 USDT to mint approximately 996,324 PLP tokens and then called the `buyUSDP` function to raise the price of PLP. The pool amount gets manipulated due to this invocation, which lifts the price of the PLP tokens.
Step 6:
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.
function initialize(
address _vault,
address _collateralToken,
address _usdp,
address _plp,
address _shortsTracker,
uint256 _cooldownDuration
) public initializer {
__ReentrancyGuard_init();
__GovernableUpgradeable_init();
vault = IVault(_vault);
collateralToken = _collateralToken;
usdp = _usdp;
plp = _plp;
shortsTracker = IShortsTracker(_shortsTracker);
cooldownDuration = _cooldownDuration;
}
Step 7:
While adding liquidity, the exchange rate between USDP and PLP was set up at a 1:1 ratio, but while removing the same amount of PLP, the attacker was able to exchange it for approximately 1.9 times more USDP. Therefore, the attacker unstaked all PLP in the PLPManager contract using an incorrect exchange rate to profit for approximately 901,455 USDT.
Step 8:
These stolen funds were then transferred to this address, and are parked there at the time of this writing.
The team acknowledged the occurrence of the exploit and stated that they have reached out to their auditors, Binance and USDT, for their help and assistance. They have paused the PLP contract and all associated contracts, pending further investigations.
The team also stated that they are open to making a reasonable offer to the hacker in hopes of retrieving the stolen funds.
The Palmswap exploit highlights the critical need for robust security measures and comprehensive smart contract auditing in DeFi projects. The attack leveraged price manipulation of the underlying assets to cause a substantial loss.
To prevent such exploits, it's essential to use trusted price oracle services that aggregate data from multiple sources and guard against manipulation. Regular contract audits and thorough testing can help identify potential vulnerabilities and address them before attackers can exploit them.
Moreover, a well-designed economic model that considers potential attack vectors can help mitigate risks. Implementing safeguards that limit maximum profits from trades or incorporating automated systems to temporarily halt trading in case of drastic price changes can add an extra layer of protection.
While we cannot prevent all hacks, the impact of this attack could have been significantly reduced if Palmswap had established a dedicated cover pool with Neptune Mutual. Our platform offers coverage to users who suffer losses due to smart contract vulnerabilities through our parametric policies.
With Neptune Mutual, users who purchase parametric cover policies do not need to provide evidence of their loss to receive payouts. Once an incident is confirmed and resolved through our incident resolution system, payouts can be claimed immediately. Our marketplace is available on multiple popular blockchain networks, including Ethereum, Arbitrum, and the BNB chain, providing coverage to a diverse array of DeFi users and bolstering their confidence in the ecosystem.
Neptune Mutual's security team would also have evaluated the platform for DNS and web-based security, frontend and backend security, intrusion detection and prevention, and other security considerations.
Reference Source BlockSec