Analysis of the Prisma Finance Exploit

6 min read

Learn how Prisma Finance was exploited, which resulted in a loss of approximately $12.43 million.

TL;DR#

On March 28, 2024, Prisma Finance was exploited across a series of transactions on the Ethereum Mainnet, which resulted in a loss of 3,257 ETH, worth approximately $12.43 million.

Introduction to Prisma#

Prisma is a non-custodial and decentralized liquid staking protocol on Ethereum.

Vulnerability Assessment#

The root cause of the exploit is a lack of input validation.

Steps#

Step 1:

We attempt to analyze one of the attack transactions executed by the exploiter.

Step 2:

The exploit targeted the MigrateTroveZap contract, which is designed to automate the migration process between different versions of the Trove managers for the same collateral via the migrateTrove function.

The attack only affected users who had delegated their approvals to this vulnerable contract.

/// @notice Migrates a trove to another TroveManager for the same collateral
function migrateTrove(ITroveManager troveManagerFrom, ITroveManager troveManagerTo, uint256 maxFeePercentage, address upperHint, address lowerHint) external {
  address collateral = troveManagerFrom.collateralToken();
  require(address(troveManagerTo) != address(troveManagerFrom), "Cannot migrate to same TM");
  require(collateral == troveManagerTo.collateralToken(), "Migration not supported");
  (uint256 coll, uint256 debt) = troveManagerFrom.getTroveCollAndDebt(msg.sender);
  require(debt > 0, "Trove not active");
  // One SLOAD to allow set and forget
  if (!approvedCollaterals[collateral]) {
    IERC20(collateral).approve(address(borrowerOps), type(uint256).max);
    approvedCollaterals[collateral] = true;
  }
  debtToken.flashLoan(
    address(this),
    address(debtToken),
    debt - DEBT_GAS_COMPENSATION,
    abi.encode(msg.sender, address(troveManagerFrom), address(troveManagerTo), maxFeePercentage, coll, upperHint, lowerHint)
  );
  emit TroveMigrated(msg.sender, address(troveManagerFrom), address(troveManagerTo), coll, debt);
}

Step 3:

Initially, when a user calls this function, it will calculate the amount of collateral and debt that should be migrated to the new trove manager.

Within this function, it will also trigger the debtToken::flashloan function with the given collateral and debt amounts of the debt token.

/**
 * @dev Performs a flash loan. New tokens are minted and sent to the
 * `receiver`, who is required to implement the {IERC3156FlashBorrower}
 * interface. By the end of the flash loan, the receiver is expected to own
 * amount + fee tokens and have them approved back to the token contract itself so
 * they can be burned.
 * @param receiver The receiver of the flash loan. Should implement the
 * {IERC3156FlashBorrower-onFlashLoan} interface.
 * @param token The token to be flash loaned. Only `address(this)` is
 * supported.
 * @param amount The amount of tokens to be loaned.
 * @param data An arbitrary datafield that is passed to the receiver.
 * @return `true` if the flash loan was successful.
 */
// This function can reenter, but it doesn't pose a risk because it always preserves the property that the amount
// minted at the beginning is always recovered and burned at the end, or else the entire function will revert.
// slither-disable-next-line reentrancy-no-eth
function flashLoan(IERC3156FlashBorrower receiver, address token, uint256 amount, bytes calldata data) external returns (bool) {
  require(token == address(this), "ERC20FlashMint: wrong token");
  require(amount <= maxFlashLoan(token), "ERC20FlashMint: amount exceeds maxFlashLoan");
  uint256 fee = _flashFee(amount);
  _mint(address(receiver), amount);
  require(receiver.onFlashLoan(msg.sender, token, amount, fee, data) == _RETURN_VALUE, "ERC20FlashMint: invalid return value");
  _spendAllowance(address(receiver), address(this), amount + fee);
  _burn(address(receiver), amount);
  _transfer(address(receiver), _prismaCore.feeReceiver(), fee);
  return true;
}

Step 4:

The debtToken::flashloan function will then call back to the MigrateTroveZap::onFlashLoan function to complete the migration, but it lacked proper input validation, allowing the attacker to manipulate the input data.

/// @notice Flashloan callback function
function onFlashLoan(address, address, uint256 amount, uint256 fee, bytes calldata data) external returns (bytes32) {
  require(msg.sender == address(debtToken), "!DebtToken");
  (address account, address troveManagerFrom, address troveManagerTo, uint256 maxFeePercentage, uint256 coll, address upperHint, address lowerHint) = abi.decode(
    data,
    (address, address, address, uint256, uint256, address, address)
  );
  uint256 toMint = amount + fee;
  borrowerOps.closeTrove(troveManagerFrom, account);
  borrowerOps.openTrove(troveManagerTo, account, maxFeePercentage, coll, toMint, upperHint, lowerHint);
  return _RETURN_VALUE;
}

