sherlock-audit / 2024-03-zivoe-judging

8 stars 6 forks source link

Timenov - User can send `distributedAsset` to DAO. #60

Closed sherlock-admin4 closed 6 months ago

sherlock-admin4 commented 6 months ago

Timenov

medium

User can send distributedAsset to DAO.

Summary

The ZivoeYDL contract is used to manage the accounting for distributing yield across multiple contracts. The distributeYield function can be called by anyone, but the contract must be unlocked and the block.timestamp must be >= lastDistribution + daysBetweenDistributions * 86400. This will send the distributedAsset to addresses that are in protocolRecipients and residualRecipients. However there is a function returnAsset which accepts an asset parameter(must not be the distributedAsset) and sends it to the DAO. User can take benefit of the current implementation and send the distributedAsset by frontrunning.

Vulnerability Detail

Consider the following scenario:

  1. ZivoeYDL has a balance of DAI 100k dollars and USDC 50k dollars.
  2. The current distributedAsset is DAI and distributeYield function is called.
  3. The distributedAsset is updated to USDC.
  4. Malicious user sees tx in step 3 and calls returnAsset.
  5. All of USDC is sent to DAO.
  6. The call to distributeYield will not send any tokens to recipients.

Another problem is that DAO can move tokens only to a locker. This means that recipients will not receive their USDC that they should.

Impact

Users not receiving yield and tokens "locked" in DAO.

