[Solidity] Fallback, Call, Delegatecall, Function Selector, Calling Other Contract

jhcha·2023년 8월 2일
0

Solidity

목록 보기
9/17
post-thumbnail

Fallback

url: https://solidity-by-example.org/fallback/

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.17;

contract Fallback {
    event Log(string func, uint gas);

    // Fallback function must be declared as external.
    fallback() external payable {
        // send / transfer (forwards 2300 gas to this fallback function)
        // call (forwards all of the gas)
        emit Log("fallback", gasleft());
    }

    // Receive is a variant of fallback that is triggered when msg.data is empty
    receive() external payable {
        emit Log("receive", gasleft());
    }

    // Helper function to check the balance of this contract
    function getBalance() public view returns (uint) {
        return address(this).balance;
    }
}

contract SendToFallback {
    function transferToFallback(address payable _to) public payable {
        _to.transfer(msg.value);
    }

    function callFallback(address payable _to) public payable {
        (bool sent, ) = _to.call{value: msg.value}("");
        require(sent, "Failed to send Ether");
    }
}
pragma solidity ^0.8.17;

// TestFallbackInputOutput -> FallbackInputOutput -> Counter
contract FallbackInputOutput {
    address immutable target;

    constructor(address _target) {
        target = _target;
    }

    fallback(bytes calldata data) external payable returns (bytes memory) {
        (bool ok, bytes memory res) = target.call{value: msg.value}(data);
        require(ok, "call failed");
        return res;
    }
}

contract Counter {
    uint public count;

    function get() external view returns (uint) {
        return count;
    }

    function inc() external returns (uint) {
        count += 1;
        return count;
    }
}

contract TestFallbackInputOutput {
    event Log(bytes res);

    function test(address _fallback, bytes calldata data) external {
        (bool ok, bytes memory res) = _fallback.call(data);
        require(ok, "call failed");
        emit Log(res);
    }

    function getTestData() external pure returns (bytes memory, bytes memory) {
        return (abi.encodeCall(Counter.get, ()), abi.encodeCall(Counter.inc, ()));
    }
}

Fallback은 다음과 같은 경우에 실행되는 특수 기능이다.

  • 존재하지 않는 함수가 호출되거나
  • 이더를 보낼 때 receive() 함수가 없거나 msg.data가 비어있지 않은 경우

receive와 fallback 함수의 실행 호출 순위는 다음과 같다.

          send Ether
               |
         msg.data is empty?
              / \
            yes  no
            /     \
receive() exists?  fallback()
         /   \
        yes   no
        /      \
    receive()   fallback()
    */

receive는 이더를 보낼 때 msg.data가 비어있고, receive 함수가 존재하는 경우 실행된다. 그 외, receive가 없거나 msg.data가 비어있지 않으면 fallback이 실행된다.

Fallback은 transfer 혹은 send를 통해 호출되는 경우 2300 gas limit를 가진다.

Call

url: https://solidity-by-example.org/call/

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.17;

contract Receiver {
    event Received(address caller, uint amount, string message);

    fallback() external payable {
        emit Received(msg.sender, msg.value, "Fallback was called");
    }

    function foo(string memory _message, uint _x) public payable returns (uint) {
        emit Received(msg.sender, msg.value, _message);

        return _x + 1;
    }
}

contract Caller {
    event Response(bool success, bytes data);

    // Let's imagine that contract Caller does not have the source code for the
    // contract Receiver, but we do know the address of contract Receiver and the function to call.
    function testCallFoo(address payable _addr) public payable {
        // You can send ether and specify a custom gas amount
        (bool success, bytes memory data) = _addr.call{value: msg.value, gas: 5000}(
            abi.encodeWithSignature("foo(string,uint256)", "call foo", 123)
        );

        emit Response(success, data);
    }

    // Calling a function that does not exist triggers the fallback function.
    function testCallDoesNotExist(address payable _addr) public payable {
        (bool success, bytes memory data) = _addr.call{value: msg.value}(
            abi.encodeWithSignature("doesNotExist()")
        );

        emit Response(success, data);
    }
}

Call은 다른 컨트랙트와 상호작용하기 위한 저수준 (low level) 기능이다.
일반적으로 Fallback() 함수를 호출하여 Ether를 전송할 때 권장되는 방법이고, 기존 함수를 호출하는 방법으로는 권장하지 않는다.
Call이 권장되지 않는 이유는 다음과 같다.

  • 유형 검사가 우회됨.
  • 함수 존재가 생략된다.
  • Reverts are not bubbled up(?)

먼저, Receiver 컨트랙트에 payable Fallback 함수는 존재하지만 Receive 함수가 없어서 다음과 같은 warning 문구가 발생한다.

