sherlock-2023-08-cooler-h04

[H-04] isCoolerCallback can be bypassed

보고서

Summary

CoolerCallback 추상 컨트랙트를 상속하여 구체화하지 않은 컨트랙트를 콜백 컨트랙트로 등록할 수 있다. onRepay()onRoll()를 구현하지 않아 채무자가 대출을 상환할 수 없고(트랜잭션 취소 됨), 이를 통해 대여자가 의도적으로 채무 불이행 상태을 만들고 담보 토큰을 가져갈 수 있다.

Keyword

lack of input validation, logic flaw, lending protocol

Vulnerability

clearRequest 함수는 요청받은 대출 요청을 수락하는 함수이다. 대여자가 호출하므로 msg.sender는 대여자이다.

isCallback_ 파라미터를 true로 넘기면 msg.sender(대여자)가 컨트랙트이며, 주요 함수가 호출되었을 때 대여자의 콜백 함수를 호출해달라는 의미이다.

이 때, 대여자 컨트랙트가 CoolerCallback 추상 컨트랙트의 구현체인지 확인하기 위해 isCoolerCallback() 를 이용한다.

function clearRequest(
    uint256 reqID_,
    bool repayDirect_,
@>  bool isCallback_
) external returns (uint256 loanID) {
    ...
 
    // If necessary, ensure lender implements the CoolerCallback abstract.
@>  if (isCallback_ && !CoolerCallback(msg.sender).isCoolerCallback()) revert NotCoolerCallback();
 
    ...
}

그러나 이 함수는 어떠한 보호 기능도 제공하지 않는다. 대여자가 CoolerCallback 컨트랙트를 상속하지 않고, 단순히 isCoolerCallback() 함수를 구현하고 true를 반환한다면 우회할 수 있다.

예를 들어 다음과 같은 악성 컨트랙트를 작성할 수 있다.

contract maliciousLender {
@>  function isCoolerCallback() pure returns(bool) {
        return true;
    }
 
    function operation(
        address _to,
        uint256 reqID_
    ) public {
        Cooler(_to).clearRequest(reqID_, true, true);
    }
 
    function onDefault(uint256 loanID_, uint256 debt, uint256 collateral) public {}
}

대여자가 의도적으로 onDefault() 함수만 구현하고 onRepay()onRoll() 를 구현하지 않는다면 repayLoan()rollLoan() 함수는 항상 실패하게 된다.

이로 인해 채무자는 대출을 상환할 수 없어 채무 불이행 상태가 된다. 대출 불이행 상태가 되면 대여자(공격자)가 claimDefault()를 호출하여 담보를 청구할 수 있다.

또한, 대여자가 대출 소유권 이전을 하는 방식으로도 isCoolerCallback() 를 우회할 수 있다. 처음에는 적법해보이는 컨트랙트로 대출을 허용한 뒤 콜백을 구현하지 않은 컨트랙트나 EOA에게 대출 소유권을 이전했을 때, loan.callback 플래그는 여전히 true로 설정되어 있다.

/// @notice Approve transfer of loan ownership rights to a new address.
/// @param  to_ address to be approved.
/// @param  loanID_ index of loan in loans[].
function approveTransfer(address to_, uint256 loanID_) external {
    if (msg.sender != loans[loanID_].lender) revert OnlyApproved();
 
    // Update transfer approvals.
    approvals[loanID_] = to_;
}
 
/// @notice Execute loan ownership transfer. Must be previously approved by the lender.
/// @param  loanID_ index of loan in loans[].
function transferOwnership(uint256 loanID_) external {
    if (msg.sender != approvals[loanID_]) revert OnlyApproved();
 
    // Update the load lender.
    loans[loanID_].lender = msg.sender;
    // Clear transfer approvals.
    approvals[loanID_] = address(0);
}

Impact

대여자가 담보 토큰을 받기 위해 강제로 채무 불이행 상태로 만들고, 채무자는 담보 토큰을 잃는다.

Mitigation

  • 프로토콜에서 신뢰하는 주소에서만 콜백을 허용한다.
  • loan.callback이 true로 설정된 경우 대출 소유권 이전 기능을 비활성화 한다.

tags: bughunting, olympus dao, smart contract, solidity, lending protocol, lack-of-input-validation-vul, logic flaw, severity high