Unrestricted Minting Exploit in DeFi Protocol

Severity: High Reward: REDACTED

DeFi protocols introduce novel financial primitives, but also unique security challenges. In this writeup, I'll share my discovery of a critical vulnerability in a federated token contract that allowed unlimited minting of the underlying token, potentially leading to protocol insolvency and significant user losses.

Vulnerability Details

The expansion function in the Anchor Fed contract (Fed.sol) at a major DeFi protocol allowed the chair role to mint an unlimited amount of the underlying token (stablecoin) without an upper bound or a timelock/cooldown mechanism. This unrestricted minting capability posed a significant risk of supply inflation, potential peg destabilization, and economic manipulation if the chair role was compromised or misused.

Affected Code

The vulnerability resided in the expansion function of the Fed.sol contract. Here's the relevant code snippet:

function expansion(uint amount) public {
    require(msg.sender == chair, "ONLY CHAIR");
    underlying.mint(address(this), amount);
    require(ctoken.mint(amount) == 0, 'Supplying failed');
    supply = supply.add(amount);
    emit Expansion(amount);
}

Impact Details

This vulnerability had severe implications for the Anchor Fed contract:

I rated this vulnerability as High severity due to the potential for significant economic damage and the reliance on a single point of failure (chair) without additional safeguards.

Key Issues

1. No Upper Bound on Minting Amount

The amount parameter was a uint with no maximum limit enforced. The chair could mint any value up to 2^256 - 1, which is practically unlimited. This allowed for arbitrary inflation of the token supply.

2. No Timelock or Cooldown Period

The function could be called repeatedly in the same transaction or across multiple transactions without delay. There was no mechanism to prevent rapid, successive minting events.

3. Sole Reliance on chair Access Control

Only the chair could call this function, but there were no additional checks (e.g., multi-sig, governance approval, or timelock) to mitigate misuse or compromise of the chair role.

Proof of Concept

I developed a PoC to demonstrate the vulnerability. The test simulated the chair minting large amounts of tokens repeatedly, confirming the lack of restrictions.

Solidity Code

// SPDX-License-Identifier: MIT
pragma solidity ^0.5.16;

// Mock contracts needed for testing
contract ERC20Mock {
    mapping(address => uint256) public balanceOf;
    mapping(address => mapping(address => uint256)) public allowance;
    uint256 public totalSupply;
    
    function mint(address to, uint256 amount) public {
        balanceOf[to] += amount;
        totalSupply += amount;
    }
    
    function burn(uint256 amount) public {
        balanceOf[msg.sender] -= amount;
        totalSupply -= amount;
    }
    
    function approve(address spender, uint256 amount) public returns (bool) {
        allowance[msg.sender][spender] = amount;
        return true;
    }
}

contract CErc20Mock {
    ERC20Mock public underlyingToken;
    
    constructor(address underlying_) public {
        underlyingToken = ERC20Mock(underlying_);
    }
    
    function mint(uint256 amount) external returns (uint) {
        return 0;
    }
    
    function underlying() external view returns (address) {
        return address(underlyingToken);
    }
    
    function redeemUnderlying(uint256 amount) external returns (uint) {
        return 0;
    }
}

contract Fed {
    using SafeMath for uint;
    
    CErc20Mock public ctoken;
    ERC20Mock public underlying;
    address public chair;
    address public gov;
    uint public supply;

    event Expansion(uint amount);
    event Contraction(uint amount);

    constructor(CErc20Mock ctoken_, address gov_) public {
        ctoken = ctoken_;
        underlying = ERC20Mock(ctoken_.underlying());
        underlying.approve(address(ctoken), uint(-1));
        chair = msg.sender;
        gov = gov_;
    }

    function expansion(uint amount) public {
        require(msg.sender == chair, "ONLY CHAIR");
        underlying.mint(address(this), amount);
        require(ctoken.mint(amount) == 0, 'Supplying failed');
        supply = supply.add(amount);
        emit Expansion(amount);
    }
}

// POC Contract
contract FedExploitTest {
    Fed public fed;
    CErc20Mock public ctoken;
    ERC20Mock public underlying;
    
    event LogBalance(uint256 balance);
    event LogSupply(uint256 supply);
    
    constructor() public {
        // Deploy mock tokens
        underlying = new ERC20Mock();
        ctoken = new CErc20Mock(address(underlying));
        
        // Deploy Fed with this contract as gov
        fed = new Fed(ctoken, address(this));
    }
    
    function runExploit() public {
        // Initial state
        emit LogSupply(underlying.totalSupply());
        
        // Exploit 1: Mint a large amount multiple times to show no restrictions
        for(uint i = 0; i < 5; i++) {
            fed.expansion(1000000 * 10**18); // 1 million tokens each time
            emit LogSupply(underlying.totalSupply());
        }
    }
    
    // Additional function to show minting can be done repeatedly
    function mintMore(uint256 amount) public {
        fed.expansion(amount);
        emit LogSupply(underlying.totalSupply());
    }
}

Exploitation Steps

  1. Deploy the FedExploitTest contract
  2. Call runExploit() to mint 5 million tokens (5 iterations of 1M * 10^18)
  3. Call mintMore(1000000000000000000000000) multiple times to mint additional 1M tokens without restrictions

Results

From the initial supply of 0, I was able to mint unlimited tokens in rapid succession:

This demonstrated that the chair could mint any amount of tokens without limits or delays, potentially destabilizing the entire protocol.

Observations and Analysis

Through my analysis, I observed several key issues with the Fed contract's design:

Recommended Mitigation Strategies

To address this vulnerability, I recommended several mitigation strategies:

1. Implement Maximum Mint Limits

// Add protocol-wide configuration
uint256 public maxMintAmount;

function expansion(uint amount) public {
    require(msg.sender == chair, "ONLY CHAIR");
    require(amount <= maxMintAmount, "AMOUNT EXCEEDS MAXIMUM");
    // ... rest of the function
}

2. Add Timelock Mechanism

// Add cooldown period
uint256 public lastMintTimestamp;
uint256 public constant MINT_COOLDOWN = 1 days;

function expansion(uint amount) public {
    require(msg.sender == chair, "ONLY CHAIR");
    require(block.timestamp >= lastMintTimestamp + MINT_COOLDOWN, "COOLDOWN ACTIVE");
    
    // ... rest of the function
    
    lastMintTimestamp = block.timestamp;
}

3. Implement Multi-signature Requirements

Replace the single chair role with a multi-signature mechanism requiring approval from multiple authorized entities, significantly reducing the risk of compromise or misuse.

The Vendor's Response

After reporting this issue through the protocol's bug bounty program, the security team determined that the vulnerability was out of scope for a reward. Their rationale was that attacks requiring access to privileged addresses (including governance and strategist contracts) without additional modifications to the privileges were excluded from the bounty program.

While I understand their perspective based on the specific terms of the bug bounty program, I maintain that unlimited minting with no cooldown could destabilize the token's peg even if the chair is a multi-sig. Past DeFi exploits have shown that privileged roles can be compromised, and on-chain limits would provide a valuable safety net.

Lessons Learned

This vulnerability highlights several important principles for secure DeFi development:

Conclusion

Despite not qualifying for a bounty under the program's specific terms, disclosing this vulnerability was still valuable for the wider security community. It demonstrates how even protocol-level controls and privileged operations should incorporate safeguards against potential abuse or compromise.

The DeFi ecosystem continues to evolve, and security must evolve with it. By discussing these issues openly, we contribute to a more secure Web3 environment that can better protect user funds and protocol stability.

Back to Blog