> ## Documentation Index
> Fetch the complete documentation index at: https://phylaxsystems-docs-project-incidents.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# UxLink Multisig Ownership Compromise

> Compromised private keys led to \$10M+ drain from UxLink multisig through threshold manipulation and owner replacement

## Description

On September 22, 2025, UxLink's multisig was compromised through private key theft, resulting in \$11.3M direct theft plus unauthorized minting of 10 trillion tokens worth \~\$28M. Attackers manipulated the multisig configuration to drain funds and gain token minting privileges.

## Core Vulnerability Mechanism

The attack exploited two weaknesses:

1. **Compromised Private Keys**: Attackers gained access to UxLink's multisig owner private keys
2. **Unrestricted Multisig Configuration**: Safe contract allowed threshold reduction and owner changes without security checks
3. **Token Minting Access**: Compromised multisig gained Manager privileges over the UXLINK token contract

**Attack Vector:**
Compromised private keys enabled execution of multisig transactions without validation or rate limiting.

## Attack Analysis

### Attack Sequence

**Phase 1: Multisig Compromise**
Attackers used compromised private keys to revoke admin privileges, add themselves as owners, and reduce the multisig threshold to 1, gaining full control.

**Phase 2: Treasury Drainage**
Drained \$11.3M in assets (USDT, USDC, WBTC, ETH) and converted them across chains.

**Phase 3: Token Exploitation**
Used multisig control to mint unlimited UXLINK tokens, causing a price crash and significant market cap loss.

### Root Causes

1. **Private Key Compromise**: Compromised private keys due to poor operational security
2. **No Multisig Protection**: No safeguards against rapid configuration changes
3. **No Rate Limiting**: Multisig changes executed immediately without cooling periods
4. **Missing Access Controls**: No whitelisting for owner changes

## Proposed Solution

Multisig protection assertions could have prevented this attack by enforcing governance invariants:

```solidity theme={null}
// SPDX-License-Identifier: MIT
pragma solidity 0.8.28;

import {Assertion} from "../../lib/credible-std/src/Assertion.sol";
import {PhEvm} from "../../lib/credible-std/src/PhEvm.sol";
import {ISafe} from "@safe-global/safe-contracts/contracts/interfaces/ISafe.sol";

contract UxLinkMultisigProtectionAssertion is Assertion {
    // Minimum threshold that must be maintained
    uint256 constant MIN_THRESHOLD = 2;

    // Whitelisted addresses that can become owners (set at deployment)
    address constant ALLOWED_OWNER_1 = 0x1111111111111111111111111111111111111111;
    address constant ALLOWED_OWNER_2 = 0x2222222222222222222222222222222222222222;
    address constant ALLOWED_OWNER_3 = 0x3333333333333333333333333333333333333333;

    function triggers() external view override {
        registerCallTrigger(
            this.assertThresholdProtection.selector,
            ISafe.changeThreshold.selector
        );

        registerCallTrigger(
            this.assertOwnerAddition.selector,
            ISafe.addOwnerWithThreshold.selector
        );

        registerCallTrigger(
            this.assertOwnerRemoval.selector,
            ISafe.removeOwner.selector
        );
    }

    /// @notice Prevents threshold reduction below minimum
    function assertThresholdProtection() external view {
        ISafe safe = ISafe(ph.getAssertionAdopter());

        PhEvm.CallInputs[] memory calls = ph.getCallInputs(
            address(safe),
            ISafe.changeThreshold.selector
        );

        for (uint256 i = 0; i < calls.length; i++) {
            uint256 newThreshold = abi.decode(calls[i].input, (uint256));

            // Never allow threshold below minimum
            require(newThreshold >= MIN_THRESHOLD, "Threshold cannot go below minimum");
        }
    }

    /// @notice Validates new owner additions against whitelist
    function assertOwnerAddition() external view {
        ISafe safe = ISafe(ph.getAssertionAdopter());

        PhEvm.CallInputs[] memory calls = ph.getCallInputs(
            address(safe),
            ISafe.addOwnerWithThreshold.selector
        );

        for (uint256 i = 0; i < calls.length; i++) {
            (address newOwner, uint256 threshold) = abi.decode(calls[i].input, (address, uint256));

            // Basic validation
            require(newOwner != address(0), "Invalid owner address");

            // Check whitelist
            require(
                newOwner == ALLOWED_OWNER_1 ||
                newOwner == ALLOWED_OWNER_2 ||
                newOwner == ALLOWED_OWNER_3,
                "New owner not whitelisted"
            );

            // Never allow threshold below minimum
            require(threshold >= MIN_THRESHOLD, "Threshold cannot go below minimum");
        }
    }

    /// @notice Validates owner removals don't reduce threshold below minimum
    function assertOwnerRemoval() external view {
        ISafe safe = ISafe(ph.getAssertionAdopter());

        PhEvm.CallInputs[] memory calls = ph.getCallInputs(
            address(safe),
            ISafe.removeOwner.selector
        );

        for (uint256 i = 0; i < calls.length; i++) {
            (, , uint256 threshold) = abi.decode(calls[i].input, (address, address, uint256));

            // Never allow threshold below minimum
            require(threshold >= MIN_THRESHOLD, "Threshold cannot go below minimum");
        }
    }
}
```

<Note>
  Assertions cannot track state across transactions (e.g., cooling periods). The original attack used rapid sequential transactions, which would require off-chain monitoring or timelocked governance contracts to fully prevent. However, the assertions above still provide strong protection by enforcing minimum thresholds and owner whitelisting.
</Note>

### How These Assertions Prevent the Attack

**What they do:**

1. **Threshold Protection**: Prevents threshold from dropping below a safe minimum (e.g., 2)
2. **Owner Whitelisting**: Only allows pre-approved addresses to become owners
3. **Removal Validation**: Ensures owner removals don't reduce threshold below minimum

**How they prevent the attack:**

* **Step 1**: `assertThresholdProtection()` blocks any attempt to reduce threshold to 1
* **Step 2**: `assertOwnerAddition()` blocks attacker addresses that aren't whitelisted
* **Step 3**: `assertOwnerRemoval()` ensures threshold stays above minimum even when removing owners
* **Result**: Attacker cannot gain solo control of the multisig

**Key insight**: While these assertions can't prevent private key compromise or enforce time delays between transactions, they establish hard boundaries that attackers cannot cross regardless of how many keys they control.

## Key Takeaway

The UxLink attack shows that multisig security requires both operational security (private key protection) and protocol-level protections (configuration validation). The \$39.3M total impact (\$11.3M direct + \$28M token minting) demonstrates how compromised private keys can lead to complete protocol takeover when governance controls are missing.

Credible Layer assertions can enforce governance invariants: maintaining reasonable thresholds, validating owner changes, and preventing rapid reconfiguration. This approach ensures operational security failures don't lead to complete protocol compromise.
