Blockchain

[Solidity] 8. 이더 송수신과 `payable`, `transfer`, `send`, `call`

clolee 2025. 4. 18. 19:27

✅ 8. 이더 송수신과 payable, transfer, send, call

입출금 로직, NFT 구매, 스테이킹, DAO 자금 배분 등 거의 모든 디앱에서 사용
보안 중요


📌 1. 이더를 받기 위한 함수 조건

이더를 받으려면 최소한 하나 이상의 payable 함수가 있어야 합니다.

✅ 가장 간단한 형태

receive() external payable {
    // 수신 로직
}

📌 2. payable 키워드

이더를 받을 수 있는 함수 또는 이더를 보낼 수 있는 주소 타입에 붙입니다.

✅ 사용 위치

대상 예시
함수 function deposit() public payable { ... }
주소 payable(msg.sender).transfer(1 ether);

📌 3. 이더 수신 함수

함수 조건 역할
receive() msg.data 없음 순수 이더 수신용
fallback() msg.data 있음 존재하지 않는 함수 호출 시 실행

✅ 예시

receive() external payable {
    emit Received(msg.sender, msg.value);
}

fallback() external payable {
    emit FallbackCalled();
}

📌 4. 이더 전송 방법 3가지 (중요)

방법 성공 여부 확인 가스 제공량 특징
transfer 자동 revert 2300 gas 안전하지만 제한적
send bool 반환 2300 gas 실패 여부 수동 처리 필요
call bool, data 반환 모든 가스 전달 (기본) 가장 유연하나 보안 주의 필요

✅ 예제 1: transfer – 권장 X (가스 제한)

payable(to).transfer(1 ether);
  • 실패 시 자동 revert
  • 2300 gas만 전달됨 → 수신자 컨트랙트에 receive()가 없거나 로직이 많으면 실패

✅ 예제 2: send – 잘 안 씀

bool success = payable(to).send(1 ether);
require(success, "Send failed");
  • 실패 여부를 명시적으로 확인해야 함

✅ 예제 3: call – 권장 방식 (보안 주의)

(bool sent, ) = payable(to).call{value: 1 ether}("");
require(sent, "Call failed");
  • 가스 제한 없음 → 수신자가 receive() 또는 fallback() 구현되어야 함
  • ✅ 현재는 call이 가장 유연하고 권장되지만,
  • 재진입 공격 방지 필수 (Checks-Effects-Interactions 패턴 사용)

📌 5. 잔액 확인

address(this).balance // 컨트랙트 잔액
msg.sender.balance    // 호출자 잔액

✅ 실전 예제: 입금/출금 기능 구현

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

contract Wallet {
    address public owner;

    constructor() {
        owner = msg.sender;
    }

    receive() external payable {}

    function getBalance() public view returns (uint) {
        return address(this).balance;
    }

    function withdraw(address payable to, uint amount) public {
        require(msg.sender == owner, "Not owner");

        (bool sent, ) = to.call{value: amount}("");
        require(sent, "Failed to send Ether");
    }
}

📌 6. 보안: 재진입(Reentrancy) 공격 방어

이더 전송 전에 상태 변경 → 재진입 공격 취약

❌ 잘못된 예

function withdraw() public {
    // 이더 전송 먼저 → 외부 컨트랙트에서 다시 이 함수 호출 가능!
    (bool sent, ) = msg.sender.call{value: balances[msg.sender]}("");
    require(sent, "fail");

    balances[msg.sender] = 0;
}

✅ 안전한 패턴: Checks → Effects → Interactions

function withdraw() public {
    uint amount = balances[msg.sender];
    balances[msg.sender] = 0; // ✅ 먼저 상태 변경
    (bool sent, ) = msg.sender.call{value: amount}("");
    require(sent, "fail");
}

🧠 실무 팁 요약

항목 설명
payable이 없으면 이더 수신 불가 컴파일 에러 발생
transfer는 더 이상 권장되지 않음 가스 부족 문제 발생 가능
call은 유연하지만 재진입 방어 필수 반드시 state update → call 순서 유지
receive()/fallback() 구분 명확히 msg.data 존재 여부 기준
컨트랙트 잔액은 address(this).balance this는 현재 컨트랙트 주소

✅ [8단계 요약]

키워드 설명
payable 이더 송수신 가능 설정
receive() 단순 이더 수신 전용 함수
fallback() 비정상 호출 대응 함수
transfer / send / call 이더 전송 방법 (현재는 call 권장)
Checks-Effects-Interactions 재진입 공격 방어 전략