Designed to Fail꞉ A Case Study of Creditum

image

Two individuals (Xam and Entropy) have wreaked havoc on the Fantom network, repeatedly exploiting their own projects for personal gain. It is the same pattern of creating projects with malicious code, transferring ownership to another party, and exploiting the project before moving on to the next one. Protocols such as Scream Finance, Creditum, and StakeSteak, along with others, have fallen prey to their schemes. This article uncovers the various tactics they employed to deceive investors, including their use of gas-wasting contract functions, zero-cost liquidations, and even instances of leaving deployer private keys on Github. Despite insisting that these actions were mere mistakes, they repeatedly feign mental distress and resurface under new identities. Their pattern of scamming investors and moving on to new projects continues unabated, emphasizing the need for greater vigilance and awareness.

Exposing Creditum's Flawed Source Code

Gas Wasting Functions

A close examination of Creditum's source code reveals that the protocol was intentionally designed for exploitation. Before delving into the flawed liquidation engine, it's crucial to highlight the Creditum source code at address 0x04d2c9... (opens in a new tab). This code contains several functions that appear as checks but are actually designed to waste gas. One such example is the enterVerify() function, which serves no functional purpose and merely consumes gas.

 function enterVerify(
        address user, 
        address collateral, 
        uint depositAmount, 
        uint borrowAmount
    ) external override {
        // Silence warnings
        user;
        collateral;
        depositAmount;
        borrowAmount;

        if (false) {
            paused = paused;
        }
    }

This function accepts four arguments but does not use them in any meaningful way. The if (false) statement is essentially a no-op, as the condition will always evaluate to false, rendering the code within the block inoperative. Instead, the function only serves to silence any warnings related to unused variables.

Flawed Liquidation Engine

The key to exploiting Creditum was through its liquidation mechanism. Any algo-stablecoin paired with flash loans that are dependent on oracles is vulnerable to attack.

image (opens in a new tab)

In a typical lending protocol, when a user's position is liquidated, the liquidator usually receives a portion of the collateral to cover the outstanding debt. However, Creditum's design permits the seizure of collateral without any repayment of the actual debt, resulting in bad debt being accumulated and cUSD circulating without a corresponding collateralized position.

This flawed design stipulates that if a position remains above the liquidation threshold for 60 minutes, the liquidator can seize the entire collateral without any cost. Creditum's liquidation engine functions by gradually decreasing the collateral's buyout price until it reaches zero. In essence, the liquidator merely needs to wait for the buyout price to fall below the borrowed amount, allowing them to seize the collateral for free. Bad debt arises from the decreasing buyout price, making the system highly susceptible to exploitation and ultimately leading to the protocol's insolvency. Furthermore, liquidations that take place do not lead to a reduction in the borrowed amount.

Interestingly, the Creditum contract's controller can be upgraded, but the core, which contains the exploitable backdoor, is intentionally non-upgradable. Upon examining the values from 0x07f961...#readContract (opens in a new tab) and 0x07f961c...#readProxyContract (opens in a new tab), it becomes evident why the contract was designed to store tokens on the core, non-upgradable contract. This design ensured that the exploit would be permanently embedded into the system.

To better understand the mechanics of the liquidation process in Creditum, let's dive into the code.

  1. The liquidateBorrowFresh function calculates the collateral to reward the liquidator, collateral to return to the owner, and auction price:
(collateralToLiquidator, collateralToOwner, auctionPrice) = controller.getAuctionDetails(borrower, collateral);

This function is responsible for determining the distribution of collateral between the liquidator and the original owner, as well as setting the auction price for the collateral during the liquidation process.

  1. In the (auctionPrice > penalty) block, the liquidation penalty is transferred to the treasury, and the remaining auction proceeds are burned from the liquidator's holdings:
