Skip to content

Latest commit

 

History

History
225 lines (187 loc) · 7.1 KB

036.md

File metadata and controls

225 lines (187 loc) · 7.1 KB

Modern Hazel Buffalo

High

The "buyer" newer gets the receiptID ERC721 token from the "seller" in exchange for token, because the receipt ERC721 token remains stuck in the BuyOrder contract with no way to rescue / transfer it

Summary

Due to a mistake in the sellNFT function, the buyInformation.owner will never receive the purchased ERC721 token, as it will always be stuck in the address(this) buyOrder contract.

Root Cause

    function sellNFT(uint receiptID) public {
        require(buyInformation.isActive, "Buy order is not active");
        require(
            buyInformation.availableAmount > 0,
            "Buy order is not available"
        );

        IERC721(buyInformation.wantedToken).transferFrom(
            msg.sender,
            address(this),
            receiptID
        );

Internal pre-conditions

Generally none.

External pre-conditions

None.

Attack Path

  1. Stephany creates a buy order through the buyOrderFactory contract in order to sell 100e18 AERO tokens and get a veNFT in return, which would represent an equivalent amount of collateral tokens locked in a voting escrow.
  2. A instance of the buyOrder contract is programmatically deployed and initialized via buyOrderFactory when the createBuyOrder function is called.
  3. Naomi has a veNFT that fits Stephany's requirements, and she calls buyOrder's sellNFT method.
  4. In spite of exchange executed flawlessly, Stephany hasn't received the "bought" veNFT, because instead of transferring to Stephany, the buyOrder contract transferred the ERC721 token to itself.

Impact

Stephany never gets the "bought" veNFT, and just wastes the whole sold tokens amount meaninglessly.

Neither the buyOrder's sellNFT nor deleteBuyOrder are able to rescue the "bought" ERC721 token / transfer it to Stephany.

PoC

// file: buyOrderFactory.sol
// ...
    /**
     * @dev create buy order
     * @param _token token address you want to use to buy the wanted token
     * @param wantedToken the token address you want to buy
     * @param _amount amount of token you want to use to buy the wanted token
     * @param ratio ratio you want to use to buy the wanted token (5e17:0.5)
     */

    function createBuyOrder(
        address _token,
        address wantedToken,
        uint _amount,
        uint ratio
    ) public returns (address) {
        // CHECKS
        require(_amount > 0, "Amount must be greater than 0");
        require(ratio > 0, "Ratio must be greater than 0");

        DebitaProxyContract proxy = new DebitaProxyContract(
            implementationContract
        );
        BuyOrder _createdBuyOrder = BuyOrder(address(proxy));

        // INITIALIZE THE BUY ORDER
        _createdBuyOrder.initialize(
            msg.sender,
            _token,
            wantedToken,
            address(this),
            _amount,
            ratio
        );

        // TRANSFER TOKENS TO THE BUY ORDER
        SafeERC20.safeTransferFrom(
            IERC20(_token),
            msg.sender,
            address(_createdBuyOrder),
            _amount
        );

        // INDEX
        isBuyOrderLegit[address(_createdBuyOrder)] = true;
        BuyOrderIndex[address(_createdBuyOrder)] = activeOrdersCount;
        allActiveBuyOrders[activeOrdersCount] = address(_createdBuyOrder);
        activeOrdersCount++;
        historicalBuyOrders.push(address(_createdBuyOrder));

        emit BuyOrderCreated(
            address(_createdBuyOrder),
            msg.sender,
            wantedToken,
            _token,
            _amount,
            ratio
        );
        return address(_createdBuyOrder);
    }
// file: buyOrder.sol
// ...

    function initialize(
        address _owner,
        address _token,
        address wantedToken,
        address factory,
        uint _amount,
        uint ratio
    ) public initializer {
        buyInformation = BuyInfo({
            buyOrderAddress: address(this),
            wantedToken: wantedToken,
            buyRatio: ratio,
            availableAmount: _amount,
            capturedAmount: 0,
            owner: _owner,
            buyToken: _token,
            isActive: true
        });
        buyOrderFactory = factory;
    }

    function deleteBuyOrder() public onlyOwner {
        require(buyInformation.isActive, "Buy order is not active");
        // save amount on memory
        uint amount = buyInformation.availableAmount;
        buyInformation.isActive = false;
        buyInformation.availableAmount = 0;

        SafeERC20.safeTransfer(
            IERC20(buyInformation.buyToken),
            buyInformation.owner,
            amount
        );

        IBuyOrderFactory(buyOrderFactory)._deleteBuyOrder(address(this));
        IBuyOrderFactory(buyOrderFactory).emitDelete(address(this));
    }

    function sellNFT(uint receiptID) public {
        require(buyInformation.isActive, "Buy order is not active");
        require(
            buyInformation.availableAmount > 0,
            "Buy order is not available"
        );

        IERC721(buyInformation.wantedToken).transferFrom(
            msg.sender,
            address(this),
            receiptID
        );
        veNFR receipt = veNFR(buyInformation.wantedToken);
        veNFR.receiptInstance memory receiptData = receipt.getDataByReceipt(
            receiptID
        );
        uint collateralAmount = receiptData.lockedAmount;
        uint collateralDecimals = receiptData.decimals;

        uint amount = (buyInformation.buyRatio * collateralAmount) /
            (10 ** collateralDecimals);
        require(
            amount <= buyInformation.availableAmount,
            "Amount exceeds available amount"
        );

        buyInformation.availableAmount -= amount;
        buyInformation.capturedAmount += collateralAmount;
        uint feeAmount = (amount *
            IBuyOrderFactory(buyOrderFactory).sellFee()) / 10000;
        SafeERC20.safeTransfer(
            IERC20(buyInformation.buyToken),
            msg.sender,
            amount - feeAmount
        );

        SafeERC20.safeTransfer(
            IERC20(buyInformation.buyToken),
            IBuyOrderFactory(buyOrderFactory).feeAddress(),
            feeAmount
        );

        if (buyInformation.availableAmount == 0) {
            buyInformation.isActive = false;
            IBuyOrderFactory(buyOrderFactory).emitDelete(address(this));
            IBuyOrderFactory(buyOrderFactory)._deleteBuyOrder(address(this));
        } else {
            IBuyOrderFactory(buyOrderFactory).emitUpdate(address(this));
        }
    }

Mitigation

    function sellNFT(uint receiptID) public {
        require(buyInformation.isActive, "Buy order is not active");
        require(
            buyInformation.availableAmount > 0,
            "Buy order is not available"
        );

        IERC721(buyInformation.wantedToken).transferFrom(
            msg.sender,
-           address(this),
+           buyInformation.owner,
            receiptID
        );