Step 5:

As a result, the attacker could trigger the closeTrove and openTrove functions, causing the migration of the assets on arbitrary addresses that were not even owned by the attacker, as long as they had delegated approvals to the exploited contracts.

Step 6:

In this attack transaction, the attacker was able to target this specific address and close its trove, after which the MigrateTroveZap contract received a refund of 1,745 wstETH. The attacker subsequently opened a new trove, spending 463 wstETH.

Step 7:

After the callback on the `onFlashloan` completed, around 1282 wstETH were remained in the MigrateTroveZap contract.

Step 8:

The attacker then opened their own trove and then called the MigrateTroveZap contract to migrate it, using the remaining 1,282 wstETH from their own trove. These troves were closed, and the profits were then withdrawn.

Step 9:

The attacker transferred the stolen funds to three different EOAs:
0x5d0064f3B54C8899Ab797445551058Be460C03C6: 1,000 ETH, worth $3,511,296
0x57f7033F84894770F876bf64772E7EBA48990D65: 1,500 ETH, worth $5,266,944
0x2d413803a6eC3Cb1ed1a93BF90608f63b157507a: 757.69 ETH, worth $3,660,482

Aftermath#

The team acknowledged the occurrence of the exploit and stated that their core engineering contributors will pause the protocol to investigate further on the issue. The vault owners were urged to disable the delegate approvals for the LST and LRT contracts.

One of the exploiters has already laundered assets worth 1,100 ETH, amounting to approximately $3.91 million to Tornado Cash. Another EOA, who had received 757.69 ETH from the exploiter, sent an on-chain message to the deployer stating the attack was a white-hat rescue and they were willing to refund the stolen assets. This address has also sent 750 ETH worth of assets to Tornado Cash.

This address reportedly mentioned on-chain that the funds have been moved to a safer place and also asked a series of questions to the deployer of Prisma Finance. These questions include their analogy with smart contracts, whether the contracts were audited before they were deployed, and the responsibilities of developers in these situations. There are two other on-chain messages sent by the supposedly white-hat hacker in this and this transaction, asking the team to act on their demands.

Solution#

Addressing the exploit that affected Prisma Finance requires a multifaceted approach, focusing on both immediate remediation and long-term preventative strategies. The core issue stemmed from a lack of input validation in the contract, which was exploited to execute unauthorized transactions. To rectify this and enhance overall security, a comprehensive audit of all smart contracts within the protocol is essential. This audit should be conducted by reputable external auditors with a proven track record of identifying vulnerabilities in DeFi protocols. The objective is to scrutinize not just the affected contract but all contracts for similar vulnerabilities or logic flaws.

In parallel, implementing stricter input validation across the board is crucial. This includes verifying the integrity of all input data for smart contracts and ensuring that only expected and secure data can influence contract execution. These measures can prevent similar exploits by ensuring that only legitimate operations are allowed and that any attempt to manipulate contract behavior with malicious data is thwarted.

Moreover, the exploit underscores the importance of rigorous permission management, particularly concerning the delegation of contract approvals. A reevaluation of the delegation process is necessary, with the aim of implementing more granular control over permissions. Users should have clear visibility into the actions they are authorizing and the potential risks associated with these actions. Introducing a more robust framework for permission delegation could involve time-bound approvals, limit caps on transactions, and more explicit user consent mechanisms.

Even with robust security protocols in place, the risk of exploits remains. In these scenarios, Neptune Mutual plays a crucial role. Establishing a dedicated cover pool with Neptune Mutual can greatly reduce the negative impact of incidents similar to the Prisma Finance exploit. Specializing in coverage for losses attributed to smart contract vulnerabilities, Neptune Mutual offers parametric policies tailored to these specific risks.

Collaborating with Neptune Mutual simplifies the recovery process for users, removing the need to provide extensive evidence of their loss. Once an incident has been verified and resolved through our detailed incident resolution process, our priority shifts to promptly delivering compensation and support to those affected. This approach guarantees rapid assistance for users caught in such security lapses.

Our services are available across several leading blockchain platforms, such as EthereumArbitrum, and the BNB chain, ensuring broad accessibility. This widespread presence allows us to protect a wide array of DeFi participants, shielding them from potential threats and vulnerabilities.

Reference Source ExVul

By

Tags