if (penalty != 0) {
    IERC20(fToken).safeTransferFrom(liquidator, treasury, penalty);
}
fToken.burn(liquidator, auctionPrice - penalty);
  1. The (auctionPrice <= penalty) block ensures that the entire repayment is transferred to the treasury directly from the liquidator:
if (auctionPrice != 0) {
    IERC20(fToken).safeTransferFrom(liquidator, treasury, auctionPrice);
}
  1. The following code snippet is responsible for transferring the rewarded collateral to the liquidator:
if (collateralToLiquidator != 0) {
    IERC20(collateral).safeTransfer(liquidator, collateralToLiquidator);
}

The liquidation engine's flaw is rooted in its handling of collateral rewards and repayments. It permits the collateral's buyout price to linearly decrease until it reaches zero.

  1. The getAuctionDetails function calculates the auction price, the amount to reward the liquidator, and the amount to return to the owner:
    /// @notice Gets auction price, amount to reward liquidator, and amount to return to owner
    /// @param borrower The borrower to be liquidated
    /// @param collateral The borrower's collateral to liquidate
    /// @return (amount of collateral to reward liquidator, amount of collateral to return to owner, cost to buyout the auction)
    function getAuctionDetails(address borrower, address collateral) public view returns (uint, uint, uint) {
        uint depreciationDuration = collateralData[collateral].depreciationDuration;
        (uint triggerTimestamp, uint initialPrice) = core.auctionData(collateral, borrower);
        uint timeElapsed = block.timestamp - triggerTimestamp;
        uint debt = getDebtValue(borrower, collateral);
        uint penalty = debt * collateralData[collateral].liquidationPenalty / MULTIPLIER;
        (uint totalCollateral, , ) = core.userData(collateral, borrower);

        // Calculate collateral to reward liquidator, collateral to return to owner, and auction price
        return calcAuctionDetails(depreciationDuration, timeElapsed, initialPrice, debt + penalty, totalCollateral);
    }

This function is the core of the problem. It calculates the auction price, the collateral to reward the liquidator, and the collateral to return to the owner based on the depreciation duration, time elapsed, initial price, debt, penalty, and total collateral. The issue lies in the fact that the function allows the buyout price of the collateral to linearly drop until it reaches zero. As a result, liquidators can simply wait for the buyout price to hit zero and seize the full collateral without repaying the debt, causing the entire borrowed amount to become bad debt. Such a design has severe implications for the protocol's long-term sustainability, as it continually generates bad debt and undermines the value of the circulating cUSD stablecoin.

image (opens in a new tab)

This design flaw allowed a single liquidator at address 0x167530... (opens in a new tab) to seize entire collateral amounts without repaying the associated debt. This address was responsible for 95% of the liquidations within the Creditum protocol. By taking advantage of the protocol's vulnerable liquidation engine, this address was able to accumulate substantial profits without repaying user debts, which resulted in the protocol's collapse.

An example of this exploit, or trigger liquidation can be seen at 0x081be7... (opens in a new tab) or on Tenderly's dashboard (opens in a new tab).

image (opens in a new tab)

Regrettably, Creditum was not the only target. MCLB intervened to halt the deployment of Singularity, which was also susceptible to the same flash loan/oracle exploit. This pattern of exploiting protocols (Scream, SteakStake, Singularity) suggests that these incidents were more than mere mistakes. Investors are misled while the individuals behind these exploits claim ignorance, feign distress, and eventually re-emerge under new identities.

Deceptive Audit

The PeckShield audit reveals two critical points: the exploiters had a clear understanding of the vulnerabilities in the protocol and intentionally removed the exploitable code to receive a passing grade on the audit. Examining the commit hash values associated with the code is crucial, as these values persist even when transferring Git repositories.

image (opens in a new tab)

image

image

