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 |
재진입 공격 방어 전략 |