OpenZeppelin Address library 분석

frenchkebab·2023년 5월 16일
0

EIP / Open Source

목록 보기
9/9

Intro

Openzeppelin의 Address library는 address 타입의 변수에 사용할 수 있는 라이브러리다.

low-level call을 사용할 때 return value를 사용하지 않는 등의 실수를 방지하고, 가독성을 높일 수 있다.

하나하나씩 살펴보자

1. isContract()

    /**
     * @dev Returns true if `account` is a contract.
     *
     * [IMPORTANT]
     * ====
     * It is unsafe to assume that an address for which this function returns
     * false is an externally-owned account (EOA) and not a contract.
     *
     * Among others, `isContract` will return false for the following
     * types of addresses:
     *
     *  - an externally-owned account
     *  - a contract in construction
     *  - an address where a contract will be created
     *  - an address where a contract lived, but was destroyed
     *
     * Furthermore, `isContract` will also return true if the target contract within
     * the same transaction is already scheduled for destruction by `SELFDESTRUCT`,
     * which only has an effect at the end of a transaction.
     * ====
     *
     * [IMPORTANT]
     * ====
     * You shouldn't rely on `isContract` to protect against flash loan attacks!
     *
     * Preventing calls from contracts is highly discouraged. It breaks composability, breaks support for smart wallets
     * like Gnosis Safe, and does not provide security since it can be circumvented by calling from a contract
     * constructor.
     * ====
     */
    function isContract(address account) internal view returns (bool) {
        // This method relies on extcodesize/address.code.length, which returns 0
        // for contracts in construction, since the code is only stored at the end
        // of the constructor execution.

        return account.code.length > 0;
    }

해당 account가 contract인지를 확인하는 함수이다.

해당 accountcode 길이를 검사한다.

부적절한 사용: 주소가 EOA일 경우만 작동

하지만 원본 코드의 주석에도 나와있는데, 다음의 경우에는 contract임에도 결과값이 false가 나올 수 있어 유의해야 한다.

  1. contractconstructor() 내부
    -> constructor에서 isContract()를 사용하는 함수를 external call 하였을 경우, 해당 contract가 아직 배포되기 이전이라, address만 배정되고 코드 길이는 여전히 0으로 나타나게 된다.

  2. 배포될 예정인 address일 경우
    -> create2 opcode를 사용하면 미리 코드를 배포할 주소를 계산할 수 있다. 따라서 isContract()가 실행되어 false을 return한 이후에 해당 주소로 contract가 배포될 가능성이 있다.

  3. destroycontract일 경우
    -> selfdestruct opcode를 사용하여 해당 contract의 bytecode가 제거된 경우에도 false가 return될 수 있다.
    (하지만 selfdestruct opcode의 경우 보안상의 이유로 deprecate되었다.)

contract로부터의 external call을 prevent하기 위해 사용하지 말라고 나와있다.

안전하게 사용할 수 있는 경우: 주소가 contract일 경우에만 작동

  • contract가 '아닌지'를 검사 -> X
    :위의 경우들을 보면 contract임에도 false를 return하도록 할 수 있다. 따라서 isContract()를 호출하는 함수에서 해당 주소가 contract가 아닌 EOA일 경우에만 동작하도록 하는 것은 부적절한 사용이다.

  • contract가 '맞는지'를 검사 -> O
    : 해당 주소가 contract일 경우에만 작동하도록 할 경우에는 isContract()를 안전하게 사용할 수 있다. EOA의 code.length가 0보다 클 수는 없기 때문이다.

isContract()true를 return하고 나서, selfdestruct를 이용하여 해당 contract의 코드를 없앨 수 있다고 나와있지만 selfdestruct는 0.8.18 코드에서 deprecate되었다.
(⚠️ 하지만 여전히 이전 버전을 사용하게 될 경우를 고려하면 이 경우도 무조건 안전하다고 생각할 수는 없다.)

sendValue()

    /**
     * @dev Replacement for Solidity's `transfer`: sends `amount` wei to
     * `recipient`, forwarding all available gas and reverting on errors.
     *
     * https://eips.ethereum.org/EIPS/eip-1884[EIP1884] increases the gas cost
     * of certain opcodes, possibly making contracts go over the 2300 gas limit
     * imposed by `transfer`, making them unable to receive funds via
     * `transfer`. {sendValue} removes this limitation.
     *
     * https://consensys.net/diligence/blog/2019/09/stop-using-soliditys-transfer-now/[Learn more].
     *
     * IMPORTANT: because control is transferred to `recipient`, care must be
     * taken to not create reentrancy vulnerabilities. Consider using
     * {ReentrancyGuard} or the
     * https://solidity.readthedocs.io/en/v0.8.0/security-considerations.html#use-the-checks-effects-interactions-pattern[checks-effects-interactions pattern].
     */
    function sendValue(address payable recipient, uint256 amount) internal {
        require(address(this).balance >= amount, "Address: insufficient balance");

        (bool success, ) = recipient.call{value: amount}("");
        require(success, "Address: unable to send value, recipient may have reverted");
    }

transfer 함수를 대체하여 사용하도록 만들어졌다.

참고로 transfer의 경우에는 return값이 없어 revert되면 전송에 실패한 것이고, revert되지 않으면 성공한 것이다.

send의 경우에는 transfer와 달리 boolean return값으로 성공 여부를 판별한다. (실패해도 revert되지 않는 문제점이 있다.)

