스마트 컨트랙트와 Solidity·EVM 실행 구조
스마트 컨트랙트의 상태와 코드 구조, Solidity 문법, EVM 가스·호출 모델, 보안 설계와 운영 트레이드오프를 정리한다.
2026-08-14 · 최초 발행 2025-10-31
체인 위에서 코드와 상태가 함께 움직이는 방식
스마트 컨트랙트는 블록체인에 배포되는 불변 코드와 영속 상태를 포함하는 프로그램 단위다. 합의 참여자가 같은 입력을 처리해 같은 결과를 검증하며, 상태 변경은 트랜잭션 단위로 원자적으로 처리되고 실패 시 롤백된다.
컨트랙트는 바이트코드, Storage, Events/Logs, ABI로 이루어진다. 주소를 가진 계정 모델 안에서 외부계정(EOA)과 컨트랙트 계정은 구분된다. 코드 실행과 상태 변경, 이벤트 로그 기록은 온체인에서 이뤄지고, UI·오라클·인덱서·모니터링·키 관리는 오프체인 시스템이 맡는다.
배포된 코드 해시는 체인에 고정된다. 같은 입력에 대한 결과도 결정적이지만, 코드를 바꾸려면 프록시 패턴처럼 별도의 업그레이드 메커니즘을 설계해야 한다. 상태 변경 이력은 불변 원장에 남으므로 추적할 수 있다.
가스는 연산, 스토리지, 로그에 비용을 부과하는 자원 모델이다. 무한 루프와 자원 남용을 막는 대신 Out-of-Gas가 발생하면 트랜잭션 전체가 롤백되고 사용한 가스는 소모된다. 컨트랙트는 Call, Delegatecall, Staticcall로 다른 컨트랙트를 동기 호출할 수 있으며, ERC-20·ERC-721·ERC-1155 같은 인터페이스 표준은 이 조합을 가능하게 한다.
재진입 공격, 권한 오남용, MEV·프런트러닝은 이 실행 환경이 가진 대표적 위협이다. 온체인 권한 관리, 타임락, 다중서명, 업그레이드 정책은 코드와 분리할 수 없는 설계 요소다.
Solidity에서 상태와 권한의 경계를 세우기
Solidity는 정적 타입의 컨트랙트 지향 언어로 EVM 바이트코드로 컴파일된다. 주류 버전인 0.8.x 계열은 산술 오버플로를 기본으로 검사한다. 상속, 라이브러리, 인터페이스, 이벤트, 커스텀 에러, 모디파이어를 제공하며, 최신 문법과 보안 업데이트는 최신 정보를 확인해야 한다.
상태 변수에는 public·internal·private 가시성을 부여하고, 함수는 view·pure 여부를 구분한다. 수신자가 이더를 받을 수 있는지에는 payable이 관여하며 fallback과 receive도 서로 다른 역할을 가진다. 데이터는 영속 영역인 storage, 임시 영역인 memory, 읽기 전용 영역인 calldata 중 목적에 맞는 곳을 선택한다.
외부 호출이 필요한 코드에는 Checks-Effects-Interactions 패턴, ReentrancyGuard, Pull-Payment 방식이 사용된다. 빌드와 테스트에는 Hardhat·Foundry, 정적 분석에는 Slither, 퍼징에는 Echidna를 쓸 수 있다. 형식 검증 도구와 감리를 함께 적용하고, 테스트넷에서 메인넷으로 넘어가는 릴리스 게이트, Etherscan 등의 소스 검증, 릴리스 노트도 배포 과정에 포함한다.
환경: Solidity ^0.8.20, Hardhat(>=2.19) 또는 Foundry(>=0.2.0), Node LTS
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
error NotBuyer();
error NotSeller();
error InvalidState();
error ZeroValue();
error TransferFailed();
import {ReentrancyGuard} from "solady/utils/ReentrancyGuard.sol";
contract Escrow is ReentrancyGuard {
enum State { Initiated, Funded, Released, Refunded }
State public state;
address public immutable buyer;
address public immutable seller;
uint256 public amount;
event Funded(address indexed buyer, uint256 amount);
event Released(address indexed seller, uint256 amount);
event Refunded(address indexed buyer, uint256 amount);
constructor(address _buyer, address _seller) {
require(_buyer != address(0) && _seller != address(0), "zero addr");
buyer = _buyer;
seller = _seller;
state = State.Initiated;
}
function fund() external payable nonReentrant {
if (msg.sender != buyer) revert NotBuyer();
if (state != State.Initiated) revert InvalidState();
if (msg.value == 0) revert ZeroValue();
amount = msg.value;
state = State.Funded;
emit Funded(msg.sender, msg.value);
}
function release() external nonReentrant {
if (msg.sender != buyer) revert NotBuyer();
if (state != State.Funded) revert InvalidState();
state = State.Released;
(bool ok, ) = seller.call{value: amount}("");
if (!ok) revert TransferFailed();
emit Released(seller, amount);
}
function refund() external nonReentrant {
if (msg.sender != seller) revert NotSeller();
if (state != State.Funded) revert InvalidState();
state = State.Refunded;
(bool ok, ) = buyer.call{value: amount}("");
if (!ok) revert TransferFailed();
emit Refunded(buyer, amount);
}
}
이 예시는 상태 전이로 불변식을 유지하고, 가시성과 모디파이어로 호출 경계를 제한한다. 커스텀 에러는 실패 원인을 명확히 하며 가스 절감에 활용된다. call의 반환값을 확인하고, 인출형 접근을 적용해 재진입 위험을 낮춘다.
EVM의 가스와 상태 저장소
EVM은 256-bit 워드를 사용하는 스택 기반 머신이다. 일시적인 메모리, 영속적인 스토리지, 코드 공간이 분리되어 있다. 계정 상태는 nonce, balance, storageRoot, codeHash로 표현되며, 상태 트리는 Merkle-Patricia Trie를 기반으로 일관성을 유지한다. 향후 Verkle 전환 가능성은 최신 정보 확인이 필요하다.
opcode마다 가스 비용은 다르다. SSTORE와 LOG 같은 고가 연산은 특히 주의해야 한다. 가스 환급 규칙과 SELFDESTRUCT 동작은 EIP 변화에 민감하므로 최신 정보를 확인해야 한다.
EIP-1559에서 유효 가스가격은 다음과 같이 계산한다.
min(maxFeePerGas, baseFee + maxPriorityFeePerGas)
총 비용은 gasUsed × 유효 가스가격이다. gasUsed=100,000, baseFee=20 gwei, priority=1 gwei, maxFee≥21 gwei를 가정하면 비용은 100,000 × 21 gwei = 2,100,000 gwei(=0.0021 ETH)다.
EVM은 외부 I/O를 허용하지 않는다. 난수와 시간에 의존하는 로직은 특히 주의해야 하며, block.timestamp에는 조작 여지가 있다. 암호 연산을 위한 프리컴파일이 제공되고, 블록 안의 트랜잭션은 직렬로 실행된다. 트랜잭션은 원자성과 격리성을 보장한다.
트랜잭션이 실행되고 확정성에 이르기까지
사용자는 가스 한도와 수수료를 설정해 서명된 트랜잭션을 만들고 네트워크로 전파한다. 밸리데이터 또는 블록빌더가 이를 선택해 블록에 포함하면 EVM이 바이트코드를 실행한다. require, revert, assert, 에러가 발생하면 상태 변경은 전부 되돌아가지만, 사용한 가스는 소모된다.
성공한 실행은 상태 루트를 갱신하고 이벤트 로그와 receipt를 만든다. 이후 합의의 최종성에 도달할수록 확정성은 높아지며, 그 양상은 L1·L2와 합의알고리즘에 따라 다르다.
call은 외부 컨트랙트의 상태를 바꿀 수 있어 재진입 위험을 동반한다. delegatecall은 호출자의 storage에 기록하므로 업그레이드 프록시의 핵심이지만 스토리지 레이아웃 충돌을 조심해야 한다. staticcall은 상태 변경을 금지한 읽기 호출로, 오라클 조회와 검증 로직에 활용된다.
실패 경로는 커스텀 에러와 try/catch로 제어하고 revert로 원자성을 유지한다. 루프와 외부 호출을 최소화해 가스 그리핑을 막고, 접근 제어·레이트리밋·파우저를 도입할 수 있다.
자동 집행이 필요한 온체인 업무
결제 에스크로와 B2B 정산에서는 선입금 뒤 조건 충족 시 자동 송금하고, 분쟁 시 환불하는 로직을 구현할 수 있다. 다중서명과 타임락은 승인 흐름에 적용된다.
AMM 기반 탈중앙 거래소는 유동성 풀과 가격 곡선을 구현하고 스왑 및 수수료 분배를 자동화한다. 오라클과 TWAP는 가격 조작 방어에 사용된다. DAO는 토큰 기반 투표·제안·실행을 타임락과 퀘럼을 포함해 온체인에서 관리하며, 금고와 지출 정책을 자동 집행한다.
NFT와 권리 관리에는 ERC-721·ERC-1155의 민팅, 로열티, 임대, 소각 및 마켓 연동이 포함된다. 메타데이터를 고정할지 변경할지는 별도 정책으로 다룬다. 온체인 데이터 레지스트리와 공급망에서는 시리얼과 증명을 앵커링해 감사 추적성을 확보하고, 오라클로 물리 세계의 이벤트를 반영한다.
이런 구조는 체인과 L2에 따라 분~수십 분 단위의 확정성을 제공한다. 중개와 수동 조정을 줄여 처리량 대비 인건비를 축소할 수 있고, 트랜잭션 원자성은 장애의 영향 범위를 제한한다. 변경 이력과 로그를 온체인에 보존해 투명성과 감사 용이성을 높이며, 표준 인터페이스를 통한 구성요소 재사용으로 상호운용성과 합성 가능성도 강화한다. 공개 규약과 오픈 인프라는 벤더 종속을 낮추는 선택지가 된다.
불변 코드의 운영 비용
CEI 패턴, 재진입 보호, Ownable·Role 기반 접근 제어, Pause·Timelock은 기본적인 방어 수단이다. 업그레이드 프록시를 사용한다면 스토리지 레이아웃을 고정하고, 불변식과 UUPS 권한 관리를 함께 설계해야 한다. 감사·퍼징·버그바운티·Mainnet fork 시뮬레이션을 병행하며 배포 전후 모니터링을 운영한다.
재진입과 승수 취약점에는 인출형 결제와 외부 호출 최소화로 대응한다. MEV·프런트러닝에는 commit-reveal, 서명 기반 오프체인 매칭, 유연한 슬리피지를 고려할 수 있다. 오라클 위험은 다중 소스, 탈중앙 오라클, 지연 및 편차 한계 설정으로 다룬다.
불변성과 업그레이드 가능성 사이에는 신뢰·안전성과 민첩성·버그 수정성의 충돌이 있다. 가스 최적화는 비용을 줄일 수 있지만 가독성과 유지보수성을 낮출 수 있다. L1 보안과 L2 확장성도 수수료·속도·보안과 탈중앙성 사이에서 균형을 요구한다. 최신 롤업 규격과 브리지 보안은 최신 정보 확인이 필요하다.
| 지표 | 스마트 컨트랙트 | 전통 서버사이드 |
|---|---|---|
| 성능 | TPS·지연 제한, 가스 비용 존재 | 고성능 수평 확장 용이 |
| 확장성 | L2·샤딩·롤업 의존 | 캐시·큐·오토스케일 일반화 |
| 일관성 | 체인 합의 기반 강한 일관성(확정성 지연) | 최종 일관성·트랜잭션 DB 선택 가능 |
| 안정성 | 코드 불변·원자성 보장, 업그레이드 제약 | 릴리스·롤백·패치 유연 |
| 운영 편의 | 공개 감사·모니터링 용이, 변경 어려움 | 관측·제어 다양, 내부 통제 중심 |