| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 1 | 2 | 3 | 4 | |||
| 5 | 6 | 7 | 8 | 9 | 10 | 11 |
| 12 | 13 | 14 | 15 | 16 | 17 | 18 |
| 19 | 20 | 21 | 22 | 23 | 24 | 25 |
| 26 | 27 | 28 | 29 | 30 |
- byte code
- audit
- hard fork
- tx.origin
- Oracle Cloud
- ethernaut
- Smart contract
- Assembly
- web3
- writeup
- secureum
- transaction
- syntax
- libray
- EVM
- NaughtCoin
- openzepplin
- Ethererum
- coin flip
- solidity
- TransferFrom
- ethereum
- Block
- web assembly
- soft fork
- Coin
- approve
- Wargame
- ethereum virtual machine
- chain reorganization
- Today
- Total
c0mpos3r
[Secureum] Solidity 101 본문
101 key aspects of Solidity
1. Solidity는 Ethereum(및 기타 블록체인)에서 EVM을 기반으로 스마트 컨트랙트를 구현하는 고급 언어입니다.
- 이더리움 (Ethereum): 블록체인 기술을 기반으로 스마트 컨트랙트 기능을 구현하기 위한 분산 컴퓨팅 플랫폼이자, 해당 플랫폼의 자체 암호화폐 이름입니다.
- 블록체인 (BlockChain): P2P(Peer-to-Peer) 네트워크를 통해 참여자들이 공동으로 기록하고 관리하는 분산 데이터베이스의 한 형태입니다.
- EVM (Ethereum Virtual Machine): 이더리움 가상 머신의 약자로, 이더리움 블록체인 위에서 스마트 컨트랙트 코드를 실행하는 격리된 가상 환경입니다. 특정 운영체제에 구애받지 않고 코드를 실행할 수 있게 해줍니다.
- 스마트 컨트랙트 (Smart Contract): 블록체인 위에서 특정 조건이 충족되면 사전에 프로그래밍된 계약 내용을 자동으로 실행하는 디지털 계약입니다.
2. C++, Python, JavaScript의 영향을 받았습니다. 구문과 객체 지향 프로그래밍(OOP) 개념은 C++에서, 수정자, 다중 상속, C3 선형화, super 키워드는 Python에서 영향을 받았습니다. v0.4.0 이전에는 JavaScript의 함수 수준 스코프와 var 키워드의 영향도 받았습니다.
- 다중 상속 (Multiple Inheritance): 하나의 컨트랙트가 여러 부모 컨트랙트로부터 함수와 상태 변수 등을 물려받는 기능입니다. 코드의 재사용성을 높이고 컨트랙트를 모듈화하는 데 유용합니다.
- C3 선형화 (C3 Linearization): 다중 상속 시 발생할 수 있는 모호함을 해결하기 위해 부모 컨트랙트의 호출 순서를 결정하는 알고리즘입니다. 솔리디티는 오른쪽에서 왼쪽 순으로 상속 계층을 명확하게 정의합니다.
- 예시: contract C is A, B {}의 상속 순서는 C -> B -> A가 됩니다.
- super: 자식 컨트랙트에서 C3 선형화에 의해 결정된 상속 순서상 바로 다음 부모 컨트랙트의 함수를 호출할 때 사용하는 키워드입니다.
- 함수 수준 스코프 (Function-level Scope): (v0.4.0 이전) 변수가 선언된 코드 블록({...})과 상관없이 함수 전체에서 유효 범위를 갖는 방식입니다. 현재 솔리디티는 버그 발생 가능성을 줄이기 위해 변수가 선언된 블록 내에서만 유효한 블록 수준 스코프(Block-Level Scope)를 사용합니다.
- var 키워드: (v0.4.0 이전) 변수의 타입을 명시하지 않고 선언할 때 사용했던 키워드입니다. 컴파일러가 대입된 값에 따라 타입을 자동으로 추론했으나, 코드의 명확성과 안정성을 위해 현재는 사용되지 않고 모든 변수는 타입을 명시적으로 선언해야 합니다.
3. Solidity는 정적 타입 언어이며, 상속, 라이브러리, 복잡한 사용자 정의 타입을 지원하는 완전한 기능을 갖춘 고급 언어입니다.
- 정적 타입 (Statically typed): 변수의 타입을 컴파일 시점에 결정하는 방식입니다. 타입을 명시적으로 선언해야 하므로 코드의 안정성이 높고 오류를 조기에 발견하기 쉽습니다.
- 상속 (Inheritance): 부모 컨트랙트의 속성과 기능을 자식 컨트랙트가 물려받아 사용할 수 있는 객체 지향 프로그래밍의 특징입니다.
- 라이브러리 (Libraries): 재사용 가능한 코드를 모아놓은 특수한 형태의 컨트랙트로, 여러 컨트랙트에서 공통으로 사용하는 함수를 효율적으로 관리할 수 있게 해줍니다.
- 사용자 정의 타입 (User-defined types): 개발자가 struct(구조체) 등을 사용하여 자신만의 데이터 타입을 직접 만들어 사용할 수 있는 기능입니다.
4. 솔리디티 소스 파일은 프라그마, 임포트, 그리고 컨트랙트/구조체/열거형 정의를 포함할 수 있습니다. 컨트랙트 내부 코드 작성의 권장 순서는 상태 변수, 이벤트, 수정자, 생성자, 함수 순입니다.
- 권장 레이아웃: 코드를 논리적 순서(상태 변수 → 이벤트 → 수정자 → 생성자 → 함수)로 작성하면 가독성이 높아지고 유지보수가 용이해집니다.
5. SPDX 라이선스 식별자: 소스 파일의 시작 부분에 라이선스를 명시하는 주석을 다는 것이 권장됩니다. (예: // SPDX-License-Identifier: MIT)
- SPDX (Software Package Data Exchange): 소프트웨어의 라이선스 정보를 기계가 읽을 수 있는(machine-readable) 표준 형식으로 나타내기 위한 규약입니다. 컴파일 시 이 정보가 바이트코드 메타데이터에 포함되어 라이선스 관리가 용이해집니다.
6. 프라그마(Pragma): 특정 컴파일러 기능이나 검사를 활성화하는 데 사용되며, 해당 소스 파일에만 적용됩니다.
- 버전 프라그마 (Version Pragma): 사용할 컴파일러 버전이나 ABI 코더 버전을 지정합니다.
- 실험적 프라그마 (Experimental Pragma): 아직 기본으로 활성화되지 않은 실험적인 기능을 사용하도록 설정합니다. (예: SMTChecker)
7. 버전 프라그마: 소스 파일에 사용할 솔리디티 컴파일러 버전을 지정합니다. (예: pragma solidity ^0.8.3;)
- 버전 일치 검사: 프라그마는 컴파일러 버전을 바꾸는 것이 아니라, 현재 사용하는 컴파일러가 프라그마에 명시된 버전과 호환되는지 검사하는 역할을 합니다. 버전이 맞지 않으면 에러가 발생합니다.
- 브레이킹 체인지 (Breaking changes): 버전 번호 x.y.z에서 가운데 숫자인 y가 바뀌면 이전 버전과 호환되지 않는 큰 변경사항이 있음을 의미합니다. (예: 0.5.z → 0.6.0)
- 플로팅 프라그마 (^): 캐럿(^) 기호는 "명시된 버전 이상, 다음 브레이킹 체인지 버전 미만"을 의미합니다. ^0.8.3은 0.8.3부터 0.9.0 미만 버전의 컴파일러에서 컴파일할 수 있음을 뜻합니다.
8. ABI 코더 프라그마: ABI 인코더/디코더 구현체를 선택합니다. (예: pragma abicoder v2;)
- ABI (Application Binary Interface): 컨트랙트와 외부(다른 컨트랙트 또는 애플리케이션)가 상호작용하기 위한 함수 호출 규격입니다.
- abicoder v2: 중첩된 배열이나 구조체처럼 복잡한 데이터 타입을 처리할 수 있는 새로운 ABI 코더입니다. 현재는 v2가 기본값으로 활성화되어 있습니다.
9. 실험적 프라그마 (SMTChecker): 아직 기본 기능이 아닌 실험적인 기능을 활성화합니다.
- SMTChecker: pragma experimental SMTChecker;를 사용하면 코드의 안전성을 추가로 검사할 수 있습니다. require문을 가정으로 삼아 assert문의 조건이 항상 참인지 수학적으로 증명하려 시도하고, 산술 오버플로우/언더플로우, 0으로 나누기, 배열의 범위를 벗어난 접근 등 다양한 잠재적 버그를 자동으로 찾아냅니다.
10. 임포트 (Imports): JavaScript(ES6)와 유사하게 코드를 모듈화하기 위해 다른 소스 파일을 가져오는 기능을 지원합니다. (예: import "./filename.sol";)
- 모듈화: 코드를 여러 파일로 분리하여 관리함으로써 가독성과 재사용성을 높일 수 있습니다.
11. 주석 (Comments): 솔리디티는 한 줄 주석(//)과 여러 줄 주석(/* ... */)을 모두 지원합니다.
- 주석은 코드의 가독성과 유지보수성을 높이는 중요한 역할을 합니다. 컨트랙트, 함수, 변수 등이 어떤 의도로 작성되었고, 어떤 가정을 전제로 하는지 설명하는 인라인 문서로 사용하는 것이 좋습니다.
12. NatSpec 주석: 일반 사용자와 개발자를 위한 문서를 생성하는 데 사용되는 특별한 형식의 주석입니다.
- NatSpec(Ethereum Natural Language Specification Format)은 세 개의 슬래시(///) 또는 이중 별표 블록(/** ... */)을 사용하여 작성합니다. 함수나 상태 변수 선언 바로 위에 작성하면 컴파일러가 이를 분석해 JSON 형식의 문서를 자동으로 만들어 줍니다. 모든 public 인터페이스에 NatSpec 주석을 작성하는 것이 권장됩니다.
- 주요 NatSpec 태그:
- @title: 컨트랙트/인터페이스의 제목을 설명합니다.
- @author: 작성자의 이름을 명시합니다.
- @notice: 일반 사용자에게 이 기능이 무엇을 하는지 설명합니다. (예: "토큰 20개를 전송합니다.")
- @dev: 개발자에게 기술적인 세부 사항을 설명합니다. (예: "오버플로우 방지를 위해 SafeMath 라이브러리를 사용합니다.")
- @param: 함수의 매개변수(파라미터)에 대해 설명합니다.
- @return: 함수의 반환 값에 대해 설명합니다.
- @inheritdoc: 부모 컨트랙트의 NatSpec 주석을 그대로 상속받아 누락된 태그를 채웁니다.
- @custom...: 개발자가 용도에 맞게 직접 정의하는 커스텀 태그입니다.
13. 컨트랙트 (Contracts): 객체 지향 언어의 클래스(Class)와 유사하며, 영구적인 데이터(상태 변수)와 이 데이터를 수정하는 함수를 포함합니다.
- 컨트랙트는 솔리디티 코드의 기본 구성 단위이며, 다른 컨트랙트를 상속받아 기능을 확장할 수 있습니다.
14. 컨트랙트는 상태 변수, 함수, 함수 수정자, 이벤트, 에러, 구조체, 열거형 타입을 포함할 수 있습니다.
- 이는 컨트랙트라는 하나의 단위 안에서 선언하고 정의할 수 있는 코드 요소들의 목록입니다.
15. 상태 변수 (State Variables): 컨트랙트의 모든 함수가 접근할 수 있으며, 그 값이 컨트랙트 스토리지에 영구적으로 저장되는 변수입니다.
- 상태 변수에 저장된 데이터는 트랜잭션을 통해 값이 변경되지 않는 한 블록체인 상에 계속해서 남아있습니다.
16. 상태 변수 접근 제어자 (State Visibility Specifiers): 상태 변수는 public, internal, private 중 하나의 접근 제어자를 명시해야 합니다.
- public: 변수와 동일한 이름의 getter 함수가 자동으로 생성되어 컨트랙트 내부뿐만 아니라 외부에서도 직접 조회할 수 있습니다.
- internal: 해당 컨트랙트와 이 컨트랙트를 상속받는 자식 컨트랙트 내부에서만 접근할 수 있습니다.
- private: 해당 변수가 선언된 컨트랙트 내부에서만 접근할 수 있습니다. (자식 컨트랙트도 접근 불가)
- ⚠️ 중요: 변수를 private으로 선언하더라도 다른 컨트랙트에서의 접근만 막을 뿐, 블록체인 외부의 관찰자는 체인에 기록된 모든 데이터를 볼 수 있습니다. 데이터 비공개가 아님에 유의해야 합니다.
17. 상태 변수: constant와 immutable
- 상태 변수는 constant(상수) 또는 immutable(불변)로 선언하여 한 번 값이 할당된 후에는 절대 수정할 수 없도록 만들 수 있습니다.
- constant: 컴파일 시점에 값이 결정되어야 하는 변수입니다. 선언과 동시에 값을 할당해야 합니다. block.timestamp처럼 실행 시점에 결정되는 값은 사용할 수 없습니다.
- immutable: 컨트랙트 배포 시점(생성자 실행 시)에 딱 한 번만 값을 할당할 수 있습니다. 선언 시 또는 생성자 내부에서 값을 지정할 수 있습니다.
18. constant와 immutable 변수의 가스 비용
- 이 두 종류의 변수는 일반 상태 변수보다 가스비가 훨씬 저렴합니다. 컴파일러가 이 변수들을 위한 별도의 스토리지 공간(슬롯)을 예약하지 않고, 코드 내에서 해당 변수가 사용되는 모든 위치에 실제 값을 복사해 넣기 때문입니다.
- constant와 immutable의 차이:
- constant: 컴파일 시점에 값이 정해지므로, 코드를 최적화할 여지가 더 많아 immutable보다 가스비가 저렴할 수 있습니다.
- immutable: 배포 시점에 값이 정해지고, 이 값이 32바이트 공간을 차지하며 코드 곳곳에 복사됩니다.
- 지원 타입: constant는 값 타입과 string을 지원하며, immutable은 값 타입만 지원합니다.
19. 함수 (Functions): 코드의 실행 가능한 단위입니다.
- 함수는 일반적으로 컨트랙트 내부에 정의되지만, 컨트랙트 외부에도 정의할 수 있습니다. 다른 컨트랙트에서의 접근 가능 여부를 결정하는 접근 제어자를 가집니다.
20. 함수 파라미터 (Function parameters): 변수와 동일한 방식으로 선언되며, 함수 내부에서는 지역 변수처럼 사용됩니다.
- 함수를 호출할 때 외부로부터 값을 전달받는 통로 역할을 합니다. 사용하지 않는 파라미터는 가독성을 위해 변수명을 생략할 수 있습니다.
21. 함수 반환 변수 (Function Return Variables): 함수가 반환할 변수는 returns 키워드 뒤에 선언합니다.
- 반환 변수의 이름은 생략할 수 있으며, 선언 시 각 타입의 기본값으로 초기화됩니다. 함수 본문에서 지역 변수처럼 값을 할당할 수 있습니다.
- 반환 값을 지정하는 방법은 두 가지입니다.
- 선언된 반환 변수에 직접 값을 할당하고 return; 없이 함수를 종료합니다.
- return 키워드를 사용하여 직접 값을 반환합니다. (예: return (value1, "string_value");)
- 여러 값을 반환할 때는 return (v0, v1, ..., vn) 형식을 사용하며, returns에 선언된 변수들의 개수 및 타입과 일치해야 합니다.
22. 함수 수정자 (Function Modifiers): 함수의 동작을 선언적으로 변경하는 데 사용됩니다.
- 주로 함수 실행 전에 특정 조건을 검사하는 용도로 쓰입니다. (예: 특정 주소만 함수를 호출할 수 있도록 제한)
- 수정자 내부에 있는 _ (언더스코어) 위치에 실제 함수 본문 코드가 삽입되어 실행됩니다.
- 하나의 함수에 여러 수정자를 공백으로 구분하여 적용할 수 있으며, 명시된 순서대로 평가됩니다.
- 수정자는 _ 없이 조건을 만족하지 않을 경우 함수 본문을 아예 실행하지 않도록 할 수도 있습니다.
23. 함수 접근 제어자 (Function Visibility Specifiers): 함수는 public, external, internal, private 중 하나로 접근 수준을 명시해야 합니다.
- public: 컨트랙트 내부와 외부 모두에서 호출할 수 있습니다.
- external: 컨트랙트 외부에서만 호출할 수 있습니다. (다른 컨트랙트나 트랜잭션을 통해 호출)
- external 함수는 내부에서 직접 호출할 수 없지만, this.f()와 같이 this를 통해 외부 호출 방식으로 호출하는 것은 가능합니다.
- internal: 해당 컨트랙트와 이 컨트랙트를 상속받는 자식 컨트랙트 내부에서만 호출할 수 있습니다.
- private: 해당 함수가 선언된 컨트랙트 내부에서만 호출할 수 있습니다. (자식 컨트랙트도 호출 불가)
24. 함수 상태 변경 지정자 (Function Mutability Specifiers): 함수는 view 또는 pure로 지정하여 상태 변경 여부를 명시할 수 있습니다.
- view: 컨트랙트의 상태를 읽을 수는 있지만, 수정할 수는 없는 함수입니다. (EVM의 STATICCALL 옵코드를 통해 강제됩니다.)
- 상태 수정으로 간주되는 행위: 상태 변수에 쓰기, 이벤트 발생시키기, 다른 컨트랙트 생성, selfdestruct 사용 등
- pure: 컨트랙트의 상태를 읽지도, 수정하지도 않는 함수입니다.
- 상태 읽기로 간주되는 행위: 상태 변수 읽기, block, tx, msg의 특정 멤버 접근 등
- EVM 수준의 강제성: EVM 레벨에서는 STATICCALL을 통해 상태에 쓰는 행위(view)를 막을 수는 있지만, 상태를 읽는 행위(pure)를 원천적으로 막는 것은 불가능합니다. 따라서 pure는 주로 컴파일 시점의 문법 검사에 의존합니다.
25. 함수 오버로딩 (Function Overloading): 하나의 컨트랙트 내에 이름은 같지만 파라미터(매개변수) 타입이 다른 여러 함수를 정의하는 것입니다.
- 함수 호출 시 전달된 인자(argument)의 타입과 개수에 맞는 함수가 자동으로 선택됩니다.
- 오버로딩 규칙을 적용할 때 반환 값의 타입은 고려되지 않습니다.
26. 자유 함수 (Free Functions): 컨트랙트 외부에 정의된 함수입니다.
- 컨트랙트에 소속되지 않은 함수로, 항상 암묵적으로 internal 접근 제어자를 가집니다. 이 함수를 호출하는 모든 컨트랙트 코드에 함수의 내용이 포함되어 컴파일됩니다.
27. 이벤트 (Events): EVM의 로깅(logging) 기능을 추상화한 것입니다.
- 이벤트를 발생시키면(emit), 전달된 인자(argument)들이 트랜잭션의 로그(log)에 저장됩니다.
- 이 로그 데이터는 블록체인에 영구적으로 기록되지만, 컨트랙트 내부에서는 이 데이터에 접근할 수 없습니다.
- 대신, 블록체인 외부의 디앱(DApp) 같은 애플리케이션이 이더리움 클라이언트의 RPC 인터페이스를 통해 이벤트를 구독하고 실시간으로 감지하여 활용할 수 있습니다.
28. 인덱싱된 이벤트 파라미터 (Indexed Event Parameters): 이벤트 파라미터 앞에 indexed 키워드를 붙이면 효율적인 검색이 가능해집니다.
- 최대 3개의 파라미터를 indexed로 지정할 수 있습니다. 인덱싱된 파라미터는 로그의 데이터 영역이 아닌 토픽(topics)이라는 특별한 자료 구조에 저장되어, 특정 이벤트를 필터링하거나 검색할 때 매우 유용합니다.
- string이나 bytes 같은 동적 배열 타입을 indexed로 지정하면, 데이터 자체가 아닌 데이터의 Keccak-256 해시값이 토픽으로 저장됩니다.
- indexed가 아닌 파라미터들은 모두 로그의 데이터 부분에 저장됩니다.
29. emit: 이벤트를 발생시킬 때 사용하는 키워드입니다.
- emit 키워드 뒤에 이벤트 이름과 전달할 인자들을 명시하여 사용합니다. (예: emit Deposit(msg.sender, _id, msg.value);)
30. 구조체 타입 (Struct Types): 여러 변수를 하나의 그룹으로 묶어 사용자 정의 데이터 타입을 만드는 것입니다.
- 서로 다른 타입의 변수들을 하나의 의미 있는 단위로 묶을 수 있습니다. 구조체의 멤버에는 .(점)을 사용하여 접근합니다.
- 예시: s.user, s.amount
31. 열거형 (Enums)은 가독성을 높이기 위해, 한정된 개수의 상수 값들로 이루어진 사용자 정의 타입을 만들 때 사용됩니다.
- enum은 최소 1개, 최대 256개의 멤버를 가질 수 있습니다.
- enum의 멤버들은 0부터 시작하는 부호 없는 정수(unsigned integer) 값으로 표현되며, 정수와 명시적으로 상호 변환이 가능합니다.
- 별도로 값을 지정하지 않으면, enum의 기본값은 첫 번째 멤버가 됩니다.
32. 생성자 (Constructor)는 컨트랙트가 블록체인에 배포될 때 단 한 번만 실행되는 특별하고 선택적인 함수입니다.
- constructor 키워드로 선언하며, 컨트랙트당 하나만 허용됩니다.
- 생성자 실행이 완료된 후, 생성자 코드 자체와 생성자 내부에서만 호출된 internal 함수 코드를 제외한 최종 컨트랙트 코드가 블록체인에 저장됩니다.
33. receive 함수는 순수한 이더(Ether) 전송을 처리하기 위한 특별한 함수입니다.
- receive() external payable { ... } 형태로 선언하며, 인자를 가질 수 없고 값을 반환하지도 않습니다.
- 아무 데이터 없이(.calldata가 비어있음) 컨트랙트에 이더가 보내질 때(예: .send() 또는 .transfer() 사용 시) 자동으로 실행됩니다.
- ⚠️ 중요: .send()나 .transfer()를 통해 호출될 경우, receive 함수는 최대 2300 가스만 사용할 수 있어 복잡한 로직 수행이 어렵고 간단한 로깅 정도만 가능합니다.
- 컨트랙트에 receive 함수가 없더라도 채굴 보상(coinbase transaction)이나 다른 컨트랙트의 selfdestruct 대상으로 지정되면 이더를 받을 수 있습니다. 이 경우 컨트랙트는 이더 수신에 반응하거나 거부할 수 없으므로, address(this).balance 값이 컨트랙트 내부에서 수동으로 계산한 잔액과 다를 수 있습니다.
34. fallback 함수는 다른 어떤 함수와도 일치하지 않는 함수가 호출되었을 때 실행되는 최후의 보루 같은 함수입니다.
- fallback() external [payable] 또는 fallback(bytes calldata _input) external [payable] returns (bytes memory _output) 형태로 선언합니다.
- 실행 조건:
- 호출된 함수 시그니처가 컨트랙트 내의 어떤 함수와도 일치하지 않을 때.
- receive 함수가 없는데, 데이터 없이 이더만 전송될 때.
- 이더를 받으려면 반드시 payable로 선언해야 합니다. payable fallback 함수가 receive 함수 대신 사용될 경우, 마찬가지로 2300 가스 제한에 걸릴 수 있습니다.
35. 정적 타이핑 (Statically-typed): 솔리디티는 정적 타입 언어입니다. 이는 모든 변수(상태 변수, 지역 변수)의 타입을 컴파일 시점에 명시해야 함을 의미합니다.
- 실행 전에 컴파일러가 타입 검사를 수행하므로, 런타임에 발생할 수 있는 타입 관련 오류를 미리 방지하여 코드의 안정성을 높입니다. (C, C++, Java, Rust 등과 동일)
36. 값 타입(Value Types)과 참조 타입(Reference Types): 솔리디티의 타입은 크게 두 가지 범주로 나뉩니다.
- 값 타입 (Value Types): 변수가 함수 인자로 전달되거나 다른 변수에 할당될 때, 데이터가 항상 복사됩니다. 원본 데이터에 영향을 주지 않습니다.
- 참조 타입 (Reference Types): 변수를 다룰 때 데이터 자체가 아닌 데이터가 저장된 위치(주소)에 대한 참조가 전달됩니다. 여러 변수가 동일한 데이터를 가리키고 수정할 수 있습니다.
37. 값 타입 (Value Types): 데이터가 항상 복사되는 타입들입니다.
- 종류: Booleans (bool), Integers (int, uint), Fixed Point Numbers (fixed, ufixed), Address, Contract, Fixed-size Byte Arrays (bytes1, ..., bytes32), Literals (리터럴), Enums (열거형), Functions.
38. 참조 타입 (Reference Types): 데이터의 위치(참조)를 통해 다루는 타입들입니다. 데이터가 어디에 저장되는지(storage, memory, calldata) 명시하는 것이 중요합니다.
- 종류: Arrays (배열, 동적 배열 bytes와 string 포함), Structs (구조체), Mappings (매핑).
39. 기본값 (Default Values)
변수는 선언될 때, 모든 바이트가 0으로 채워진 초기 기본값을 갖습니다.
- bool: false
- uint / int: 0
- 정적 배열, bytes1~bytes32: 각 요소가 해당 타입의 기본값으로 초기화됩니다.
- 동적 배열, bytes, string: 비어 있는 배열 또는 문자열
- enum: 첫 번째 멤버
40. 스코프 (Scoping)
변수의 유효 범위를 의미하는 스코프는 C99 언어의 규칙을 따릅니다.
- 변수는 선언된 직후부터, 자신을 포함하는 가장 작은 { } 코드 블록이 끝날 때까지 유효합니다.
- 예외 및 특징:
- for 루프의 초기화 부분에서 선언된 변수는 해당 for 루프 안에서만 유효합니다.
- 함수나 수정자의 파라미터는 해당 함수/수정자 본문({ }) 전체에서 유효합니다.
- 상태 변수, 함수, 컨트랙트 등 코드 블록 외부에서 선언된 항목들은 선언 위치보다 앞에서도 사용 가능합니다. (예: 상태 변수를 선언하기 전에 함수에서 사용, 재귀 함수 호출 등)
41. 불리언 (Boolean)
bool 키워드로 선언하며, true와 false 두 가지 상수 값만 가집니다.
- 연산자: ! (논리 부정), && (논리 AND), || (논리 OR), == (같음), != (같지 않음)
- 단축 평가 (Short-circuiting): &&와 || 연산자는 단축 평가 규칙을 따릅니다. 예를 들어, f(x) || g(y)에서 f(x)가 true이면, g(y)는 가스 소모 등 부수 효과(side-effect)가 있더라도 아예 실행되지 않습니다.
42. 정수 (Integers)
부호 있는 정수(int)와 부호 없는 정수(uint) 타입을 제공합니다.
- uint8부터 uint256까지, int8부터 int256까지 8비트 단위로 크기를 지정할 수 있습니다.
- uint는 uint256의 별칭(alias)이며, int는 int256의 별칭입니다.
- 연산자:
- 비교 연산자: <=, <, ==, !=, >=, >
- 비트 연산자: & (AND), | (OR), ^ (XOR), ~ (NOT)
- 쉬프트 연산자: << (왼쪽 쉬프트), >> (오른쪽 쉬프트)
- 산술 연산자: +, -, *, /, % (나머지), ** (제곱)
43. 정수 산술 연산
솔리디티 0.8.0 버전부터 정수 연산은 기본적으로 checked 모드에서 수행됩니다.
- checked 모드 (기본값): 연산 결과가 해당 정수 타입의 범위를 벗어나면(오버플로우 또는 언더플로우 발생 시) 트랜잭션 전체가 실패(revert)합니다.
- unchecked 모드: unchecked { ... } 블록을 사용하면 검사를 비활성화할 수 있습니다. 이 블록 안에서는 연산 결과가 범위를 벗어나도 오류가 발생하지 않고, 값이 순환(wrapping)합니다. (예: uint8에서 255 + 1 = 0)
44. 고정 소수점 숫자 (Fixed Point Numbers)
소수점을 표현하는 fixed 및 ufixed 타입은 아직 솔리디티에서 완벽하게 지원되지 않습니다.
- 변수를 선언할 수는 있지만, 값을 할당하거나 연산하는 기능은 없습니다.
- 소수점 연산이 필요할 경우, DSMath, PRBMath와 같은 외부 라이브러리를 사용하는 것이 일반적입니다.
45. 주소 타입 (Address Type)
이더리움 주소(20바이트 값)를 저장하며, 두 가지 종류가 있습니다.
- address: 일반 주소 타입.
- address payable: 이더(Ether)를 받을 수 있는 주소 타입으로, .transfer()와 .send() 멤버를 추가로 가집니다. 일반 address 타입에는 이더를 직접 보낼 수 없습니다.
- 타입 변환: address payable에서 address로의 변환은 자동으로 가능하지만, address를 address payable로 변환하려면 payable(주소)와 같이 명시적으로 변환해야 합니다.
46. 주소 타입의 멤버
주소 타입 변수는 다음과 같은 유용한 멤버(속성 및 함수)를 가집니다.
- <address>.balance: 해당 주소가 보유한 이더 잔액 (단위: Wei)
- <address>.code: 해당 주소에 배포된 컨트랙트의 바이트코드
- <address>.codehash: 해당 주소 코드의 해시값
- <address payable>.transfer(금액): 지정한 금액의 이더를 전송합니다. 실패 시 revert되며, 2300 가스만 전달합니다.
- <address payable>.send(금액): 지정한 금액의 이더를 전송합니다. 실패 시 false를 반환하며, 2300 가스만 전달합니다.
- <address>.call(데이터): 로우레벨 CALL을 실행합니다. 성공 여부(bool)와 반환 데이터를 반환하며, 사용 가능한 모든 가스를 전달합니다.
- <address>.delegatecall(데이터): 로우레벨 DELEGATECALL을 실행합니다. 다른 컨트랙트의 코드를 현재 컨트랙트의 컨텍스트(스토리지, 상태 등)에서 실행합니다.
- <address>.staticcall(데이터): 로우레벨 STATICCALL을 실행합니다. call과 유사하지만, 호출된 함수가 상태를 변경하면 revert됩니다.
47. transfer
이더를 전송하는 가장 기본적인 방법입니다.
- 현재 컨트랙트의 잔액이 부족하거나 받는 측에서 이더 수신을 거부하면 트랜잭션 전체가 실패(revert)합니다.
- 안전하지만, 전달되는 가스가 2300으로 고정되어 있어 수신 측 컨트랙트가 복잡한 로직을 수행할 수 없습니다.
48. send
transfer의 로우레벨 버전입니다.
- 가장 큰 차이점은 전송 실패 시 revert되는 대신 false를 반환한다는 점입니다.
- 따라서 send를 사용할 때는 require(success, "Failed to send Ether");와 같이 반드시 반환 값을 확인해야 합니다. 현재는 보안상 잘 사용되지 않습니다.
49. call / delegatecall / staticcall
ABI 규약을 따르지 않는 컨트랙트와 상호작용하거나, 데이터를 더 세밀하게 제어할 때 사용하는 로우레벨 함수들입니다.
- abi.encode... 함수들로 인코딩된 바이트 데이터를 인자로 받습니다.
- delegatecall: 라이브러리 컨트랙트처럼, 다른 곳에 저장된 코드를 가져와 현재 컨트랙트의 데이터(스토리지, msg.sender 등)를 가지고 실행하고 싶을 때 사용됩니다.
- staticcall: 호출하는 함수가 상태를 변경하지 않는다는 것이 확실할 때 사용합니다.
- 이 함수들은 가스량(gas)과 전송할 이더량(value)을 직접 지정할 수 있어 유연성이 높지만, 그만큼 보안에 각별히 유의해야 합니다.
50. 컨트랙트 타입 (Contract Type)
모든 컨트랙트는 그 자체로 하나의 타입을 정의합니다.
- 컨트랙트 타입은 address 타입과 명시적으로 상호 변환할 수 있습니다.
- 컨트랙트 타입의 멤버는 해당 컨트랙트의 모든 external 함수와 public으로 선언된 상태 변수들입니다. 이를 통해 다른 컨트랙트의 함수를 직접 호출할 수 있습니다. (예: ERC20 token = ERC20(토큰주소); token.transfer(받는주소, 금액);)
51. 고정 크기 바이트 배열 (Fixed-size Byte Arrays)
bytes1부터 bytes32까지의 값 타입은 1바이트부터 최대 32바이트까지의 바이트 시퀀스를 저장합니다.
- byte[]는 바이트의 동적 배열이지만, 각 요소 사이에 31바이트의 불필요한 공간(패딩)을 낭비하므로 비효율적입니다. (스토리지 제외)
- 따라서 임의 길이의 바이트 시퀀스를 다룰 때는 byte[] 대신 bytes 타입을 사용하는 것이 훨씬 효율적입니다.
52. 리터럴 (Literals)
리터럴은 코드에 직접 작성하는 고정된 값을 의미하며, 5가지 종류가 있습니다.
- 주소 리터럴 (Address Literals): EIP-55 주소 체크섬 검사를 통과하는 16진수 문자열은 address 타입으로 인식됩니다.
- 정수/유리수 리터럴 (Rational and Integer Literals): 123_456과 같이 가독성을 위해 숫자 사이에 언더스코어(_)를 사용할 수 있습니다.
- 문자열 리터럴 (String Literals): 큰따옴표("foo") 또는 작은따옴표('bar')로 감싸서 표현합니다.
- 유니코드 리터럴 (Unicode Literals): unicode 키워드를 접두사로 붙여 UTF-8 시퀀스를 포함할 수 있습니다. (예: unicode"유니코드")
- 16진수 리터럴 (Hexadecimal Literals): hex 키워드를 접두사로 붙이고 따옴표로 감싸서 표현합니다. (예: hex"001122FF")
53. 열거형 (Enums)
(이전 항목 복습) enum은 사용자 정의 타입을 만드는 방법 중 하나입니다.
- 최소 한 개의 멤버가 필요하며, 최대 256개를 넘을 수 없습니다. 선언 시 기본값은 첫 번째 멤버입니다.
54. 함수 타입 (Function Types)
함수 자체를 하나의 타입으로 다룰 수 있습니다. 함수 타입의 변수에 함수를 할당하거나, 다른 함수에 인자로 전달하고 반환받을 수 있습니다.
- internal 함수: 현재 컨트랙트 내부에서만 호출 가능한 함수 타입입니다.
- external 함수: 주소(address)와 함수 시그니처로 구성되며, 외부 함수 호출을 통해 전달되거나 반환될 수 있습니다.
55. 참조 타입과 데이터 위치 (Reference Types & Data Location)
모든 참조 타입은 데이터가 저장되는 위치를 반드시 명시해야 합니다. 위치는 memory, storage, calldata 세 가지가 있습니다.
- memory: 함수의 실행 중에만 데이터를 유지하는 임시 저장 공간입니다. 함수 호출이 끝나면 사라집니다.
- storage: 컨트랙트의 생명주기 동안 데이터를 영구적으로 보관하는 곳입니다. (상태 변수가 저장되는 위치)
- calldata: 함수 인자를 저장하는 수정 불가능한 비영구적 공간입니다. external 함수의 파라미터에 필수적으로 사용됩니다.
56. 데이터 위치와 할당 (Data Location & Assignment)
데이터 위치는 값의 할당 방식에도 큰 영향을 미칩니다.
- storage ↔ memory (또는 calldata → memory) 간의 할당은 항상 데이터 전체가 독립적으로 복사됩니다.
- memory → memory로의 할당은 참조(reference)만 만듭니다. 즉, 하나의 변수를 수정하면 다른 변수에도 변경 사항이 반영됩니다.
- storage → 지역 storage 변수로의 할당 역시 참조만 할당합니다. (스토리지 변수를 가리키는 포인터처럼 동작)
- 그 외에 storage에 값을 쓰는 모든 할당은 항상 복사가 일어납니다.
57. 배열 (Arrays)
배열은 컴파일 시점에 크기가 고정된 고정 크기 배열과, 실행 중에 크기가 변할 수 있는 동적 크기 배열이 있습니다.
- 고정 크기 배열은 T[k], 동적 크기 배열은 T[]로 선언합니다.
- 인덱스는 0부터 시작하며, 배열의 범위를 벗어나 접근하면 트랜잭션이 실패합니다.
58. 배열의 멤버 (Array members)
동적 storage 배열은 다음과 같은 멤버를 가집니다.
- .length: 배열의 요소 개수를 반환합니다.
- .push(): 배열 끝에 타입의 기본값(0)으로 초기화된 요소를 추가하고, 해당 요소에 대한 참조를 반환합니다.
- .push(x): 배열 끝에 주어진 요소 x를 추가합니다. (아무것도 반환하지 않음)
- .pop(): 배열의 마지막 요소를 제거합니다.
59. bytes와 string
bytes와 string은 특별한 형태의 동적 배열입니다.
- bytes는 byte[]와 유사하지만, calldata와 memory에서 데이터가 촘촘하게 채워져 있어 훨씬 효율적입니다.
- string은 bytes와 동일하지만, .length나 인덱스 접근이 허용되지 않습니다.
- 권장 사항:
- 임의 길이의 원시 바이트 데이터에는 bytes를 사용하세요.
- 임의 길이의 UTF-8 문자열 데이터에는 string을 사용하세요.
- 길이가 32바이트 이하로 제한될 수 있다면, 가스비가 훨씬 저렴한 bytes1 ~ bytes32 타입을 항상 우선적으로 사용하세요.
60. 메모리 배열 (Memory Arrays)
memory에 동적 배열을 생성할 때는 new 연산자를 사용합니다.
- storage 배열과 가장 큰 차이점: memory 배열은 생성된 후에 크기를 조절할 수 없습니다. (.push나 .pop 사용 불가)
- 따라서 memory 배열을 사용하려면 필요한 크기를 미리 계산하거나, 더 큰 새 배열을 만들어 기존 요소를 모두 복사해야 합니다.
61. 배열 리터럴 (Array Literals)
배열 리터럴은 [...] 대괄호 안에 쉼표로 구분된 표현식 목록을 넣어 배열을 직접 생성하는 방법입니다. (예: [1, 2, 3])
- 배열 리터럴은 항상 고정된 크기의 memory 배열로 생성됩니다.
- 배열의 기본 타입은 리터럴의 첫 번째 요소를 기준으로 결정되며, 나머지 모든 요소는 이 타입으로 암묵적 형 변환이 가능해야 합니다.
- 고정 크기 memory 배열은 동적 크기 memory 배열에 할당할 수 없습니다.
62. push와 pop의 가스 비용
스토리지 배열의 길이를 변경하는 .push()와 .pop()은 가스 소모량이 다릅니다.
- .push(): 스토리지 공간은 기본적으로 0으로 초기화되어 있으므로, .push()를 호출하여 배열 길이를 늘리는 데 드는 가스 비용은 일정(constant)합니다.
- .pop(): 배열 길이를 줄이는 .pop()의 비용은 제거되는 요소의 "크기"에 따라 달라집니다. 만약 제거되는 요소가 다른 배열이나 복잡한 구조체라면, 그 내부의 모든 데이터를 명시적으로 삭제(delete)하는 것과 유사한 과정을 거치므로 매우 비쌀 수 있습니다.
63. 배열 슬라이스 (Array Slices)
배열 슬라이스는 배열의 연속된 일부분에 대한 "뷰(view)"를 제공하며, x[start:end] 형태로 사용합니다.
- start는 포함되고 end는 포함되지 않습니다. (x[start]부터 x[end-1]까지)
- start와 end는 생략 가능하며, 생략 시 각각 0과 배열의 끝을 의미합니다.
- ⚠️ 매우 중요: 배열 슬라이스는 오직 calldata 배열에만 사용 가능합니다.
- 슬라이스는 타입 이름이 없으므로 변수에 직접 할당할 수 없으며, 중간 표현식에서만 존재합니다.
- 인덱스로 요소에 접근할 수 있지만, .length와 같은 멤버는 없습니다.
64. 구조체 타입 (Struct Types)
(이전 항목 복습) 구조체는 여러 타입을 하나의 단위로 묶는 사용자 정의 타입입니다.
- 구조체는 매핑이나 배열 내에서 사용될 수 있으며, 자기 자신 안에 매핑이나 배열을 멤버로 가질 수 있습니다.
- ⚠️ 중요한 규칙: 구조체는 자기 자신 타입의 멤버를 포함할 수 없습니다. 이는 무한 재귀를 방지하기 위함입니다. (예: struct S { S self; }는 불가능)
65. 매핑 타입 (Mapping Types)
매핑은 mapping(_KeyType => _ValueType) 구문을 사용하여 키-값 쌍을 정의합니다.
- _KeyType: bytes, string, enum, 컨트랙트 등 내장 값 타입만 가능합니다. 매핑, 구조체, 배열 등 복잡한 타입은 키로 사용할 수 없습니다.
- _ValueType: 매핑을 포함한 모든 타입을 값으로 사용할 수 있습니다.
- 핵심 특징:
- 실제 키 데이터가 저장되지 않고, 키의 keccak256 해시값을 사용하여 값을 찾아갑니다. 이 때문에 매핑은 반복문(iteration)을 돌릴 수 없습니다.
- 매핑은 오직 storage에만 존재할 수 있습니다.
- public으로 공개된 함수의 파라미터나 반환 값으로 사용할 수 없습니다.
66. L-Value 관련 연산자
L-Value(할당 가능한 변수 등)에 사용되는 특별한 연산자들입니다.
- a += e는 a = a + e와 동일합니다. (-=, *=, /=, %= 등도 마찬가지)
- a++ (후위 증가): a의 값을 1 증가시키지만, 표현식 자체는 증가하기 전의 원래 값을 반환합니다.
- ++a (전위 증가): a의 값을 1 증가시키고, 증가된 후의 새로운 값을 반환합니다.
67. delete 연산자
delete는 변수를 해당 타입의 초기 기본값(0)으로 되돌립니다.
- 정수: a = 0과 동일합니다.
- 배열: delete a는 동적 배열의 길이를 0으로 만들거나, 정적 배열의 모든 요소를 기본값으로 초기화합니다. delete a[x]는 해당 인덱스의 요소만 삭제하며 배열 길이는 그대로 둡니다.
- 구조체: 모든 멤버를 기본값으로 리셋합니다.
- 매핑:
- delete는 매핑 자체에는 아무런 영향을 주지 않습니다.
- 하지만 delete a[x]처럼 특정 키에 해당하는 값은 삭제할 수 있습니다.
68. 암묵적 형 변환 (Implicit Conversions)
컴파일러가 특정 상황에서 자동으로 타입을 변환하는 것입니다.
- 규칙: 의미적으로 타당하고 정보 손실이 없을 때만 가능합니다.
- 예시: uint8는 uint16으로 변환 가능하지만, 음수를 표현할 수 없는 uint256으로는 int8을 변환할 수 없습니다.
69. 명시적 형 변환 (Explicit Conversions)
개발자가 강제로 타입을 변환하는 것입니다. 컴파일러의 안전장치를 우회하므로 예기치 않은 결과를 낳을 수 있어 주의가 필요합니다.
- 정수: 더 작은 타입으로 변환 시 상위 비트가 잘리고, 더 큰 타입으로 변환 시 왼쪽에 패딩이 추가됩니다.
- 고정 크기 바이트: 더 작은 타입으로 변환 시 오른쪽 바이트가 잘리고, 더 큰 타입으로 변환 시 오른쪽에 패딩이 추가됩니다.
70. 리터럴과 기본 타입 간의 변환
코드에 직접 작성된 값(리터럴)과 변수 타입 간의 변환 규칙입니다.
- 숫자 리터럴: 잘림 없이 표현 가능한 모든 정수 타입으로 암묵적 변환이 가능합니다.
- 문자열/16진수 문자열 리터럴: bytes 타입의 크기와 문자열의 길이가 정확히 일치할 경우, 해당 고정 크기 바이트 배열(bytes1~bytes32)로 변환될 수 있습니다.
71. 이더 단위 (Ether Units)
숫자 리터럴 뒤에 wei, gwei, ether와 같은 접미사를 붙여 이더의 하위 단위를 직접 표현할 수 있습니다.
- wei가 가장 작은 기본 단위입니다.
- 1 gwei == 1e9 wei (10억 wei)
- 1 ether == 1e18 wei (10^18 wei)
72. 시간 단위 (Time Units)
숫자 리터럴 뒤에 시간 단위 접미사를 붙여 시간 값을 표현할 수 있습니다.
- seconds가 기본 단위이며, 1 minutes는 60 seconds, 1 hours는 60 minutes, 1 days는 24 hours, 1 weeks는 7 days와 동일하게 변환됩니다.
- ⚠️ 주의: 이 단위를 사용하여 달력을 계산할 때는 주의해야 합니다. 윤년이나 윤초 때문에 모든 년이 365일이 아니고, 모든 날이 24시간인 것은 아닙니다.
- 이 접미사는 변수에 직접 붙일 수는 없지만, 곱셈을 통해 적용할 수 있습니다. (예: uint256 timeLimit = 5 * 1 minutes;)
73. 블록 및 트랜잭션 속성 (Global Variables)
컨트랙트 내 어디서든 접근할 수 있는, 블록체인과 트랜잭션에 대한 정보를 담고 있는 전역 변수 및 함수입니다.
- blockhash(uint blockNumber): 주어진 블록 번호의 해시값. (최근 256개 블록에 대해서만 작동)
- block.chainid: 현재 체인의 ID.
- block.coinbase: 현재 블록을 채굴한 채굴자의 주소.
- block.difficulty: 현재 블록의 난이도.
- block.gaslimit: 현재 블록의 가스 한도.
- block.number: 현재 블록 번호.
- block.timestamp: 현재 블록의 타임스탬프 (초 단위).
- gasleft(): 현재 남은 가스량.
- msg.data: 전체 calldata.
- msg.sender: 메시지(현재 함수 호출)를 보낸 주소. (바로 직전 호출자)
- msg.sig: calldata의 첫 4바이트 (호출된 함수의 시그니처).
- msg.value: 메시지와 함께 전송된 이더의 양 (wei 단위).
- tx.gasprice: 트랜잭션의 가스 가격.
- tx.origin: 트랜잭션을 최초로 시작한 외부 소유 계정(EOA) 주소. (msg.sender와 다를 수 있음)
74. msg 객체의 가변성
msg.sender와 msg.value를 포함한 msg 객체의 모든 값은 외부 함수가 호출될 때마다 변경될 수 있습니다.
- 이는 컨트랙트가 다른 컨트랙트를 호출할 때, 호출된 컨트랙트의 msg.sender는 호출한 컨트랙트의 주소가 되기 때문입니다. msg 객체는 항상 현재 실행되는 호출의 컨텍스트를 반영합니다.
75. 난수(Randomness) 생성 주의사항
⚠️ 절대로 block.timestamp나 blockhash를 난수 생성의 소스로 사용해서는 안 됩니다.
- 채굴자는 이 값들을 어느 정도 조작할 수 있습니다. 예를 들어, 특정 타임스탬프 값을 가진 블록을 만들기 위해 채굴을 잠시 보류할 수 있습니다. 이는 예측 가능한 결과를 낳아 보안 취약점으로 이어질 수 있습니다.
76. blockhash의 접근 제한
확장성 문제로 인해 모든 블록의 해시값에 접근할 수는 없습니다.
- 가장 최근 256개 블록의 해시값만 조회가 가능하며, 이보다 오래된 블록의 해시를 조회하면 0이 반환됩니다.
77. ABI 인코딩 및 디코딩 함수
ABI(Application Binary Interface) 명세에 따라 데이터를 인코딩(압축)하거나 디코딩(해석)하는 내장 함수입니다.
- abi.decode(데이터, (타입, ...)): 바이트 데이터를 주어진 타입으로 디코딩합니다.
- abi.encode(...): 주어진 인자들을 ABI 명세에 따라 인코딩합니다.
- abi.encodePacked(...): 주어진 인자들을 빈틈없이 빽빽하게 인코딩합니다. 데이터 충돌의 모호함이 발생할 수 있어 주의가 필요합니다.
- abi.encodeWithSelector(선택자, ...): 주어진 인자들을 인코딩한 후, 그 앞에 4바이트 함수 선택자를 붙입니다.
- abi.encodeWithSignature("함수서명", ...): 주어진 함수 서명을 해시하여 선택자를 만든 후 인자들과 함께 인코딩합니다.
78. 에러 처리 (Error Handling)
실행을 중단하고 상태 변경을 되돌리는(revert) 방법입니다.
- assert(bool 조건): 내부 오류를 검사하는 데 사용됩니다. 조건이 false이면 Panic 에러를 발생시킵니다. (예: 코드 로직상 절대 일어나면 안 되는 일 검사)
- require(bool 조건, "에러 메시지"): 입력값이나 외부 요소를 검사하는 데 사용됩니다. 조건이 false이면 실행을 되돌립니다. 가장 일반적으로 사용되는 에러 처리 방식입니다.
- revert("에러 메시지"): 조건 없이 즉시 실행을 중단하고 상태를 되돌립니다.
79. 수학 및 암호학 함수
자주 사용되는 수학 및 암호학 관련 내장 함수입니다.
- addmod(x, y, k): (x + y) % k를 계산합니다. 2^256 오버플로우 없이 계산합니다.
- mulmod(x, y, k): (x * y) % k를 계산합니다. 2^256 오버플로우 없이 계산합니다.
- keccak256(데이터): Keccak-256 해시를 계산합니다.
- sha256(데이터): SHA-256 해시를 계산합니다.
- ripemd160(데이터): RIPEMD-160 해시를 계산합니다.
- ecrecover(해시, v, r, s): 타원 곡선 디지털 서명(ECDSA)으로부터 서명자의 주소를 복구합니다.
80. ecrecover의 서명 가변성(Malleability) 주의사항
⚠️ ecrecover를 사용할 때, 서명 가변성(signature malleability) 취약점에 유의해야 합니다.
- 개인키 없이도, 유효한 서명을 약간 변형하여 내용은 같지만 형식이 다른 또 다른 유효한 서명을 만들어낼 수 있습니다.
- 만약 서명 자체를 고유 ID처럼 사용한다면, 동일한 내용에 대해 여러 개의 유효한 서명이 존재할 수 있어 문제가 될 수 있습니다.
- 이 문제를 해결하려면 OpenZeppelin의 ECDSA 라이브러리처럼 검증된 라이브러리를 사용하는 것이 안전합니다.
81. 컨트랙트 관련 키워드
- this: 현재 컨트랙트 자신을 가리키는 키워드입니다. address 타입으로 명시적 형 변환이 가능합니다.
- selfdestruct(address payable 수령인): 현재 컨트랙트를 파괴하고, 컨트랙트에 남아있는 모든 이더(Ether)를 지정된 수령인 주소로 보냅니다.
82. selfdestruct의 특이사항
selfdestruct는 몇 가지 독특한 동작 방식을 가집니다.
- 이더를 받는 수령인 컨트랙트의 receive 함수나 fallback 함수가 실행되지 않습니다.
- 컨트랙트는 실제로는 트랜잭션이 끝나는 시점에 완전히 파괴됩니다.
- 만약 selfdestruct 호출 이후에 revert가 발생하면, 컨트랙트 파괴가 "취소"될 수 있습니다.
83. 타입 정보 (컨트랙트)
type(X) 표현식을 사용하여 타입 X에 대한 정보를 얻을 수 있습니다.
- type(C).name: 컨트랙트 C의 이름(string)을 반환합니다.
- type(C).creationCode: 컨트랙트 C를 생성하는 바이트코드(memory 바이트 배열)를 반환합니다. create2 옵코드를 사용한 커스텀 배포 로직에 활용될 수 있습니다.
- type(C).runtimeCode: 컨트랙트 C의 런타임 바이트코드(memory 바이트 배열)를 반환합니다. 블록체인에 실제 배포되어 실행되는 코드입니다.
- type(I).interfaceId: 인터페이스 I의 EIP-165 인터페이스 식별자(bytes4)를 반환합니다.
84. 타입 정보 (정수)
정수 타입 T에 대한 정보를 얻을 수 있습니다.
- type(T).min: 타입 T가 표현할 수 있는 최솟값을 반환합니다.
- type(T).max: 타입 T가 표현할 수 있는 최댓값을 반환합니다.
85. 제어 구조 (Control Structures)
솔리디티는 C나 JavaScript와 의미가 동일한 if, else, while, do, for, break, continue, return 제어문을 지원합니다.
- 조건문에서 괄호 ()는 생략할 수 없지만, 실행할 구문이 한 줄이라면 중괄호 {}는 생략할 수 있습니다.
- ⚠️ 중요: C나 JavaScript와 달리, 숫자 1을 true로 자동 변환하지 않습니다. 따라서 if (1) { ... }와 같은 코드는 유효하지 않습니다.
86. 예외 처리 (Exceptions)
솔리디티는 상태를 되돌리는(state-reverting) 방식으로 에러를 처리합니다. 예외가 발생하면 현재 호출 및 모든 하위 호출에서 발생한 상태 변경이 모두 취소됩니다.
- 예외는 기본적으로 상위 호출로 "버블업(bubble up)"되어 전파됩니다.
- 예외 전파가 일어나지 않는 경우: send 및 로우레벨 함수(call, delegatecall, staticcall)는 예외를 전파하는 대신, 첫 번째 반환 값으로 false를 반환합니다.
- 외부 호출에서 발생하는 예외는 try/catch 문으로 잡을 수 있습니다.
- 예외는 두 종류의 시그니처를 가집니다.
- Error(string): 일반적인 에러 상황(require에서 발생)에 사용됩니다.
- Panic(uint256): 버그가 없는 코드에서는 절대 발생해서는 안 되는 에러(assert에서 발생)에 사용됩니다.
87. 로우레벨 함수의 특이사항
⚠️ EVM의 설계상, 로우레벨 함수(call, delegatecall, staticcall)는 호출 대상 계정이 존재하지 않을 경우에도 true를 반환합니다. 따라서 필요한 경우, 함수 호출 전에 대상 계정이 실제로 존재하는지 별도로 확인해야 합니다.
88. assert 함수
assert는 내부 오류를 테스트하고, 불변성(invariants)을 확인하는 데에만 사용해야 합니다.
- assert 조건이 false이면 Panic(uint256) 타입의 에러를 발생시킵니다.
- 올바르게 작동하는 코드는 잘못된 외부 입력이 들어오더라도 절대 Panic을 발생시켜서는 안 됩니다.
89. Panic 발생 상황
다음과 같은 상황에서 Panic 예외가 발생하며, 에러 코드를 통해 원인을 알 수 있습니다.
- 0x01: assert의 인자가 false로 평가될 때.
- 0x11: unchecked 블록 외부에서 산술 연산 오버플로우/언더플로우가 발생할 때.
- 0x12: 0으로 나누거나 나머지 연산을 할 때.
- 0x21: 너무 크거나 음수인 값을 enum 타입으로 변환할 때.
- 0x31: 비어있는 배열에 .pop()을 호출할 때.
- 0x32: 배열이나 배열 슬라이스에 범위를 벗어난 인덱스로 접근할 때.
- 0x41: 메모리를 너무 많이 할당하거나 너무 큰 배열을 생성할 때.
- 0x51: 초기화되지 않은 내부 함수 타입 변수를 호출할 때.
90. require 함수
require는 실행 시점까지는 알 수 없는 유효한 조건을 보장하기 위해 사용되어야 합니다.
- 주로 함수에 전달된 입력값을 검증하거나, 외부 컨트랙트 호출의 반환 값을 확인하는 데 쓰입니다.
- 조건이 false이면 Error(string) 타입의 에러를 발생시킵니다. assert와 달리, 선택적으로 에러 메시지를 제공할 수 있습니다.
91. Error(string) 예외 발생 상황
다음과 같은 상황에서 Error(string) 예외(또는 데이터 없는 예외)가 발생합니다.
- require의 인자가 false로 평가될 때.
- 코드가 없는 컨트랙트를 대상으로 외부 함수 호출을 수행할 때.
- payable 지정자가 없는 public 함수(생성자, 폴백 함수 포함)를 통해 이더(Ether)를 받을 때.
- public getter 함수를 통해 이더(Ether)를 받을 때.
92. revert
revert 문(statement)이나 함수를 사용하여 직접 예외를 발생시킬 수 있습니다.
- revert CustomError(arg1, ...);: 커스텀 에러를 발생시키는 최신 방식으로, 괄호 없이 사용합니다. 에러 이름 자체가 4바이트로 인코딩되므로 문자열을 사용하는 것보다 가스비가 훨씬 저렴합니다.
- revert("에러 메시지");: 에러 메시지 문자열과 함께 예외를 발생시키는 함수입니다.
- 자세한 설명은 비용이 들지 않는 NatSpec 주석을 통해 제공하는 것이 좋습니다.
93. try/catch - try 블록
try 키워드는 외부 함수 호출이나 컨트랙트 생성(new ContractName())에서 발생할 수 있는 에러를 처리하는 데 사용됩니다.
- try 블록은 외부 호출 자체에서 발생하는 revert만 감지하며, try를 감싸는 복잡한 표현식 내부의 다른 에러는 감지하지 못합니다.
- returns (반환타입 변수명) 부분을 추가하여 외부 호출의 반환 값을 받을 수 있습니다.
- 에러 없이 호출이 성공하면, 첫 번째 성공 블록({...})이 실행됩니다.
94. try/catch - catch 블록
에러 유형에 따라 다른 catch 블록을 사용하여 에러를 처리할 수 있습니다.
- catch Error(string memory reason) { ... }: revert("이유")나 require(false, "이유")로 발생한 일반적인 에러를 잡습니다.
- catch Panic(uint errorCode) { ... }: assert 실패, 0으로 나누기, 배열 접근 오류, 산술 오버플로우 등 치명적인 내부 에러(Panic)를 잡습니다.
- catch (bytes memory lowLevelData) { ... }: 에러 시그니처가 다른 catch 절과 일치하지 않거나 에러 데이터가 없는 경우 실행됩니다. 로우레벨 에러 데이터에 직접 접근할 수 있습니다.
- catch { ... }: 에러 데이터에 관심이 없다면 가장 간단한 형태로 사용할 수 있습니다.
95. try/catch와 상태 변경
- 실행 흐름이 catch 블록에 도달했다면, 외부 호출로 인한 상태 변경은 모두 되돌려진(reverted) 것입니다.
- 실행 흐름이 성공 블록에 도달했다면, 외부 호출로 인한 상태 변경은 그대로 유지된 것입니다.
96. 호출 실패의 원인
⚠️ 외부 호출 실패의 원인을 단정하지 마세요.
- 에러 메시지는 호출한 컨트랙트가 아니라, 그보다 더 깊은 호출 체인에서 발생하여 전달된 것일 수 있습니다.
- 또한, 의도된 에러 조건이 아니라 가스 부족(out-of-gas)으로 인해 호출이 실패했을 수도 있습니다. (호출자는 항상 가스의 63/64을 보유하므로, 피호출자가 가스를 다 써도 호출자는 약간의 가스가 남아있습니다.)
97. 프로그래밍 스타일
코딩 스타일은 일관성이 핵심입니다. 일관성 있는 스타일은 코드의 가독성과 유지보수성을 높이며, 이는 궁극적으로 보안에 영향을 미칩니다.
98. 코드 레이아웃
- 들여쓰기: 레벨당 4칸 공백(space)을 사용합니다. (탭 사용 금지)
- 빈 줄: 최상위 선언(컨트랙트, 라이브러리 등) 사이에는 두 줄의 빈 줄을 둡니다.
- 줄 길이: 한 줄은 최대 79자 또는 99자로 제한하는 것이 좋습니다.
- 파일 인코딩: UTF-8을 권장합니다.
- 임포트: import 문은 항상 파일의 최상단에 위치시킵니다.
- 함수 순서: 가독성을 위해 constructor → receive → fallback → external → public → internal → private 순서로 그룹화하고, 각 그룹 내에서는 view와 pure 함수를 마지막에 배치합니다.
99. 코드 레이아웃 (추가)
- 공백: 괄호 (), 대괄호 [], 중괄호 {} 바로 안쪽에는 추가 공백을 넣지 마세요.
- 중괄호: 선언과 같은 줄에서 열고({), 별도의 줄에서 닫습니다(}).
- 문자열: 작은따옴표(') 대신 큰따옴표(")를 사용합니다.
- 연산자: 연산자 양옆에는 한 칸의 공백을 둡니다.
- 파일 요소 순서: Pragma → Import → Interface → Library → Contract 순서로 배치합니다.
100. 이름 규칙 (Naming Convention)
- 피해야 할 이름: l(소문자 L), O(대문자 O), I(대문자 I)는 숫자 1, 0과 혼동되기 쉬우므로 한 글자 변수명으로 절대 사용하지 마세요.
- 컨트랙트, 라이브러리, 구조체, 이벤트: CapWords (파스칼 케이스, PascalCase)를 사용합니다. (예: SimpleToken, MyStruct)
- 함수: mixedCase (카멜 케이스, camelCase)를 사용합니다. (예: getBalance)
101. 이름 규칙 (추가)
- 함수 인자, 지역/상태 변수: mixedCase (카멜 케이스)를 사용합니다.
- 상수: UPPER_CASE_WITH_UNDERSCORES (대문자 스네이크 케이스)를 사용합니다. (예: MAX_BLOCKS)
- 수정자(Modifiers): mixedCase (카멜 케이스)를 사용합니다.
- 열거형(Enums): CapWords (파스칼 케이스)를 사용합니다.
- 이름 충돌 회피: 예약어와 이름이 충돌할 경우, 이름 뒤에 언더스코어(_)를 붙이는 관례를 사용합니다. (예: address_)
https://secureum.substack.com/p/solidity-101
Solidity 101
101 key aspects of Solidity
secureum.substack.com
'Web3 > Solidity' 카테고리의 다른 글
| [Secureum] Solidity 201 (2) | 2025.07.11 |
|---|---|
| [Solidity] Basic Syntax (2) | 2025.06.06 |