
Recently, @PrismaFi was exploited (or compromised) by a white hat hacker ?. While the terminology isn't crucial, what matters most is learning from the incident.
Summary
The vulnerability affects Prisma Trove owners who have approved a contract to manage their Trove's position via setDelegateApproval(MigrateTroveZap, True) on the BorrowerOperations contract. The issue arose when a user executed a transaction with insufficient input validation in the onFlashloan function, enabling the user to manipulate the contract's behavior and access a portion of the user's collateral.
Prismafi Background
Prisma Finance deployed a contract called MigrateTroveZap to facilitate trove owners migrating their Troves (collateralized debt positions) to new TroveManager contracts. This was part of a broader system upgrade announced in March 2024.
As part of these updates, Prisma deprecated the original 4 LST (Liquid Staking Token) Vaults with a 4% interest rate cap, replacing them with new LST Vaults, allowing for higher rates. The 3 LRT (Liquid Restaking Token) Vaults were also deprecated and replaced with new versions optimized for more efficient redemptions.
Prisma built the MigrateTroveZap helper contract to ensure a smooth transition, enabling trove owners to seamlessly move their Trove positions from the old TroveManager contracts to the new ones without needing to close positions, remove liquidity, or incur additional costs. The contract uses flash loans to close the user's old Trove and immediately open a new Trove with the same collateral and debt in the new TroveManager, all in one atomic transaction.
Root Causes of hack
The users took advantage of the vulnerability through the following steps:
1. The users bypassed the migrate function and called the flash loan directly with carefully crafted input data.
2. The flash loan's callback function, onFlashloan(), was called.
3. The lack of proper checks allowed the users to close a trove owner's Trove and immediately reopen it within the same TroveManager. The transaction caused the owner's position to be closed and reopened, resulting in the trove owner having a new position with the same debt but less collateral and the difference in collateral sitting in the zap contract.
4. The users then opened a new Trove and used the MigrateTroveZap contract to migrate it, effectively using the remaining collateral for their own Trove.
5. Finally, the users closed their Trove and took the profits.
Here is a brief explanation of the hack.
1) The attack focused on the MigrateTroveZap contract, designed to simplify the transfer process between various Trove managers for the same collateral using the migrateTrove function. Users who had authorized this contract were affected by the exploit.
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 - DEBTGASCOMPENSATION,
abi.encode(msg.sender, address(troveManagerFrom), address(troveManagerTo), maxFeePercentage, coll, upperHint, lowerHint)
);
emit TroveMigrated(msg.sender, address(troveManagerFrom), address(troveManagerTo), coll, debt);
}
When a user activates the migrateTrove function it calculates the collateral and debt to be moved to the new Trove manager. Additionally, it triggers the debtToken::flashloan function, borrowing collateral and debt tokens based on the user's trove data.
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) == RETURNVALUE, "ERC20FlashMint: invalid return value");
_spendAllowance(address(receiver), address(this), amount + fee);
_burn(address(receiver), amount);
transfer(address(receiver), prismaCore.feeReceiver(), fee);
return true;
}
3) 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.
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 RETURNVALUE;
}Consequently, attackers could execute the closeTrove and openTrove functions, shifting assets to arbitrary addresses—even those not owned by the attacker—as long as they had granted approvals to the exploited contracts.
After the onFlashloan callback, approximately 1282 wstETH remained in the MigrateTroveZap contract. The attacker subsequently opened their trove and migrated it using the remaining wstETH. These troves were closed, and the profits were withdrawn.
Attacker's Exploit Breakdown (Example Transaction):
The attackers targeted vulnerable Troves one by one, manipulating them through a series of transactions. Let's break down a simplified example to understand how it worked:
1. Targeted Trove Closure and Reopening
- The attacker initiated a transaction that leveraged the MigrateTroveZap contract.
- This caused the wstEthTroveManagerV2 contract (which manages user Troves) to:
- Close the victim's Trove, which initially had 1745.08 wstETH collateral and 1,442,100 mkUSD debt.
- Briefly send the victim's collateral (*1745.08 wstETH) to the MigrateTroveZap* contract.
- Then, immediately reopen the victim's Trove with a significantly lower collateral amount (463.18 wstETH). The debt amount (1,443,398 mkUSD) remained almost the same.
- This manipulation resulted in the victim's collateral ratio dropping drastically from around 498% to a risky 132%*, putting their Trove at risk of liquidation (having their remaining collateral seized to cover the debt).
2. Exploiting Remaining Collateral
- The attacker then opened a new, much smaller Trove with just 1 wstETH collateral and 2,000 mkUSD debt.
- They strategically used the MigrateTroveZap contract again to migrate their new Trove.
- During this migration, the contract likely accessed the remaining collateral (1282.79 wstETH) left from the victim's manipulated Trove in the MigrateTroveZap contract.
- As a result, the attacker reopened a new Trove with most of the remaining collateral (1282.79 wstETH) and a slightly higher debt (2,001.8 mkUSD).
3. Profiting from the Exploit
- Finally, the attacker closed their newly created Trove, receiving the stolen collateral (1282.79 wstETH).
- They then used these stolen funds to repay any necessary flash loans borrowed from platforms like Balancer or Prisma to facilitate the attack.
How you avoid similar incidents ?
Before deploying any code, it's important to make sure the transfer of assets is handled carefully. This means double-checking the permissions for accessing functions to ensure that whoever is calling them, the functions themselves, and the data being transferred are all safe and reliable.
Learn more About Flash Loan Attacks
Author: Piyush Shukla