스마트 컨트랙트 개발 및 보안 감사
스마트 컨트랙트 개발 및 보안 감사
스마트 컨트랙트(Smart Contract)는 블록체인 가상 머신(EVM 등) 위에서 사전에 프로그래밍된 조건에 따라 자동으로 실행되는 불변(Immutable)의 프로그램입니다.
1. Solidity 핵심 문법과 EVM 메모리 구조
1.1 데이터 저장 위치 (Data Location)와 가스 비용
EVM의 가스 소모량은 데이터가 저장되는 위치에 따라 극단적으로 달라집니다.
- Storage (스토리지): 블록체인 상태(State)에 영구 기록되는 공간.
SSTORE연산 시 최대 20,000 gas 소모 (가장 비쌈). - Memory (메모리): 함수 실행 중에만 존재하는 임시 휘발성 메모리.
- Calldata (캘데이터): 외부 트랜잭션 호출 인자(Arguments)가 담기는 불변의 읽기 전용 영역 (가장 저렴).
2. 주요 토큰 표준 (ERC Standards)
| 표준 | 용도 | 핵심 인터페이스 |
|---|---|---|
| ERC-20 | 대체 가능한 토큰 (Fungible Token) | totalSupply(), balanceOf(), transfer(), approve(), transferFrom() |
| ERC-721 | 대체 불가능한 토큰 (NFT) | ownerOf(tokenId), safeTransferFrom(), tokenURI() |
| ERC-1155 | 멀티 토큰 표준 (FT + NFT 하이브리드) | 단일 트랜잭션으로 여러 종류의 토큰을 일괄 전송(Batch Transfer)하여 가스비 대폭 절감 |
| ERC-4337 | 계정 추상화 (Account Abstraction) | 스마트 컨트랙트 지갑(시드구문 없는 소셜 로그인, 가스비 대납 Bundler) |
3. 스마트 컨트랙트 필수 보안 패턴과 취약점 (SWC)
스마트 컨트랙트는 한 번 배포되면 수정할 수 없으므로 철저한 보안 방어 패턴을 준수해야 합니다.
3.1 재진입 공격 (Reentrancy Attack)
2016년 360만 ETH가 탈취된 The DAO 해킹 사건의 주범입니다. 컨트랙트가 외부 주소로 이더를 전송할 때, 상대방의 fallback() 또는 receive() 함수가 실행 완료되기 전에 원본 함수의 출금 로직을 재호출하여 잔고를 0으로 만드는 공격입니다.
취약한 코드 예시:
// 위험: 잔고 차감 전에 송금 실행
function withdraw() public {
uint256 amount = balances[msg.sender];
require(amount > 0);
(bool success, ) = msg.sender.call{value: amount}(""); // 외부 호출
require(success);
balances[msg.sender] = 0; // 사후 차감 (취약점!)
}
안전한 방어 패턴: Checks-Effects-Interactions & ReentrancyGuard
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
function withdraw() public nonReentrant {
// 1. Checks (조건 검증)
uint256 amount = balances[msg.sender];
require(amount > 0, "No funds");
// 2. Effects (내부 상태 변경을 먼저 실행)
balances[msg.sender] = 0;
// 3. Interactions (외부 상호작용은 마지막에 실행)
(bool success, ) = msg.sender.call{value: amount}("");
require(success, "Transfer failed");
}
3.2 정수 오버플로우/언더플로우 (Overflow/Underflow)
Solidity 0.8.0 이상에서는 기본 연산자에 산술 오버플로우 검사가 내장되어 있어 오버플로우 발생 시 자동으로 revert됩니다.
3.3 가스 최적화 테크닉
- 변수 패킹 (Struct Packing): EVM은 32바이트(256비트) 단위로 스토리지를 읽습니다.
uint128 a; uint128 b;를 인접 선언하면 단 1개의 32바이트 슬롯에 묶여 저장되므로 가스를 절반으로 아낄 수 있습니다. - Custom Error 사용:
require(cond, "Long error string")대신if (!cond) revert CustomError()를 사용하면 바이트코드 크기와 배포/실행 가스를 대폭 절감합니다.
하위 세부 문서 목록 (2개)