Although the audit focused on the Github repository associated with Xam's repository, the commit hashes are notably missing. Furthermore, Xam has not pushed any updates to the Creditum repository. It appears that the audit conducted on Xam's Github profile may have been misleading since the auditor might have assessed a different repository that lacked the malicious code. This evidence suggests that the exploiters knowingly removed the offending code to pass the audits, demonstrating that their actions were not simple mistakes but rather calculated actions to deceive investors and the wider community.

Exploiting Transaction

image (opens in a new tab)

Firstly, the address responsible 0x167530... (opens in a new tab) is a Tornado Cash funded address, and as mentioned before was responsible for a staggering 95% of all liquidations within Creditum. This proved to be an incredibly lucrative endeavor, as the address only had to repay a marginal amount of the users' debt, receiving the underlying collateral free or almost free each time by simply waiting.

Let's examine the transaction 0x081be7... (opens in a new tab) or on Tenderly's dashboard (opens in a new tab).

image (opens in a new tab)

This transaction is an example of an exploit of Creditum's linear buyout mechanism. The borrower had borrowed 167,998.39 cUSD against collateral worth 177,858.96 USDC. The CDP fell below the required collateralization ratio, triggering liquidation.

AmountActionFromTo
59,879.60 cUSDFlash loan0x00000xfc59
59,879.60 cUSDSent to multisig by liquidator0xfc59Multisig Wallet
51,341.77 cUSDBurned0xfc590x0000
177,877.99 USDC ($177,858.96)Sent to liquidator contractCreditum: Creditum Core0xfc59
177,877.99 USDC ($177,858.96)Sent to Creditum CDP by liquidator0xfc59Creditum: Creditum Core
177,877.99 cUSDBurned by liquidator contract0xfc59N/A was
117,998.39 cUSDBurned by liquidator contract0xfc59N/A
117,998.39 USDC ($117,985.76)Withdrawn from CDP by liquidatorCreditum: Creditum Core0xfc59
117,998.39 USDC ($117,985.76)Collateral sent to offending wallet0xfc590xfd36
59,879.60 cUSDFlash loan repaid0x00000xfc59

To the untrained eye, it may appear that there was a massive arbitrage between the liquidator contract and the stabilizer pool. However, upon checking the state changes, it can be seen A liquidator used a flash loan to borrow 59,879.60 cUSD and 177,877.99 USDC. The liquidator was able to obtain 117,985 USDC worth of collateral, but was only required to pay off ~50,000 cUSD of the original debt. As a result, Creditum was left with 117,985 cUSD of bad debt from a position that had 177,858.96 USDC of collateral and 167,998.39 cUSD debt.

image (opens in a new tab)

  1. A user borrows 167,998.39 cUSD against a CDP collateralized with 177,858.96 worth of USDC.
  2. The borrower was liquidated by 0x167530... (opens in a new tab) due to the collateralization ratio falling below the required level.
  3. A liquidator employs a flash loan, borrowing 177,858.96 USDC and 59,879.60 cUSD.
  4. The liquidator transfers 177,858.96 USDC to the Creditum protocol contract and burns 59,879.60 cUSD.
  5. The liquidator then moves the 177,858.96 USDC to the borrower's CDP and burns the entire amount.
  6. The liquidator proceeds to burn the remaining 117,985.76 USDC and 117,998.39 cUSD.
  7. The liquidator withdraws 117,985.76 USDC from the borrower's CDP and transfers it to their wallet, which is responsible for 95% of all liquidations on Creditum.
  8. The liquidator acquires 117,985.76 USDC worth of collateral but only needs to pay off ~50,000 cUSD of the original debt, leaving Creditum with 117,985.76 cUSD of bad debt.
  9. The flash loan is repaid.

Creditum's linear buyout mechanism allowed 0x167530... (opens in a new tab) to patiently wait for the buyout price to drop below the borrowed amount, effectively making the position nearly cost-free to liquidate. This transaction exemplifies the inevitability of bad debt in this design. While the liquidator obtained 117,985 USDC worth of collateral, they were only required to pay off ~50,000 cUSD of the original debt. Consequently, Creditum was burdened with 117,985 cUSD of bad debt. This design is inherently unsustainable, as it discourages competition among liquidators by allowing them to wait for positions to become almost free to seize, regardless of market efficiency. Liquidators would only compete to receive the largest percentage of the collateral, allow the linear buyout mechanism to increase profits.