testCallFoo 함수를 통해 Receiver 컨트랙트를 호출하면 Receiver.foo 함수가 동작하는 것을 event Received 로그로 확인할 수 있다.

testCallDoesNotExist 함수를 호출하면 Receiver의 fallback 함수가 동작하는 것을 event Received 로그로 확인할 수 있다.

Soldiity Call에 대해 설명이 정리되어 있는 글
참고자료: https://velog.io/@octo__/Solidity-call-function

Delegatecall

url: https://solidity-by-example.org/delegatecall/

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.17;

// NOTE: Deploy this contract first
contract B {
    // NOTE: storage layout must be the same as contract A
    uint public num;
    address public sender;
    uint public value;

    function setVars(uint _num) public payable {
        num = _num;
        sender = msg.sender;
        value = msg.value;
    }
}

contract A {
    uint public num;
    address public sender;
    uint public value;

    function setVars(address _contract, uint _num) public payable {
        // A's storage is set, B is not modified.
        (bool success, bytes memory data) = _contract.delegatecall(
            abi.encodeWithSignature("setVars(uint256)", _num)
        );
    }
}

delegatecall은 call과 유사한 저수준 (low level) 함수이다.
A 컨트랙트가 delegatecall을 통해 B 컨트랙트를 호출하면 B의 코드가 실행된다.
코드 주석에 따라 B 컨트랙트를 먼저 배포한 후 A 컨트랙트를 배포한다.
다음, A 컨트랙트의 setVars를 호출하면 B 컨트랙트의 setVars 함수가 실행되지만 변경되는 상태변수는 B 컨트랙트가 아닌 delegatecall을 실행한 A 컨트랙트의 상태변수만 변경된다.
이 때, B 컨트랙트의 상태변수는 A의 상태변수와 storage layout (상태 변수 선언 상태)이 같아야 한다.

Function Selector

url: https://solidity-by-example.org/function-selector/

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.17;

contract FunctionSelector {
    /*
    "transfer(address,uint256)"
    0xa9059cbb
    "transferFrom(address,address,uint256)"
    0x23b872dd
    */
    function getSelector(string calldata _func) external pure returns (bytes4) {
        return bytes4(keccak256(bytes(_func)));
    }
}

함수가 호출될 때, calldata의 첫 4바이트가 호출할 함수를 지정한다.
해당 4 바이트를 function selector 라고 한다.
예를 들어 아래 코드에서 call을 통해 transfer을 실행시킨다.

addr.call(abi.encodeWithSignature("transfer(address,uint256)", 0xSomeAddress, 123))

이 때, abi.encodeWithSignature(...) 리턴 값의 첫 4 바이트는 function selector이다.
function selector는 keccak256 해시 결과값의 첫 4 바이트를 사용한다.

    function getSelector(string calldata _func) external pure returns (bytes4) {
        return bytes4(keccak256(bytes(_func)));
    }


따라서, getSelector _func로 "transfer(address,uint256)"를 입력했을 때 0xa9059cbb 값이 리턴되는 것을 알 수 있고, 이 것이 transfer(address,uint256) 함수의 function selector 값이다.

// solidity 에서 function selector를 알아내는 연산 방법
bytes4(keccak256(bytes(_func)))

Calling Other Contract

url: https://solidity-by-example.org/calling-contract/


// SPDX-License-Identifier: MIT
pragma solidity ^0.8.17;

contract Callee {
    uint public x;
    uint public value;

    function setX(uint _x) public returns (uint) {
        x = _x;
        return x;
    }

    function setXandSendEther(uint _x) public payable returns (uint, uint) {
        x = _x;
        value = msg.value;

        return (x, value);
    }
}

contract Caller {
    function setX(Callee _callee, uint _x) public {
        uint x = _callee.setX(_x);
    }

    function setXFromAddress(address _addr, uint _x) public {
        Callee callee = Callee(_addr);
        callee.setX(_x);
    }

    function setXandSendEther(Callee _callee, uint _x) public payable {
        (uint x, uint value) = _callee.setXandSendEther{value: msg.value}(_x);
    }
}

Solidity는 컨트랙트에서 다른 컨트랙트를 호출하기 위한 2개의 방식을 지원한다.
가장 쉬운 방법은 다음과 같이 직접 호출하는 방식이다.

A.foo(x, y, z);

다른 방법은 저수준 (low-level) 함수, call을 사용해서 호출할 수 있다.

    function setX(Callee _callee, uint _x) public {
        uint x = _callee.setX(_x);
    }

특이사항으로 매개변수에 Callee _callee라고 받지만, 실제로 address처럼 작동한다.
컨트랙트 명이 callee가 아닌 다른 컨트랙트를 생성 후에 address값을 매개변수로 넣고 확인한 결과, 정상적으로 동작한다.

0개의 댓글