28890 - [SC - Insight] EBTCTokensol mint function lack of checks allow...

Submitted on Mar 1st 2024 at 01:09:47 UTC by @cryptonoob2k for Boost | eBTC

Report ID: #28890

Report type: Smart Contract

Report severity: Insight

Target: https://github.com/ebtc-protocol/ebtc/blob/release-0.7/packages/contracts/contracts/EBTCToken.sol

Impacts:

  • Permanent freezing of funds

Description

Bug Description

EBTCToken.sol mint function logic is incompatible with restrictions implemented in EBTCToken.sol::transfer and EBTCToken.sol::transferFrom methods that prevents EBTCToken holding EBTC tokens breaking EBTCToken balance restriction and leading to EBTC tokens funds stuck in contract unable to recover

Brief/Intro

EBTCToken.sol::transfer and EBTCToken.sol::transferFrom methods implements restrictions to block users to send EBTC tokens to EBTCToken contract, thus ensuring EBTCToken contract EBTC balance always remains 0. However this restriction can be bypassed using mint function.

Vulnerability Details

The restriction inside transfer and transferFrom are implemented using the internal function _requireValidRecipient:

function _requireValidRecipient(address _recipient) internal view {
    require(
        _recipient != address(0) && _recipient != address(this),	// <@ block
        "EBTC: Cannot transfer tokens directly to the EBTC token contract or the zero address"
    );
    //...
}

This function ensures that EBTCToken's EBTC balance remains 0 because it blocks transfer to EBTCToken address:

contract EBTCToken is IEBTCToken, AuthNoOwner, PermitNonce {
	//...
    function transfer(address recipient, uint256 amount) external override returns (bool) {
        _requireValidRecipient(recipient);
        _transfer(msg.sender, recipient, amount);
        return true;
    }

    function transferFrom(
        address sender,
        address recipient,
        uint256 amount
    ) external override returns (bool) {
    	_requireValidRecipient(recipient);
        _transfer(sender, recipient, amount);
        //...
    }

However this restriction doesnt hold if a user mints tokens directly to this contract, because in mint function there isnt this check in place:

    function mint(address _account, uint256 _amount) external override {
        _requireCallerIsBOorCdpMOrAuth();	// <@ no restriction 
        _mint(_account, _amount);
    }

    function _mint(address account, uint256 amount) internal {
        require(account != address(0), "EBTCToken: mint to zero recipient!");

        _totalSupply = _totalSupply + amount;
        _balances[account] = _balances[account] + amount;
        emit Transfer(address(0), account, amount);
    }

Impact Details

By using mint function to directly issue EBTC tokens to EBTCToken contract the restrictions implemented in transfer and transferFrom functions to keep EBTCToken balance to 0 are bypassed allowing EBTCToken contract to hold tokens and have EBTC tokens stuck in contract

Risk Breakdown

The vulnerability is easy to exploit, however to exploit it mint capability is needed leading to stuck tokens in EBTCToken contract and balance restriction bypass

Recommendation

Implement a restriction in mint function like the ones implemented in transfer and transferFrom function such as

    function mint(address _account, uint256 _amount) external override {
        _requireCallerIsBOorCdpMOrAuth();
       _requireValidRecipient(recipient);
        _mint(_account, _amount);
    }

Proof of Concept

Here is a foundry test file, save it in packages/contracts/foundry_test subdir and run it with:

forge test -vvv --match-contract EBTCTokenMintToItself

Code:

// SPDX-License-Identifier: UNLICENSED
pragma solidity 0.8.17;
import "forge-std/Test.sol";
import "../contracts/Dependencies/EbtcMath.sol";
import {eBTCBaseFixture} from "./BaseFixture.sol";

contract EBTCTokenMintToItself is eBTCBaseFixture {
    uint256 public mintAmount = 1e18;

    function setUp() public override {
        eBTCBaseFixture.setUp();
        eBTCBaseFixture.connectCoreContracts();
        eBTCBaseFixture.connectLQTYContractsToCore();
    }

    function testEBTCUserWithMintingPermisionCanMint() public {
        address user = _utils.getNextUserAddress();
        // Grant mint permissions to user
        vm.prank(defaultGovernance);
        authority.setUserRole(user, 1, true);

        // Starting balance
        uint256 totalSupply0 = eBTCToken.totalSupply();
        uint256 balanceOfeBTCToken0 = eBTCToken.balanceOf(address(eBTCToken));
        console.log("\n********** Starting balance ************");
        console.log("eBTCToken.balanceOf(address(eBTCToken)) ",eBTCToken.balanceOf(address(eBTCToken)));

        vm.startPrank(user);
        vm.deal(user, type(uint96).max);

        // User can mint to eBTCToken
        eBTCToken.mint(address(eBTCToken), mintAmount);

        vm.stopPrank();

        // Check balance
        uint256 totalSupply1 = eBTCToken.totalSupply();
        uint256 balanceOfeBTCToken1 = eBTCToken.balanceOf(address(eBTCToken));

        console.log("\n********** Final balance ************");
        console.log("eBTCToken.balanceOf(address(eBTCToken)) ",eBTCToken.balanceOf(address(eBTCToken)));

        assertEq(totalSupply1 - totalSupply0, mintAmount);
        assertEq(balanceOfeBTCToken1 - balanceOfeBTCToken0, mintAmount);
        assertGt(balanceOfeBTCToken1,0);
    }
}

Last updated