이전글
receive, fallback, delegatecall
이번 글에서는 receive, fallback, delegatecall에 대해 알아보겠습니다.
1. 솔리디티 이더 송수신 정리
receive와 fallback에 대해 알아보기 전에 먼저 솔리디티에서의 이더 송수신을 정리해 보겠습니다.
- EOA에서 EOA로의 이더 전송은 스마트 컨트랙트를 거치지 않아도 됩니다.
- EOA에서 CA로의 이더 전송은 payable이 붙은 생성자와 함수를 이용해야 합니다. 생성자에 payable이 붙은 경우 컨트랙트 생성과정에서 CA로 이더를 전송할 수 있으며, 함수에 payable이 붙은 경우 함수 호출 과정에서 CA로 이더를 전송할 수 있습니다.
- CA에서 EOA로의 이더 전송은 transfer, send, call 함수를 이용할 수 있습니다.
이제 이번 글의 주제인 CA에서 CA로 이더를 전송하는 방법에 대해 알아보겠습니다. 이더를 전송하는 CA와 이더를 전송받는 CA 두 개의 컨트랙트가 필요하므로, 컨트랙트를 두 개 작성하여 배포해 보겠습니다. 두 컨트랙트 중 하나는 이더 전송을 위해 send 함수를 사용하겠습니다.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract Sender {
function send(address payable addr, uint value) public payable {
bool result = addr.send(value);
require(result, "Failed to send Ether");
}
}
contract Receiver {
}
이제 두 컨트랙트를 배포한 후 send 함수를 이용해 Receiver 컨트랙트로 이더 전송을 시도해 보겠습니다. payable이 붙은 함수를 호출하므로 먼저 EOA에서 CA로 보낼 이더의 양을 입력합니다. 그리고 Receiver 컨트랙트 계정의 주소를 복사한 후 전송할 이더의 양과 함께 매개변수로 넣어줍니다.
위와 같이 함수를 호출하여 Sender에서 Receiver 컨트랙트로 이더 전송을 실행하면, 트랜잭션은 실패하게 됩니다. transfer나 call을 이용해도 동일하게 실패하게 됩니다. 그 이유는 이더를 전송받는 Receiver 컨트랙트에 recevie나 fallback이 없기 때문입니다.
2. receive
receive와 fallback은 컨트랙트가 이더를 받을 수 있도록 하는 특수한 함수입니다. 과거에는 fallback만 존재했지만, 현재는 receive와 fallback으로 분리되었습니다.
먼저 receive 함수에 대해 알아보겠습니다. receive 함수는 컨트랙트가 이더를 전송받았을 때 실행되는 함수입니다. 매개변수나 리턴값을 가질 수 없으며, external과 payable 키워드가 반드시 필요합니다. 다음과 같이 receive 함수를 작성할 수 있습니다.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract Sender {
function send(address payable addr, uint value) public payable {
bool result = addr.send(value);
require(result, "Failed to send Ether");
}
}
contract Receiver {
receive() external payable {}
}
이제 이더 전송을 다시 시도해 보면 이더가 정상적으로 전송되는 것을 확인할 수 있습니다.
receive를 사용할 경우 한 가지 주의할 점이 있습니다. receive는 transfer, send, call로 보내진 모든 이더를 받을 수 있습니다. 이 중 transfer과 send의 경우 2300 gwei로 가스 사용량이 제한되어 있습니다. receive 함수도 함수이기 때문에 함수 내부에서 코드를 실행할 수 있습니다. 즉, transfer과 send로 이더를 보낼 경우 가스 사용량 제한으로 receive 함수 내부 코드가 정상적으로 실행되지 않을 수 있습니다.
예를 들어 Receiver 컨트랙트가 받은 이더를 다시 Sender에게 그대로 전송하도록 receive 함수를 작성해 보겠습니다. 참고로 msg.value는 전송받은 이더의 양을 뜻하는 전역변수입니다.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract Sender {
receive() external payable {}
function send(address payable addr, uint value) public payable {
bool result = addr.send(value);
require(result, "Failed to send Ether");
}
}
contract Receiver {
receive() external payable {
payable(msg.sender).transfer(msg.value);
}
}
위 코드를 실행하면 가스비 부족으로 트랜잭션이 실패하게 됩니다. 이 문제를 해결하기 위해서는 가스비 제한량이 없는 call 함수를 사용해야 합니다. 다음과 같이 call 함수를 사용하도록 수정해 보겠습니다.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract Sender {
receive() external payable {}
function send(address payable addr, uint value) public payable {
(bool result, ) = addr.call{value: value}("");
require(result, "Failed to send Ether");
}
}
contract Receiver {
receive() external payable {
payable(msg.sender).transfer(msg.value);
}
}
수정 후 이더 전송을 다시 시도하면 정상적으로 트랜잭션이 처리됨을 알 수 있습니다.
3. fallback
마지막으로 fallback 함수에 대해 알아보겠습니다. fallback 함수는 과거에 이더를 받는 용도로도 사용되었으나, 현재는 receive와 분리되어 존재하지 않는 함수를 호출하는 경우에만 호출되도록 변경되었습니다. 이 부분을 이해하기 위해 먼저 call에 대한 추가적인 개념에 대해 알아보겠습니다.
지금까지는 call을 이더 전송 목적으로만 사용했습니다. 하지만 call은 외부 스마트 컨트랙트의 함수를 호출하는 용도로도 사용할 수 있습니다. 다음과 같이 Receiver에 add 함수를 추가한 후 이를 call로 호출해 보겠습니다.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract Sender {
function send(address payable addr, uint a, uint b) public payable returns (bytes memory) {
(bool success, bytes memory result) = addr.call(abi.encodeWithSignature("add(uint256,uint256)", a, b));
require(success, "Failed");
return result;
}
}
contract Receiver {
function add(uint a, uint b) public pure returns (uint) {
return a + b;
}
}
call로 외부 함수를 호출하기 위해서는 위와 같이 abi.encodeWithSignature("함수", 매개변수)를 사용해야 합니다. 솔리디티에서 함수를 call로 요청해야 할 경우 함수 선택자라고 부르는 함수 문자열 해시값의 첫 4바이트를 사용해야 합니다. abi.encodeWithSignature는 이러한 함수 선택자와 매개변수를 반환하여 call로 보낼 수 있는 형태의 데이터로 만들어 주는 함수입니다.
만약 call을 이용해 존재하지 않는 함수에 접근하려고 시도한다면 어떻게 될까요? 당연히 오류가 발생할 것입니다. 이 경우를 위해 존재하는 것이 바로 fallback입니다.
fallback은 존재하지 않는 함수를 호출하는 경우에 호출됩니다. fallback 함수는 receive 함수와 유사하게 매개변수와 리턴값이 존재하지 않으며, external 키워드를 함께 붙여 사용해야 합니다. payable도 붙일 수 있지만 반드시 필요한 것은 아닙니다. 만약 payable을 붙인다면 receive처럼 이더를 받을 수 있습니다.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract Sender {
function send(address payable addr, uint a, uint b) public payable returns (bytes memory) {
(bool success, bytes memory result) = addr.call(abi.encodeWithSignature("sub(uint256,uint256)", a, b));
require(success, "Failed");
return result;
}
}
contract Receiver {
event Log(string msg);
fallback() external {
emit Log("fallback");
}
function add(uint a, uint b) public pure returns (uint) {
return a + b;
}
}
위와 같이 존재하지 않는 함수를 call을 통해 호출하려고 할 경우 fallback 함수가 실행되어 로그가 남게 됩니다.
4. delegatecall
마지막으로 delegatecall에 대해 알아보겠습니다. delegatecall은 call과 유사한 함수입니다. call은 이더를 전송할 수 있지만 delegatecall은 이더를 전송할 수 없다는 차이점이 있습니다. 또 한 가지의 차이점은 아래 예제를 보면서 설명해 보겠습니다.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract Sender {
uint public result;
function call(address payable addr, uint a, uint b) public payable {
(bool success, ) = addr.call(abi.encodeWithSignature("add(uint256,uint256)", a, b));
require(success, "Failed");
}
function delegatecall(address payable addr, uint a, uint b) public payable {
(bool success, ) = addr.delegatecall(abi.encodeWithSignature("add(uint256,uint256)", a, b));
require(success, "Failed");
}
}
contract Receiver {
uint public result;
function add(uint a, uint b) public {
result = a + b;
}
}
먼저 위 코드에서 확인할 수 있는 것처럼 call과 delegatecall의 사용 방법은 거의 동일합니다. 하지만 실행 결과는 차이를 보입니다. call 함수를 실행할 경우 다음과 같이 Sender의 result는 변하지 않고 Receiver의 result만 변하는 것을 확인할 수 있습니다.
하지만 delegatecall을 실행할 경우 Receiver의 result가 아닌 Sender의 result가 변하는 것을 확인할 수 있습니다.
이것이 바로 call과 delegatecall의 차이입니다. delegatecall로 Receiver 컨트랙트의 함수를 호출할 경우, Sender의 스토리지와 msg.sender, msg.value를 사용하게 됩니다. 따라서 delgatecall은 Receiver와 Sender에 동일한 상태 변수가 있어야 정상적으로 작동됩니다.
지금까지 receive, fallback, delegatecall에 대해 알아봤습니다. 감사합니다.
다음글
'이더리움 > 솔리디티' 카테고리의 다른 글
[솔리디티] 15. 인터페이스 (0) | 2023.08.09 |
---|---|
[솔리디티] 14. 상속, 오버라이딩, 다중 상속 (0) | 2023.08.07 |
[솔리디티] 12. Payable, Transfer, Send, Call (0) | 2023.08.02 |
[솔리디티] 11. 에러 처리 (0) | 2023.07.28 |
[솔리디티] 10. 이벤트 (0) | 2023.07.27 |