sherlock-audit / 2022-10-astaria-judging

6 stars 1 forks source link

Jeiwan - A buyout is paid by liquidity providers, not by a borrower #269

Closed sherlock-admin closed 1 year ago

sherlock-admin commented 2 years ago

Jeiwan

high

A buyout is paid by liquidity providers, not by a borrower

Summary

A buyout is paid by liquidity providers, not by a borrower

Vulnerability Detail

A borrower is allowed to buy out their lien to apply new loan terms to it (VaultImplementation.sol#L280). To buy out a lien, the full lien's debt must be paid (LienToken.sol#L143-L148). However, when buying out via the vault contract, it's the vault that pays the buyout:

  1. Vault calls buyoutLien on the LienToken contract (VaultImplementation.sol#L301-L303):
    IAstariaRouter(ROUTER()).LIEN_TOKEN().buyoutLien(
      ILienBase.LienActionBuyout(incomingTerms, position, recipient())
    );
  2. And the LienToken contract transfers the buyout amount from msg.sender, i.e. the vault (LienToken.sol#L143-L148):
    TRANSFER_PROXY.tokenTransferFrom(
      WETH,
      address(msg.sender), // @audit repaid by vault when called from a vault
      getPayee(lienId),
      uint256(buyout)
    );
  3. Before making the call, the vault checks if it has enough tokens–these tokens belong to liquidity providers (VaultImplementation.sol#L290-L293):
    require(
      buyout <= ERC20(underlying()).balanceOf(address(this)),
      "not enough balance to buy out loan"
    );
  4. The vault also approves spending to LienToken (VaultImplementation.sol#L297-L300):

    ERC20(underlying()).safeApprove(
      address(IAstariaRouter(ROUTER()).TRANSFER_PROXY()),
      buyout
    );

    Impact

    Borrower buys out a lien for free, liquidity providers lose money since buyouts are paid from their deposits.

    Code Snippet

    VaultImplementation.sol#L280:

    function buyoutLien(
    uint256 collateralId,
    uint256 position,
    IAstariaRouter.Commitment calldata incomingTerms
    ) external whenNotPaused {
    (, uint256 buyout) = IAstariaRouter(ROUTER()).LIEN_TOKEN().getBuyout(
    collateralId,
    position
    );
    
    require(
    buyout <= ERC20(underlying()).balanceOf(address(this)), // @audit buyout paid by the vault
    "not enough balance to buy out loan"
    );
    
    _validateCommitment(incomingTerms, recipient());
    
    ERC20(underlying()).safeApprove(
    address(IAstariaRouter(ROUTER()).TRANSFER_PROXY()),
    buyout
    );
    IAstariaRouter(ROUTER()).LIEN_TOKEN().buyoutLien(
    ILienBase.LienActionBuyout(incomingTerms, position, recipient())
    );
    }

LienToken.sol#L121:

function buyoutLien(ILienToken.LienActionBuyout calldata params) external {
  (bool valid, IAstariaRouter.LienDetails memory ld) = ASTARIA_ROUTER
    .validateCommitment(params.incoming);

  if (!valid) {
    revert InvalidTerms();
  }

  uint256 collateralId = params.incoming.tokenContract.computeId(
    params.incoming.tokenId
  );
  (uint256 owed, uint256 buyout) = getBuyout(collateralId, params.position);
  uint256 lienId = liens[collateralId][params.position];

  //the borrower shouldn't incur more debt from the buyout than they already owe
  if (ld.maxAmount < owed) {
    revert InvalidBuyoutDetails(ld.maxAmount, owed);
  }
  if (!ASTARIA_ROUTER.isValidRefinance(lienData[lienId], ld)) {
    revert InvalidRefinance();
  }

  TRANSFER_PROXY.tokenTransferFrom(
    WETH,
    address(msg.sender), // @audit repaid by vault when called from a vault
    getPayee(lienId),
    uint256(buyout)
  );

  lienData[lienId].last = block.timestamp.safeCastTo32();
  lienData[lienId].start = block.timestamp.safeCastTo32();
  lienData[lienId].rate = ld.rate.safeCastTo240();
  lienData[lienId].duration = ld.duration.safeCastTo32();

  _transfer(ownerOf(lienId), address(params.receiver), lienId);
}

Tool used

Manual Review

Recommendation

When a borrower buys out a lien, ensure it's the borrower who repays the lien's debt and the buyout fee.

SantiagoGregory commented 1 year ago

This is intentional: There is no point for the borrower to buy out their own lien since they have to pay all of their accrued debt + the additional 10% of remaining interest (no longer capped at 60 days in our latest cut). The borrower could just pay their accrued debt if they wanted to get rid of the lien. The purpose of the buyout is for other vaults (or potentially the same vault) to buy out a lien and provide better terms if they think their new terms will provide more capital in the end once the new loan is repaid.