Solidity 문법 - (22) payable 키워드

2023. 6. 4. 15:58BlockChain/Solidity 깨부수기 ( 유투브 강의 )

목차
1. Payable
2. Payable 예제코드

 

1. Payable

 

payable은 주로 이더/토큰과 상호작용을 할 때에 적어주어야하는 키워드이다.

 

payable을 적어줌으로써 함수, 변수 등등은 이더를 보내거나 받는 기능과 상호작용이 가능해진다.

 

payable은 보통 send, transfer, call 등과 함께 사용하고,

 

payable은 주로 함수(function), 주소(address), 생성자(construct)에 붙여서 사용한다.

 

send, transfer, call은 아래 포스팅의 5번쨰 항목에 목록에 설명이 되어있다.

 

https://char1ey.tistory.com/152

 

Ethereum - Solidity의 단위, 변수 및 함수

솔리디티 문서를 참고해서 작성하였다. https://solidity-kr.readthedocs.io/ko/latest/units-and-global-variables.html#id5 단위 및 전역 변수 — Solidity 0.5.10 documentation 이더 단위 Ether를 더 작은 단위로 변환하기 위해

char1ey.tistory.com

 

간단하게 설명해보자면 다음의 흐름으로 사용이 되었다.

 

send, call, transfer를 순서대로 문제점을 보완해가면서 사용했으나, 가스의 가격이 상승하면서 다시 call을 사용하게 되었다.

 

 

send

 

address.send(amount)
// 보낼주소.send(보낼양)
솔리디티 초기에 이더를 보내기 위해서 사용되었다.
가스를 2300 소비하고 성공여부를 bool 값(true, false)으로 반환해주었다.
실패시 에러를 반환하지 않고 false를 반환한다는 점이 문제가 되었다.

 

call

// 0.7 버전 이전
(bool sent, ) = address.call.gas(...).value(_value)("");
// 주소.call.gas(소비할 가스의 양).value(보낼양)("");

// 0.7 버전 이후
(bool sent, ) = address.call{value: _vaiue, gas: ...)("");
// 주소.call{value: 보낼양, gas: 소비할 가스의 양)("");
send와 다르게 가스를 얼마나 소비할지(가스의 값) 지정할 수 있다.
send와 마찬가지로 실패시 에러를 반환하지 않는다는 점이 아쉬웠다.
가스의 가격을 지정한다는 점에서 재진입공격에 취약하다.

(※ 재진입공격이란 한 스마트 컨트랙트가 다른 스마트 컨트랙트로 이더를 보내는 과정이 무한반복 되는 것을 의미한다.)

 

 

transfer

address.transfer(amount);
// 주소.transfer(보낼양);
send의 에러를 발생시키지 못하는 단점을 보완하여, 실패시 에러를 발생시킨다.
send와 마찬가지로 2300의 가스(재진입공격에 대한 문제로 인해)를 소비한다.  

 

transfer는 19년도(이스탄불 하드포크) 이후 가스의 가격이 오르면서 2300가스로는 스마트 컨트랙트를 이용하기에는 부족해졌다. 이후 가스를 지정할 수 있는 call의 사용을 권장하게 되었다.

 

 

 

2. Payable 예제코드

 

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

contract ExPayable {

    event howMuch(uint256 _value);

    // 배포할 경우에 CA에 이더를 주는 것이 가능해진다.
    constructor() payable {}

    // _to는 ether를 받아야 하는 주소 이므로 payable 필요
    // 함수 자체도 이더를 보내는 것이므로 payable 필요
    function exSend(address payable _to) public payable {
        // 보낼주소.send(보낼양)
        bool sent = _to.send(msg.value);
        require(sent, "Failed to send ether");
        emit howMuch(msg.value);
    }

    // _to에게 ether를 보내는 것이므로 payable이 필요
    function exTransfer(address payable _to) public payable {
        // transfer는 error를 발생시키므로 에러를 발생시키지 않아도 된다.
        _to.transfer(msg.value);
        emit howMuch(msg.value);
    }

    function exCall(address payable _to) public payable {
        // ~ 0.7
        // (bool sent, ) = _to.call.gas(1000).value(msg.value)("");
        // require(sent, "Falied to send ether")

        // 0.7 ~
        (bool sent, ) = _to.call{value: msg.value, gas: 1000}("");
        require(sent, "Failded to sent ether");
        emit howMuch(msg.value);
    }
}

 

Deploy 버튼 (배포)을 누를 때, value에 10 ether를 넣고 진행했다.

 

아래의 balance에는 보이진 않지만 두번째 스크린샷부터는 balance에 10 ether가 있는 것을 볼 수 있다.

 

 

100 ether였던 이더가 약 90 ether로 줄어든 것을 볼 수 있다.

 

이제 첫번째 계정에서 두번째 계정으로 이더를 보내보자.

 

이더를 보낼 경우에는 value에 적어준다. 이 value는 msg.value에 들어가게된다.

 

msg는 message의 약어로 대개 컨트랙트의 사용자(client)가 만든 message를 의미한다.

 

2번째 계정에 20 ether를 보내보도록 하자.

 

exCall을 발동시키면

 

 

이더가 줄어든 것을 확인할 수 있다.

 

모든 메서드를 실행시키지 않았지만, 모두 같은 동작을 한다.

 

다음에 배울 msg.balance를 이용하면 보다 자세하게 잔액을 확인할 수 있다.

 

또한 call을 사용하는 부분에 ( ) 두 번쨰를 비워두었는데 이곳엔 원래 bytes 타입의 데이터를 넣어준다.(call이 반환)

 

나중에 call과 delegateCall을 사용할 때에 자세히 알아보도록 하자.