Code Snippet

    function distributeYield() external nonReentrant {
        require(unlocked, "ZivoeYDL::distributeYield() !unlocked"); 
        require(
            block.timestamp >= lastDistribution + daysBetweenDistributions * 86400, 
            "ZivoeYDL::distributeYield() block.timestamp < lastDistribution + daysBetweenDistributions * 86400"
        );

        // Calculate protocol earnings.
        uint256 earnings = IERC20(distributedAsset).balanceOf(address(this));
        uint256 protocolEarnings = protocolEarningsRateBIPS * earnings / BIPS;
        uint256 postFeeYield = earnings.floorSub(protocolEarnings);

        // Update timeline.
        distributionCounter += 1;
        lastDistribution = block.timestamp;

        // Calculate yield distribution (trancheuse = "slicer" in French).
        (
            uint256[] memory _protocol, uint256 _seniorTranche, uint256 _juniorTranche, uint256[] memory _residual
        ) = earningsTrancheuse(protocolEarnings, postFeeYield); 

        emit YieldDistributed(_protocol, _seniorTranche, _juniorTranche, _residual);

        // Update ema-based supply values.
        (uint256 aSTT, uint256 aJTT) = IZivoeGlobals_YDL(GBL).adjustedSupplies();
        emaSTT = MATH.ema(emaSTT, aSTT, retrospectiveDistributions.min(distributionCounter));
        emaJTT = MATH.ema(emaJTT, aJTT, retrospectiveDistributions.min(distributionCounter));

        // Distribute protocol earnings.
        for (uint256 i = 0; i < protocolRecipients.recipients.length; i++) {
            address _recipient = protocolRecipients.recipients[i];
            if (_recipient == IZivoeGlobals_YDL(GBL).stSTT() ||_recipient == IZivoeGlobals_YDL(GBL).stJTT()) {
                IERC20(distributedAsset).safeIncreaseAllowance(_recipient, _protocol[i]);
                IZivoeRewards_YDL(_recipient).depositReward(distributedAsset, _protocol[i]);
                emit YieldDistributedSingle(distributedAsset, _recipient, _protocol[i]);
            }
            else if (_recipient == IZivoeGlobals_YDL(GBL).stZVE()) {
                uint256 splitBIPS = (
                    IERC20(IZivoeGlobals_YDL(GBL).stZVE()).totalSupply() * BIPS
                ) / (
                    IERC20(IZivoeGlobals_YDL(GBL).stZVE()).totalSupply() + 
                    IERC20(IZivoeGlobals_YDL(GBL).vestZVE()).totalSupply()
                );
                uint stZVEAllocation = _protocol[i] * splitBIPS / BIPS;
                uint vestZVEAllocation = _protocol[i] * (BIPS - splitBIPS) / BIPS;
                IERC20(distributedAsset).safeIncreaseAllowance(IZivoeGlobals_YDL(GBL).stZVE(), stZVEAllocation);
                IERC20(distributedAsset).safeIncreaseAllowance(IZivoeGlobals_YDL(GBL).vestZVE(),vestZVEAllocation);
                IZivoeRewards_YDL(IZivoeGlobals_YDL(GBL).stZVE()).depositReward(distributedAsset, stZVEAllocation);
                IZivoeRewards_YDL(IZivoeGlobals_YDL(GBL).vestZVE()).depositReward(distributedAsset, vestZVEAllocation);
                emit YieldDistributedSingle(distributedAsset, IZivoeGlobals_YDL(GBL).stZVE(), stZVEAllocation);
                emit YieldDistributedSingle(distributedAsset, IZivoeGlobals_YDL(GBL).vestZVE(), vestZVEAllocation);
            }
            else {
                IERC20(distributedAsset).safeTransfer(_recipient, _protocol[i]);
                emit YieldDistributedSingle(distributedAsset, _recipient, _protocol[i]);
            }
        }

        // Distribute senior and junior tranche earnings.
        IERC20(distributedAsset).safeIncreaseAllowance(IZivoeGlobals_YDL(GBL).stSTT(), _seniorTranche);
        IERC20(distributedAsset).safeIncreaseAllowance(IZivoeGlobals_YDL(GBL).stJTT(), _juniorTranche);
        IZivoeRewards_YDL(IZivoeGlobals_YDL(GBL).stSTT()).depositReward(distributedAsset, _seniorTranche);
        IZivoeRewards_YDL(IZivoeGlobals_YDL(GBL).stJTT()).depositReward(distributedAsset, _juniorTranche);
        emit YieldDistributedSingle(distributedAsset, IZivoeGlobals_YDL(GBL).stSTT(), _seniorTranche);
        emit YieldDistributedSingle(distributedAsset, IZivoeGlobals_YDL(GBL).stJTT(), _juniorTranche);

        // Distribute residual earnings.
        for (uint256 i = 0; i < residualRecipients.recipients.length; i++) {
            if (_residual[i] > 0) {
                address _recipient = residualRecipients.recipients[i];
                if (_recipient == IZivoeGlobals_YDL(GBL).stSTT() ||_recipient == IZivoeGlobals_YDL(GBL).stJTT()) {
                    IERC20(distributedAsset).safeIncreaseAllowance(_recipient, _residual[i]);
                    IZivoeRewards_YDL(_recipient).depositReward(distributedAsset, _residual[i]);
                    emit YieldDistributedSingle(distributedAsset, _recipient, _protocol[i]);
                }
                else if (_recipient == IZivoeGlobals_YDL(GBL).stZVE()) {
                    uint256 splitBIPS = (
                        IERC20(IZivoeGlobals_YDL(GBL).stZVE()).totalSupply() * BIPS
                    ) / (
                        IERC20(IZivoeGlobals_YDL(GBL).stZVE()).totalSupply() + 
                        IERC20(IZivoeGlobals_YDL(GBL).vestZVE()).totalSupply()
                    );
                    uint stZVEAllocation = _residual[i] * splitBIPS / BIPS;
                    uint vestZVEAllocation = _residual[i] * (BIPS - splitBIPS) / BIPS;
                    IERC20(distributedAsset).safeIncreaseAllowance(IZivoeGlobals_YDL(GBL).stZVE(), stZVEAllocation);
                    IERC20(distributedAsset).safeIncreaseAllowance(IZivoeGlobals_YDL(GBL).vestZVE(), vestZVEAllocation);
                    IZivoeRewards_YDL(IZivoeGlobals_YDL(GBL).stZVE()).depositReward(distributedAsset, stZVEAllocation);
                    IZivoeRewards_YDL(IZivoeGlobals_YDL(GBL).vestZVE()).depositReward(distributedAsset, vestZVEAllocation);
                    emit YieldDistributedSingle(distributedAsset, IZivoeGlobals_YDL(GBL).stZVE(), stZVEAllocation);
                    emit YieldDistributedSingle(distributedAsset, IZivoeGlobals_YDL(GBL).vestZVE(), vestZVEAllocation);
                }
                else {
                    IERC20(distributedAsset).safeTransfer(_recipient, _residual[i]);
                    emit YieldDistributedSingle(distributedAsset, _recipient, _residual[i]);
                }
            }
        }
    }

https://github.com/sherlock-audit/2024-03-zivoe/blob/main/zivoe-core-foundry/src/ZivoeYDL.sol#L213-L311

    function returnAsset(address asset) external {
        require(asset != distributedAsset, "ZivoeYDL::returnAsset() asset == distributedAsset");
        emit AssetReturned(asset, IERC20(asset).balanceOf(address(this)));
        IERC20(asset).safeTransfer(IZivoeGlobals_YDL(GBL).DAO(), IERC20(asset).balanceOf(address(this)));
    }

https://github.com/sherlock-audit/2024-03-zivoe/blob/main/zivoe-core-foundry/src/ZivoeYDL.sol#L314-L318

Tool used

Manual Review

Recommendation

Restrict returnAsset to be called only by trusted entity.

pseudonaut commented 6 months ago

Not relevant, or of concern

sherlock-admin2 commented 6 months ago

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

panprog commented:

invalid, there is no loss of funds, assets are simply transferred to DAO. If distributedAsset is changed, admins are trusted to do this correctly, like changing distributedAsset and only then pushing new token. Even if the token ends up in DAO, there is no problem to push it back.