두 경우 모두 forward할 수 있는 gas양이 2300으로 제한되어 있기 때문에, gas가 부족해서 transaction이 revert될 수 있는 가능성이 있다.
(receive() 함수에서 2300 gas 이상을 사용할 경우 문제가 된다.)

따라서 call을 사용하는 것이 권장되는데 이 경우 가독성이 다소 떨어지고, return 값을 반드시 체크해야하는 문제가 있다.

sendValue를 사용하면 transfer를 사용할 때처럼 보낼 value값만을 명시하여 편하게 사용할 수 있다.

functionCall()

    /**
     * @dev Performs a Solidity function call using a low level `call`. A
     * plain `call` is an unsafe replacement for a function call: use this
     * function instead.
     *
     * If `target` reverts with a revert reason, it is bubbled up by this
     * function (like regular Solidity function calls).
     *
     * Returns the raw returned data. To convert to the expected return value,
     * use https://solidity.readthedocs.io/en/latest/units-and-global-variables.html?highlight=abi.decode#abi-encoding-and-decoding-functions[`abi.decode`].
     *
     * Requirements:
     *
     * - `target` must be a contract.
     * - calling `target` with `data` must not revert.
     *
     * _Available since v3.1._
     */
    function functionCall(address target, bytes memory data) internal returns (bytes memory) {
        return functionCallWithValue(target, data, 0, "Address: low-level call failed");
    }

    /**
     * @dev Same as {xref-Address-functionCall-address-bytes-}[`functionCall`], but with
     * `errorMessage` as a fallback revert reason when `target` reverts.
     *
     * _Available since v3.1._
     */
    function functionCall(
        address target,
        bytes memory data,
        string memory errorMessage
    ) internal returns (bytes memory) {
        return functionCallWithValue(target, data, 0, errorMessage);
    }

내부적으로 functionCallWithValue에 그 값을 0을 넣어 호출해주고 있다.

두 개의 오버로딩의 경우errorMessage를 다로 명시해주는지의 유무 차이이다.

트랜잭션이 실패할 경우에는 revert된다.

성공하였고 return value가 있을 경우 bytes로 return한다.
(low-level call과 동일하게 리턴받지만, 성공 여부를 boolean으로 리턴하지 않고, transfer의 경우처럼 revert되는지 아닌지 여부로만 판단하면 됨)

functionCallWithValue()

    /**
     * @dev Same as {xref-Address-functionCall-address-bytes-}[`functionCall`],
     * but also transferring `value` wei to `target`.
     *
     * Requirements:
     *
     * - the calling contract must have an ETH balance of at least `value`.
     * - the called Solidity function must be `payable`.
     *
     * _Available since v3.1._
     */
    function functionCallWithValue(address target, bytes memory data, uint256 value) internal returns (bytes memory) {
        return functionCallWithValue(target, data, value, "Address: low-level call with value failed");
    }

    /**
     * @dev Same as {xref-Address-functionCallWithValue-address-bytes-uint256-}[`functionCallWithValue`], but
     * with `errorMessage` as a fallback revert reason when `target` reverts.
     *
     * _Available since v3.1._
     */
    function functionCallWithValue(
        address target,
        bytes memory data,
        uint256 value,
        string memory errorMessage
    ) internal returns (bytes memory) {
        require(address(this).balance >= value, "Address: insufficient balance for call");
        (bool success, bytes memory returndata) = target.call{value: value}(data);
        return verifyCallResultFromTarget(target, success, returndata, errorMessage);
    }

동일하게 errorMessage를 직접 명시해주는지 여부로 두 버전이 있다.

위의 functionCall에다가 value를 넣어줄 수 있다.

내부적으로 verifyCallREsultFromTarget을 호출하고 있으니 이것을 살펴보자.

verifyCallResultFromTarget()

    /**
     * @dev Tool to verify that a low level call to smart-contract was successful, and revert (either by bubbling
     * the revert reason or using the provided one) in case of unsuccessful call or if target was not a contract.
     *
     * _Available since v4.8._
     */
    function verifyCallResultFromTarget(
        address target,
        bool success,
        bytes memory returndata,
        string memory errorMessage
    ) internal view returns (bytes memory) {
        if (success) {
            if (returndata.length == 0) {
                // only check isContract if the call was successful and the return data is empty
                // otherwise we already know that it was a contract
                require(isContract(target), "Address: call to non-contract");
            }
            return returndata;
        } else {
            _revert(returndata, errorMessage);
        }
    }

코드를 보면 returndata를 return해주거나 errorMessage를 띄워준다.

_revert()

    function _revert(bytes memory returndata, string memory errorMessage) private pure {
        // Look for revert reason and bubble it up if present
        if (returndata.length > 0) {
            // The easiest way to bubble the revert reason is using memory via assembly
            /// @solidity memory-safe-assembly
            assembly {
                let returndata_size := mload(returndata)
                revert(add(32, returndata), returndata_size)
            }
        } else {
            revert(errorMessage);
        }
    }

revert가 발생할 경우, (call에서 false가 return되어)
errormessage를 띄워준다.
만일, revert reason이 return되었으면 그것을 띄워준다.

이것들외에도 staticcall, delegatecall을 위한 함수들이 있으나 동일한 원리로 작동하므로 따로 설명하지 않는다.

필요하다면 OpenZeppelin의 Address.sol을 살펴보면 된다!

profile
Blockchain Dev Journey

0개의 댓글