Unrestricted Minting Exploit in DeFi Protocol
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:
- Economic Manipulation: The chair could inflate the stablecoin supply arbitrarily, potentially breaking its peg and undermining the protocol's stability.
- Trust Risk: If the chair's private key was compromised or the role was abused, an attacker could mint unlimited tokens, draining value from the ecosystem or manipulating external protocols (e.g., Compound via ctoken).
- Systemic Risk: Over-supplying the stablecoin could disrupt lending markets or lead to loss of confidence in the protocol's products.
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
- Deploy the FedExploitTest contract
- Call runExploit() to mint 5 million tokens (5 iterations of 1M * 10^18)
- 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:
- After 1st mint: 1,000,000 tokens
- After 2nd mint: 2,000,000 tokens
- After 3rd mint: 3,000,000 tokens
- After 4th mint: 4,000,000 tokens
- After 5th mint: 5,000,000 tokens
- After additional calls: 8,000,000 tokens total
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:
- The chair (deployer) could mint tokens with no limits or delays, allowing for arbitrary inflation
- Additional mints succeeded immediately in separate transactions, confirming no cooldown
- Even if the chair was intended to be a trusted entity (e.g., a multi-sig), the lack of on-chain limits remained a concern
- The vulnerability could affect the entire ecosystem due to the token's role in the protocol
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:
- Defense in Depth: Even trusted roles should have on-chain constraints to minimize damage if compromised
- Economic Safety Guards: DeFi protocols should implement mechanisms to prevent economic attacks, such as rate limits and maximum caps
- Trust Minimization: While administrative privileges are often necessary, their power should be constrained through technical measures
- Multi-layer Security: Combine access controls with parameter bounds, timelocks, and monitoring systems
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.