오늘은 web3에서 Solidity를 사용하는 사람이라면 한 번쯤은 짚고넘어가야할 필요가 있는 payable 키워드를 들고와보았다.
처음 solidity를 공부할 때 부터 대체 저 키워드가 무엇인지부터 시작해서 transfer, call, send로 대체 왜 나뉘는지, 대체 어떻게 쓰는건지도 어려웠던 것 같은 기억이 있는데, 나처럼 삽질하지 말고 이거 하나 보고 그냥 가볍게 넘어가보도록 하자.
^__^
1. payable 키워드
다른 프로그래밍 언어와 달리, solidity는 가상화폐(코인)을 동력으로 하는 언어이다. 그렇다 보니, 다른 언어에서는 쓰이지 않는 송금 관련 함수 또는 키워드들이 존재하곤 한다. 즉, 가상화폐(코인)로 접근하기 위한 키워드가 사용된다는 것이다.
이더리움(ethereum) 생태계 내의 solidity 언어 내에서 코인을 전송하는 컨트랙트를 작성하기 위해서는 반드시 payable 키워드가 필요하다. 즉, payable 키워드를 사용하지 않는다면 ether를 보낼 수 없다는 것이다.
일단 어떻게 사용되는지 예제를 한번 볼까?
address payable public owner;
constructor() payable {
owner = payable(msg.sender);
}
function deposit() public payable {}
이렇게 사용이 된다고 보면 될 것 같은데,,, 이렇게 보면 대체 어떻게 사용하라는건지 감이 잘 잡히지 않는다.
좀 더 자세히 들어가보자.
payable의 사용은 크게 두 가지로 나뉜다.
- address payable: address type과 함께 사용되며, payable 키워드가 붙은 address 변수 등은 payable 해진다.
- function payable: function을 선언할 때 사용되며, payable 키워드가 붙은 함수는 지불, 잔액확인 등의 기능을 사용할 수 있다.
1-1. address payable
payable 키워드가 address 타입에 적용될 경우, 해당 주소는 ETH를 받을 수 있게 된다.
예를 들어, 사용자로부터 ETH를 받아 해당 금액을 contract 내부의 주소로 보내고 싶을 떄, payable 키워드를 사용할 수 있다.
address payable public recipient = 0x123123123...; // 이더를 받을 수 있는 주소 생성
recipient.transfer(amount); // 이더 전송
크게 어려울 것은 없기 때문에 넘어가도 될 듯 하다.
1-2. function payable
payable 키워드가 함수 타입에 적용될 경우, 그 함수는 ETH를 보내거나 받는 것, 특정 주소의 계좌 잔액을 확인하는 등의 기능을 실행할 수 있게 된다.
contract MyContract {
function deposit() public payable {
// 이 함수를 호출할 때 보낸 ETH는 자동적으로 이 컨트랙트의 Balance에 추가된다.
}
function getBalance() public view returns (uint) {
return address(this).balance;
}
}
payable주소, 함수를 이용하면 '.transfer', '.send' 등의 메서드를 이용하여 ETH를 보낼 수 있게 된다.
이제 좀 더 구체적인 사용법들을 알아보도록 하자.
2. ETH 송금 함수: .transfer(), .send(), .call()
ETH를 컨트랙트를 통해 보내기 위해 사용할 수 있는 함수 몇 가지를 알아보도록 하자.
비슷한 기능을 하는 대표적으로 세 가지 함수가 존재하는데, 이들의 차이점에 주목하면서 한번 같이 따라가보자.
입력값, 리턴값이 어떻게 다른지도 살펴보며 진행해보자.
2-1. .transfer()
사용 예시 먼저 보자.
address payable recipient = payable(0x123...); // 수신자 주소
uint256 amount = 1 ether;
recipient.transfer(amount); // 이더 전송
맨 아래, recipient.transfer(amount); 부분을 보면 된다.
- recipient는 송금받을 사람의 주소이며,
- amount는 송금을 할 액수이다.
- 리턴값이 존재하지 않는다.
.transfer()함수의 특징을 알아보자.
.transfer()함수는 지정된 금액의 ETH를 해당 주소로 전송하도록 설계되어있으며, 전송이 실패하게 될 경우, 자동으로 예외(exception)을 발생시키고, 트랜잭션을 되돌린다(revert). revert시에는 사용된 gas를 제외한 나머지가 반환된다. (2300가스를 소모함)
따라서, 에러 발생 또는 송금 실패 시에 error를 리턴할 필요가 없다.
송금 함수 중 가장 안전한 함수이기도 하다.
2-2. .send()
.send()함수는 .transfer()과 비슷하지만, 실패 시 트랜잭션을 revert()하지 않고, 실패(False)를 반환한다는 특징이 있다.
사용 예시를 확인해보자.
address payable recipient = payable(0x123...);
uint amount = 1 ether;
bool sent = recipient.send(amount);
require(sent, "Failed to send Ether");
- recipient는 송금받을 사람의 주소이며,
- amount는 송금을 할 액수이다.
- 리턴값이 존재한다.
transfer()에서는 에러를 리턴하지 않고 직접 핸들링하기 때문에 이럴 필요가 없었지만, send()에서는 직접 핸들링하는 로직을 작성해주어야 한다.
이 역시도 2300gas를 소모한다는 특징이 있다.
.send()함수는 보통 송금 실패시 자동으로 트랜잭션을 revert()하는 기능이 없기 때문에, .transfer()함수에 비해 유연한 오류처리가 필요할 때 사용한다고 보면 될 것 같다.
2-3. .call()
.call()메서드는 Solidity에서 가장 유연한 메서드 중 하나로, 모든 종류의 함수를 호출하고, 이더를 전송할 수 있다.
사용 예시를 보자.
address payable recipient = payable(0x123...);
uint amount = 1 ether;
(bool success, bytes memory data) = recipient.call{value: amount}("");
require(success, "Failed to send Ether");
이를 보면, 리턴값으로 boolean값과 bytes memory 데이터를 튜플 형태로 받는 것을 알 수 있다.
- recipient는 송금받을 사람의 주소이다.
- amount는 송금을 할 액수이다. (사용법: .call{value: amount}("");)
- 성공 여부를 튜플 형태로 반환한다.
call()을 사용하게 되면, gas비용이 그 로직에 따라 가변적이기 때문에 가스 소모량을 조절할 수 있다는 장점이 있고,
call()의 실행을 실패했을 때, 자동으로 트랜잭션을 revert하지 않기 때문에 반환 성공 여부를 체크하여 이후 로직을 처리할 필요가 있다. 하지만, call() 함수는 보안에 취약할 수 있으므로 주의할 필요가 있다. (보통 어떠한 함수가 유연하게 사용된다고 하면, 보안에 취약할 경우가 많다.)
아무래도 보안 전공생이니, 이 부분은 나중에 따로 다뤄보겠다.
오늘 내용 끝~
'web3 > Web3 dev&sec' 카테고리의 다른 글
[Web3 개발/보안] Reentrancy attack(재진입 공격)과 send(), transfer() 그리고 call() (1) | 2024.05.31 |
---|---|
[Web3 개발/보안] contract 함수 직접호출 vs 저수준 호출 (0) | 2024.05.19 |
[web3 Solidity] modifier 함수변경자 / onlyOwner (0) | 2024.04.11 |