Conclusion

In this article, we have analyzed the collapse of the Creditum protocol and identified its flawed design as the primary cause of failure. The linear buyout mechanism created an environment of unsustainable bad debt, which ultimately led to the protocol's demise. The deceptive audit misled users into believing the protocol was secure, while the gas-wasting functions served zero purpose.

Notably, the individuals Xam and Entropy have orchestrated yet another protocol's downfall. These malicious actors have a history of exploiting multiple DeFi projects by capitalizing on vulnerabilities they engineered themselves. Despite their assertions of innocent mistakes and feigned mental distress, their pattern of defrauding investors and moving on to new ventures persists.

For users that remain active and those yet to come, recognizing these patterns of exploitation is vital to prevent similar occurrences in the future.

LIQUIDATION ENGINE

        // Ensure liquidation is allowed
        uint allowed = controller.liquidateBorrowAllowed(liquidator, borrower, collateral);
        if (allowed != uint(Error.NO_ERROR)) {
            return (fail(Error(allowed)), 0, 0);
        }

        uint collateralToLiquidator;
        uint collateralToOwner;
        uint auctionPrice;
        uint collateralToTreasury;
        uint penalty;

        {
        // Calculate collateral to reward liquidator, collateral to return to owner, and auction price
        (collateralToLiquidator, collateralToOwner, auctionPrice) = controller.getAuctionDetails(borrower, collateral);

        // Calculate dust collateral to send to treasury
        uint totalCollateral = userData[collateral][borrower].deposits;
        collateralToTreasury = totalCollateral - collateralToLiquidator - collateralToOwner;

        // Calculate liquidation penalty
        uint debt = controller.getDebtValue(borrower, collateral);
        (, , , , , , uint liquidationPenalty, ) = controller.collateralData(collateral);
        penalty = debt * liquidationPenalty / MULTIPLIER;
        }

        address treasury = controller.treasury();

        ///////////////////////////
        /** NO MORE SAFE RETURNS */
        ///////////////////////////

        // Delete storage slots before performing transfers
        delete auctionData[collateral][borrower];
        delete userData[collateral][borrower];

        if (auctionPrice > penalty) {
            // Since auction price > liquidation penalty, transfer liquidation penalty to treasury
            if (penalty != 0) {
                IERC20(fToken).safeTransferFrom(liquidator, treasury, penalty);
            }

            // Burn remaining auction proceeds from liquidator
            fToken.burn(liquidator, auctionPrice - penalty);
        } else {
            // Since auction price <= liquidation penalty, transfer entire repayment to treasury directly from liquidator
            if (auctionPrice != 0) {
                IERC20(fToken).safeTransferFrom(liquidator, treasury, auctionPrice);
            }
        }

        // Send rewarded collateral to liquidator
        if (collateralToLiquidator != 0) {
            IERC20(collateral).safeTransfer(liquidator, collateralToLiquidator);
        }
        
        // Return excess collateral to owner
        if (collateralToOwner != 0) {
            IERC20(collateral).safeTransfer(borrower, collateralToOwner);
        }

        // Send dust collateral to treasury
        if (collateralToTreasury != 0) {
            IERC20(collateral).safeTransfer(treasury, collateralToTreasury);
        }

        emit LiquidateBorrow(liquidator, borrower, collateral, auctionPrice);

        // Perform safety checks post-liquidation
        // controller.liquidateBorrowVerify(liquidator, borrower, collateral);

        return (uint(Error.NO_ERROR), auctionPrice, collateralToLiquidator);
    } 

image (opens in a new tab)