체인의정석

Openzepplin - Address 라이브러리 분석 본문

블록체인/Solidity

Openzepplin - Address 라이브러리 분석

체인의정석 2022. 10. 27. 14:24
728x90
반응형

zapper 코드를 분석하다가 Address 부분을 발견하여 글로 한번 남겨본다.

 

https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/utils/Address.sol

 

GitHub - OpenZeppelin/openzeppelin-contracts: OpenZeppelin Contracts is a library for secure smart contract development.

OpenZeppelin Contracts is a library for secure smart contract development. - GitHub - OpenZeppelin/openzeppelin-contracts: OpenZeppelin Contracts is a library for secure smart contract development.

github.com

     * [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;
    }

지갑 구조에서 EOA는 스토리지가 없기 때문에 이를 이용해서 컨트렉트 인지 보는 부분인데 여기서 플래시론 공격을 가지고 체크하는 로직이 있나보다. + 컨트렉트 생성자에서 하는 호출 및 gnosis safe의 멀티시그 월렛에서 서비스를 사용못하는 문제가있다고 한다.

 

생성자 호출에 대해서는 주석이 더 달려있는데

        // 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.

이 부분을 보면 컨트렉트가 생성되는 생성자 부분에서는 생성자를 실행하는 마지막 단계에서 코드가 저장된다고 나와 있다.

한마디로 컨트렉트 호출을 생성자에서 한번 쏠때는 codesize가 0인 상태로 트랜잭션이 갈 수 있다고 볼 수 있다.

 

그럼 isContract를 하고 싶다면 어떻게 해야할까?

바로 

msg.sender == tx.origin을 써야 한다고 한다.

tx.origin을 직접 명시하면 누가 서명했는지를 알 수 있다고 한다.

 

또한 보통 컨트렉트 형태의 지갑은 일반인들이 잘 사용하지 않지만 gnosis safe는 일반 유저들도 쉽게 멀티시그 지갑을 제공해 주고 있어서 많이 사용되는것 같다. 아무튼 지갑이라 해서 EOA만 있지는 않아서 그런것 같다.

 

다음 eth send 할 때 gas limt 관련 sendValue를 따로 만들어 놨다.

이건 매우 유명한 오류인데 transfer 함수라고 이더리움을 보내는 함수 가 잇는데 이때 이걸 보통 call을 사용해서 구현한다. 그 이유는 내장된 tansfer 구문의 경우 하드포크 이후에 영향을 받아 2300가스 리밋을 넘어가서 안되는 경우가 발생하기 때문이다. 

 

아무튼 이더리움 전송해야한다? 오픈제플린의 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.5.11/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");
    }

 그 다음 functionCall

그냥 call을 부르는 것은 안전하지 않기 때문에 이 함수를 쓰라고 한다.

만약 대상이 revert reason과 함께 반환을 하게 되면 bubble up? 더 명확해진다는 뜻인거 같은데

아무튼 이걸 사용해서 call을 하면 일반적인 솔리디티 call에서 revert 메세지를 리턴할때 정상적으로 수행해줄 수 있다. 이런 의미 같다.
https://www.macmillandictionary.com/dictionary/british/bubble-up

 

BUBBLE UP (phrasal verb) definition and synonyms | Macmillan Dictionary

Definition of BUBBLE UP (phrasal verb): increase and become more obvious

www.macmillandictionary.com

또한 리턴도 raw data를 리텅해서 정확히 받는 값과 같이 되게 나오는 것이라고 한다. 

 

여기서 target은 당연히 컨트렉트일 것이고,

target을 data로 불렀을 때 revert가 나면 안된다는 조건이 걸려있다.

이게 정말 call을 종류별로 다 쪼개놨는데 조건이 다 다르다.

    /**
     * @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);
    }

그 다음은 delegateCall을 하는 부분인데 나오는 응답값이 성공적으로 나온다고 해도 컨트렉트로 보내는 경우가 아닌 일반 지갑주소가 보낸 경우는 잡아낸다.

    /**
     * @dev Same as {xref-Address-functionCall-address-bytes-}[`functionCall`],
     * but performing a delegate call.
     *
     * _Available since v3.4._
     */
    function functionDelegateCall(address target, bytes memory data) internal returns (bytes memory) {
        return functionDelegateCall(target, data, "Address: low-level delegate call failed");
    }

    /**
     * @dev Same as {xref-Address-functionCall-address-bytes-string-}[`functionCall`],
     * but performing a delegate call.
     *
     * _Available since v3.4._
     */
    function functionDelegateCall(
        address target,
        bytes memory data,
        string memory errorMessage
    ) internal returns (bytes memory) {
        (bool success, bytes memory returndata) = target.delegatecall(data);
        return verifyCallResultFromTarget(target, success, returndata, errorMessage);
    }
    
        /**
     * @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);
        }
    }

마지막으로 verifyCallResult가 있는데 revert 이유를 명확히 제시해주거나 나온 에러를 사용하여 보여주는 것이라고 할 수 있다.

    /**
     * @dev Tool to verify that a low level call was successful, and revert if it wasn't, either by bubbling the
     * revert reason or using the provided one.
     *
     * _Available since v4.3._
     */
    function verifyCallResult(
        bool success,
        bytes memory returndata,
        string memory errorMessage
    ) internal pure returns (bytes memory) {
        if (success) {
            return returndata;
        } else {
            _revert(returndata, errorMessage);
        }
    }

    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);
        }
    }

일단 성공이 안된 상황인데 데이터가 있다면 assembly로 다시 한번 접근을 해서 데이터 사이즈를 가져와서 이를 revert에 태워서 오류 메세지를 보내준다.

 

 

728x90
반응형
Comments