Skip to main content

Description

The vulnerability stems from an intricate implementation of a RebaseToken mechanism in the Abracadabra protocol:
  • The protocol used a RebaseToken with two key components:
    • Elastic value: Represents the actual assets in debt
    • Base value: Represents total borrowed shares
  • The User borrow mapping only stored borrowed shares per user, not the actual assets
  • The design appeared to intentionally allow the protocol (or anyone) to modify the assets owed by users by changing the elastic value of the RebaseToken
  • The repayForAll() function made this feature accessible to everyone
Attack Mechanism:
  1. A critical rounding error allowed the attacker to inflate the base value without correspondingly adjusting the elastic value
  2. This rounding error only manifests when elastic and base values significantly deviate from each other
  3. The repayForAll() function was instrumental in creating this deviation
Exploitation Steps:
  1. Inflate the base value (getting to elastic = 1, base = infinite)
  2. Repay the outstanding assets (elastic = 0)
  3. Leverage the first depositor problem, when elastic = 0, shares minted will be base = elastic
  4. Borrow all assets in the protocol, because of first depositor problem, shares minted will be 1:1.
  5. The attacker now has all assets in the protocol, but only owes a negligble share ratio to all outstanding shares.
Vulnerability Details:
  • The solvency check _isSolvent() relied on comparing the attacker’s ratio of total borrowed shares
  • The solvency check implicitly assumed elastic and base values would remain closely aligned
  • With base set to infinite, the attacker’s debt ratio becomes negligible

Proposed Solution

Assuming the original implementation was well-intentioned but flawed, a simple invariant check could have prevented the attack. The key invariant is: when no assets are owed (elastic = 0), there should be no debt shares (base = 0).
// SPDX-License-Identifier: MIT
pragma solidity 0.8.28;

import {Assertion} from "credible-std/Assertion.sol";

interface ICauldronV4 {
    function totalBorrow() external view returns (uint128 elastic, uint128 base);
}

contract AbracadabraRebaseInvariantAssertion is Assertion {
    function triggers() external view override {
        // Trigger on any storage change to the cauldron
        registerStorageChangeTrigger(this.assertionNoDebtSharesIfNoDebt.selector);
    }

    function assertionNoDebtSharesIfNoDebt() external view {
        ICauldronV4 cauldron = ICauldronV4(ph.getAssertionAdopter());

        ph.forkPostTx();
        (uint128 elastic, uint128 base) = cauldron.totalBorrow();

        // Core invariant: if no debt exists, no shares should exist
        if (elastic == 0) {
            require(base == 0, "No debt shares should exist when no debt");
        }
    }
}
This assertion ensures that when no assets are owed (elastic = 0), there should be no debt shares (base = 0), thereby preventing the exploit.