c0mpos3r

[Secureum] Solidity 201 본문

Web3/Solidity

[Secureum] Solidity 201

음대생 2025. 7. 11. 19:51

101 key aspects of Solidity

102. 다중 상속과 다형성

솔리디티는 다형성을 포함한 다중 상속을 지원합니다.

  • 다형성 (Polymorphism): 함수를 호출하면, 상속 계층 구조상 가장 하위(most derived)에 있는 컨트랙트의 함수가 항상 실행되는 특징을 의미합니다.
  • 컴파일 방식: 여러 컨트랙트를 상속받아도 블록체인에는 단 하나의 컨트랙트만 생성됩니다. 모든 부모 컨트랙트의 코드가 이 하나의 컨트랙트 안으로 컴파일되어 합쳐집니다.
  • 함수 오버라이딩 (Function Overriding): 부모 컨트랙트의 함수에 virtual 키워드가 붙어 있으면, 자식 컨트랙트에서 해당 함수의 동작을 덮어쓸 수 있습니다. 이때 자식 컨트랙트의 함수에는 반드시 override 키워드를 사용해야 합니다.
  • 다이아몬드 문제와 C3 선형화: 다중 상속 시 발생하는 다이아몬드 문제를 해결하기 위해, 솔리디티는 파이썬처럼 C3 선형화(C3 Linearization) 알고리즘을 사용합니다. 이를 통해 상속 순서를 명확하게 정의하며, 솔리디티는 오른쪽에서 왼쪽 순으로 부모 컨트랙트를 검색하여 처음 일치하는 함수를 실행합니다.

103. 특수한 컨트랙트 타입

  • 추상 컨트랙트 (Abstract Contracts): 하나 이상의 함수가 구현되지 않은 컨트랙트를 의미하며, abstract 키워드를 붙여야 합니다. 인스턴스화(배포)할 수 없으며, 상속을 통해서만 사용됩니다.
  • 인터페이스 (Interfaces): 함수의 명세만 있고 구현된 함수가 전혀 없는 순수한 설계도입니다.
    • 제약사항: 다른 컨트랙트를 상속할 수 없지만, 다른 인터페이스는 상속 가능합니다. / 모든 함수는 external이어야 합니다. / constructor나 상태 변수를 선언할 수 없습니다.
  • 라이브러리 (Libraries): 특정 주소에 단 한 번만 배포되고, 여러 컨트랙트에서 DELEGATECALL을 통해 코드를 재사용하는 특수한 컨트랙트입니다. 라이브러리 함수가 호출되면, 코드는 호출한 컨트랙트의 컨텍스트(스토리지, 상태 등)에서 실행됩니다.

104. using A for B

using A for B; 구문은 라이브러리 A의 함수들을 특정 타입 B에 첨부(attach)하는 데 사용됩니다.

  • 이 지시어를 사용하면, 타입 B의 변수에서 라이브러리 함수를 마치 멤버 함수처럼 호출할 수 있습니다. (예: myUint.add(5);)
  • 이때 타입 B의 변수(myUint)는 해당 라이브러리 함수의 첫 번째 파라미터로 자동으로 전달됩니다.
  • using for는 선언된 컨트랙트 내부에서만 유효합니다.

105. 부모 클래스 함수 호출

자식 컨트랙트에서 상위 계층에 있는 부모 컨트랙트의 함수를 명시적으로 호출할 수 있습니다.

  • ContractName.functionName(): 특정 부모 컨트랙트의 함수를 직접 지정하여 호출합니다.
  • super.functionName(): C3 선형화 순서상 바로 한 단계 위 부모의 함수를 호출합니다.

106. 상태 변수 섀도잉 (State Variable Shadowing)

솔리디티에서는 상태 변수 섀도잉(부모와 자식 컨트랙트에 동일한 이름의 상태 변수를 선언하는 것)이 에러로 처리됩니다. 즉, 자식 컨트랙트는 부모 컨트랙트에서 보이는 상태 변수와 동일한 이름의 상태 변수를 선언할 수 없습니다.


107. 함수 오버라이딩 시 변경 규칙

함수를 오버라이딩할 때, 가시성(visibility)과 상태 변경성(mutability)을 일부 변경할 수 있습니다.

  • 가시성: external에서 public으로만 변경 가능합니다. (더 넓은 범위로 변경)
  • 상태 변경성: 더 엄격한 쪽으로만 변경 가능합니다. (nonpayable → view → pure 순서)
  • ⚠️ 예외: payable 함수는 다른 어떤 상태 변경성으로도 변경할 수 없습니다.

108. virtual 함수

  • 인터페이스가 아닌 곳에서 구현부가 없는 함수는 반드시 virtual로 표시해야 합니다.
  • 인터페이스 내의 모든 함수는 자동으로 virtual로 간주됩니다.
  • private 함수는 virtual이 될 수 없습니다. (상속 및 오버라이딩 대상이 아니므로)

109. public 상태 변수 오버라이드

public 상태 변수는 특정 조건 하에 오버라이딩에 참여할 수 있습니다.

  • ✅ public 상태 변수는 external 함수를 오버라이드할 수 있습니다. 단, 해당 함수의 파라미터와 반환 타입이 변수의 자동 생성된 getter 함수와 일치해야 합니다.
  • ❌ 하지만, public 상태 변수 자체는 다른 함수나 변수에 의해 오버라이드될 수 없습니다.

 

110. 수정자 오버라이딩 (Modifier Overriding)

함수 수정자(modifier)도 함수처럼 서로 오버라이드할 수 있습니다.

  • 오버라이드될 부모 수정자에는 virtual 키워드를, 오버라이드하는 자식 수정자에는 override 키워드를 사용해야 합니다.
  • 단, 함수와 달리 수정자는 오버로딩( 동일 이름, 다른 파라미터)을 지원하지 않습니다.

111. 부모 생성자 (Base Constructors)

자식 컨트랙트를 배포할 때, 모든 부모 컨트랙트의 생성자는 C3 선형화 순서에 따라 호출됩니다.

  • 만약 부모 생성자에 인자(argument)가 필요하다면, 자식 컨트랙트는 반드시 그 인자들을 명시해야 합니다. 인자는 상속 목록에서 직접 지정하거나 (contract Child is Parent("arg") { ... }), 자식의 생성자 내부에서 지정할 수 있습니다.

112. 이름 충돌 에러

상속으로 인해 컨트랙트 내에서 다음 쌍들이 동일한 이름을 갖게 되면 컴파일 에러가 발생합니다.

  • 함수수정자
  • 함수이벤트
  • 이벤트수정자

113. 라이브러리 제약사항

라이브러리는 일반 컨트랙트와 비교하여 다음과 같은 제약사항을 가집니다.

  • 상태 변수를 가질 수 없습니다.
  • 상속을 하거나 상속될 수 없습니다.
  • 이더(Ether)를 받을 수 없습니다. (payable 불가)
  • 파괴될 수 없습니다. (selfdestruct 불가)
  • 호출한 컨트랙트의 상태 변수에 접근하려면, 해당 변수를 명시적으로 함수 인자로 전달받아야 합니다.
  • 상태를 변경하지 않는 view 또는 pure 함수만 직접 호출할 수 있습니다. 상태를 변경하는 함수는 DELEGATECALL을 통해서만 호출됩니다.

114. EVM 스토리지

스토리지는 256비트 워드(word)를 키(key)로, 256비트 워드를 값(value)으로 매핑하는 키-값 저장소입니다.

  • EVM의 SSTORE(쓰기)와 SLOAD(읽기) 명령어를 통해 접근합니다.
  • 모든 스토리지 위치(슬롯)는 기본적으로 0으로 초기화됩니다.

115. 스토리지 레이아웃

컨트랙트의 상태 변수들은 여러 값이 때로 하나의 스토리지 슬롯을 공유하는 방식으로 컴팩트하게 저장됩니다.

  • 동적 크기 배열과 매핑을 제외하고, 데이터는 첫 번째 상태 변수부터 순서대로 슬롯 0번부터 차곡차곡 저장됩니다.

116. 스토리지 레이아웃 패킹

32바이트보다 작은 여러 개의 연속된 항목들은 가능하면 하나의 스토리지 슬롯으로 묶입니다(packed).

  • 슬롯의 첫 번째 항목은 하위 순서에 맞춰 정렬됩니다. (오른쪽부터 채워짐)
  • 값 타입은 자신을 저장하는 데 필요한 만큼의 바이트만 사용합니다.
  • 만약 어떤 값이 슬롯의 남은 공간에 맞지 않으면, 다음 슬롯으로 넘어갑니다.

117. 스토리지 레이아웃과 구조체/배열

  • 구조체와 배열 데이터는 항상 새로운 슬롯에서 시작하며, 그 내부의 항목들은 위 패킹 규칙에 따라 촘촘하게 채워집니다.
  • 구조체나 배열 뒤에 오는 항목 또한 항상 새로운 슬롯에서 시작합니다.

118. 스토리지 레이아웃과 상속

상속을 사용하는 컨트랙트에서 상태 변수의 순서는, 가장 상위 부모 컨트랙트부터 시작하는 C3 선형화 순서에 의해 결정됩니다.

  • 패킹 규칙이 허용한다면, 서로 다른 부모 컨트랙트로부터 물려받은 상태 변수들이 하나의 스토리지 슬롯을 공유할 수도 있습니다.

119. 스토리지 레이아웃과 타입

💡 스토리지 값을 다룰 때는 uint256보다 작은 타입(예: uint128)을 사용하는 것이 가스비 절약에 유리할 수 있습니다. 컴파일러가 여러 요소를 하나의 슬롯으로 묶어, 여러 번의 읽기/쓰기를 한 번의 작업으로 처리할 수 있기 때문입니다.

  • ⚠️ 반대 효과: 하지만 슬롯에 묶인 값들 중 하나만 변경할 경우, 컴파일러는 먼저 슬롯 전체를 읽고(SLOAD), 값을 수정한 뒤, 다시 슬롯 전체를 써야(SSTORE) 합니다. 이 "읽고-쓰기" 과정은 때로 더 비효율적일 수 있습니다.

120. 스토리지 레이아웃과 순서

💡 상태 변수나 구조체 멤버의 선언 순서는 패킹 효율에 직접적인 영향을 미칩니다.

  • 예시 1: uint128, uint128, uint256 순서로 선언 → 2 슬롯 사용
    • (슬롯 0: uint128, uint128이 함께 패킹됨 / 슬롯 1: uint256)
  • 예시 2: uint128, uint256, uint128 순서로 선언 → 3 슬롯 사용
    • (슬롯 0: uint128 / 슬롯 1: uint256 / 슬롯 2: 남은 uint128)

121. 매핑과 동적 배열의 스토리지 레이아웃 (개요)

크기를 예측할 수 없는 매핑(mapping)과 동적 크기 배열(dynamically-sized array)은 다른 상태 변수들 사이에 "인라인(in-line)"으로 저장될 수 없습니다.

  • 대신, 일반적인 스토리지 레이아웃 규칙상으로는 32바이트 슬롯 하나만 차지하는 것으로 간주됩니다.
  • 이 슬롯은 일종의 위치 표시자(placeholder) 역할을 하며, 실제 데이터는 이 슬롯의 위치를 keccak256으로 해시하여 계산된 완전히 다른 스토리지 위치부터 저장됩니다.

122. 동적 배열의 스토리지 레이아웃

  • 동적 배열의 위치 표시자 슬롯 p에는 배열의 요소 개수(길이)가 저장됩니다. (bytes와 string은 예외)
  • 실제 배열 데이터는 keccak256(p) 주소부터 저장되며, 고정 크기 배열처럼 요소들이 차례대로 저장됩니다. (요소가 16바이트보다 작으면 패킹 가능)

123. 매핑의 스토리지 레이아웃

  • 매핑의 위치 표시자 슬롯 p는 의도적으로 비워둡니다. 이 슬롯의 역할은 단지 다른 매핑과 데이터 위치가 겹치지 않도록 공간을 차지하는 것입니다.
  • 키(key) k에 해당하는 값(value)은 keccak256(h(k) . p) 주소에 위치합니다. 여기서 .는 바이트 연결을 의미합니다.
  • h(k)는 키의 타입에 따라 키를 32바이트로 변환하는 방식입니다. (일반 값 타입은 패딩, string이나 bytes는 해시)

124. bytes와 string의 스토리지 레이아웃

bytes와 string은 동일하게 인코딩되며, 짧은 배열 최적화 기법을 사용합니다.

  • 데이터가 31바이트 이하인 경우 (짧은 배열):
    • 데이터와 길이가 하나의 슬롯에 함께 저장됩니다.
    • 슬롯의 최하위 바이트에 길이 * 2 값을 저장하고, 나머지 상위 바이트에 실제 데이터를 저장합니다.
  • 데이터가 32바이트 이상인 경우 (긴 배열):
    • 슬롯 p에 길이 * 2 + 1 값을 저장합니다.
    • 실제 데이터는 keccak256(p) 주소에 저장됩니다.
  • 💡 구분법: 슬롯 값의 최하위 비트가 0이면 짧은 배열, 1이면 긴 배열입니다.

125. EVM 메모리

메모리는 트랜잭션 실행 중에 임시 데이터를 저장하는 휘발성 공간입니다.

  • 선형적이며 바이트 단위로 주소를 지정할 수 있습니다.
  • MSTORE(쓰기), MLOAD(읽기) 명령어를 통해 접근합니다.
  • 새로운 실행 컨텍스트가 시작될 때마다 모든 위치가 0으로 초기화됩니다.

126. 메모리 레이아웃

솔리디티는 자유 메모리 포인터(free memory pointer)가 가리키는 위치에 새로운 객체를 할당하며, 한 번 할당된 메모리는 절대 해제되지 않습니다.

  • 자유 메모리 포인터는 초기에 0x80을 가리킵니다.

127. 예약된 메모리 공간

솔리디티는 특정 목적을 위해 메모리의 일부 공간을 예약해 둡니다.

  • 0x00 - 0x3f (64바이트): 해시 계산을 위한 임시 공간(scratch space).
  • 0x40 - 0x5f (32바이트): 자유 메모리 포인터가 저장되는 위치.
  • 0x60 - 0x7f (32바이트): 0 슬롯(zero slot). 비어있는 동적 메모리 배열의 초기값으로 사용되며, 절대 덮어쓰면 안 됩니다.

128. 메모리에서의 배열 레이아웃

  • 메모리에서 배열 요소는 항상 32바이트의 배수 공간을 차지합니다. byte[]도 마찬가지이며, 이는 스토리지의 패킹 규칙과 큰 차이점입니다. (bytes와 string은 예외)
  • 다차원 메모리 배열은 다른 메모리 배열을 가리키는 포인터입니다.
  • 동적 메모리 배열의 길이는 배열 데이터의 첫 번째 슬롯에 저장됩니다.

129. 자유 메모리 포인터

메모리의 0x40 주소에는 "다음에 할당 가능한 메모리 주소"를 가리키는 포인터 값이 저장되어 있습니다.

  • 메모리를 할당하려면 이 포인터가 가리키는 주소부터 사용하고, 사용한 만큼 포인터 값을 업데이트해야 합니다.
  • 예약된 공간 때문에, 할당 가능한 메모리는 0x80부터 시작하며 이것이 자유 메모리 포인터의 초기값입니다.

130. 초기화되지 않은 메모리

⚠️ 인라인 어셈블리 등 로우레벨 작업을 할 때, 특정 메모리 공간이 이전에 사용되지 않았다고 가정해서는 안 됩니다.

  • 즉, 해당 공간의 내용이 0이라고 절대 보장할 수 없습니다. 할당된 메모리를 해제하거나 비우는 내장 메커니즘은 없습니다.

131. 예약어 (Reserved Keywords)

솔리디티에서 예약된 키워드들입니다. 현재는 사용되지 않지만, 향후 언어의 일부가 될 수 있으므로 변수나 함수의 이름으로 사용할 수 없습니다.

  • 예: after, alias, auto, case, final, inline, macro, match, null, static, switch, typeof 등

132. 인라인 어셈블리 (Inline Assembly)

인라인 어셈블리는 EVM에 로우레벨로 직접 접근하는 방법입니다.

  • ⚠️ 경고: 이 기능은 솔리디티의 여러 안전장치와 검사를 우회하므로, 반드시 필요한 작업에만, 그리고 동작 방식에 대해 완전히 확신이 있을 때만 사용해야 합니다.
  • 인라인 어셈블리에 사용되는 언어는 Yul이라고 불립니다.
  • assembly { ... } 블록 안에 Yul 언어로 코드를 작성합니다.

133. 인라인 어셈블리에서 외부 변수/함수 접근

assembly 블록 안에서 솔리디티 변수에 접근하는 방법입니다.

  • 값 타입 지역 변수: 이름으로 직접 사용 가능합니다.
  • memory/calldata 참조 타입 지역 변수: 변수 이름은 값 자체가 아닌, 해당 변수의 메모리/calldata 주소를 의미합니다.
  • 스토리지 변수: 변수 이름 x를 직접 사용할 수 없으며, 에러가 발생합니다.
    • x.slot: 변수가 저장된 스토리지 슬롯 번호를 가져옵니다.
    • x.offset: 슬롯 내에서 변수가 시작하는 바이트 오프셋을 가져옵니다.

134. Yul 구문 (Syntax)

Yul에서 사용되는 주요 구문 요소들입니다.

  • let a := 10 : 변수 선언 및 할당
  • if ... { ... } : 조건문
  • switch ... case ... default ... : 스위치문
  • for { ... } { ... } { ... } : 초기화, 조건, 사후처리 블록을 가진 C언어 스타일의 for 루프
  • function f(a, b) -> c { ... } : 함수 정의

135. 솔리디티 v0.6.0 브레이킹 체인지 (동작 변경)

컴파일러 경고 없이 기존 코드의 동작이 변경된 사항입니다.

  • 제곱 연산(**): 결과값의 타입이 이제 항상 밑(base)의 타입을 따릅니다. (이전에는 밑과 지수 중 더 큰 타입을 따랐음)

136. 솔리디티 v0.6.0 명시성 요구사항 변경

코드를 더 명시적으로 작성하도록 강제하는 변경사항들입니다.

  • virtual / override: 함수 오버라이딩 시 virtual(부모)과 override(자식) 키워드 사용이 의무화되었습니다.
  • array.length: 스토리지 배열의 길이는 이제 읽기 전용이 되었으며, 길이에 직접 값을 할당하여 배열 크기를 조절할 수 없습니다. (push, pop 사용)
  • abstract: 구현되지 않은 함수가 있는 컨트랙트는 abstract로 명시해야 합니다.
  • 상태 변수 섀도잉: 이제 금지됩니다.

137. 솔리디티 v0.6.0 구문 및 의미 변경

  • fallback 함수의 분리: 기존의 익명 폴백 함수가 두 가지로 분리되었습니다.
    • receive(): calldata가 비어있을 때(주로 순수 이더 전송 시) 호출됩니다. 항상 payable입니다.
    • fallback(): 일치하는 함수가 없을 때 호출됩니다. payable 여부를 선택할 수 있습니다.

138. 솔리디티 v0.6.0 신규 기능

  • try/catch: 외부 호출 실패에 대응할 수 있는 예외 처리 구문이 도입되었습니다.
  • 파일 레벨 선언: struct와 enum을 컨트랙트 밖, 파일 레벨에서 선언할 수 있게 되었습니다.
  • 배열 슬라이스: calldata 배열에 슬라이스를 사용할 수 있게 되었습니다.
  • payable(x): address 타입을 address payable 타입으로 명시적 형 변환이 가능해졌습니다.

139. 솔리디티 v0.7.0 브레이킹 체인지 (동작 변경)

  • 리터럴 연산: 1 << x 나 2 ** x 처럼 리터럴 값에 변수로 쉬프트나 제곱 연산을 할 때, 이제 항상 uint256 또는 int256 타입을 사용하여 연산을 수행합니다. (이전에는 변수 x의 작은 타입을 따라가 오해의 소지가 있었음)

140. 솔리디티 v0.7.0 구문 변경

  • 가스/이더 지정 구문 변경: 외부 호출 시 가스와 이더를 지정하는 구문이 x.f{gas: 10000, value: 2 ether}(...) 형태로 변경되었습니다. (이전의 .gas().value() 구문은 에러 발생)
  • now 폐지: 전역 변수 now가 폐지되고, block.timestamp 사용이 권장됩니다. now라는 이름이 모호함을 주기 때문입니다.
  • gwei 키워드화: gwei가 이제 예약어가 되어 변수명 등으로 사용할 수 없습니다.
  • 상속 시 상태 변경성 제한: view 함수는 pure 함수로 오버라이드하는 등, 상속 시 함수의 상태 변경성을 더 엄격하게 제한할 수 있게 되었습니다.

141. 솔리디티 v0.7.0 – 불필요/위험 기능 제거

코드의 혼란과 에러를 줄이기 위해 제거된 기능들입니다.

  • 메모리 내 매핑 금지: struct나 array가 mapping을 포함하면, 이제 storage에서만 사용할 수 있습니다. (이전에는 memory에서 매핑이 조용히 무시되어 버그를 유발했음)
  • using A for B 상속 중단: 이제 using for 구문은 선언된 컨트랙트에서만 효력을 가지며, 자식 컨트랙트에서 사용하려면 별도로 다시 선언해야 합니다.
  • 음수 쉬프트 금지: 부호 있는 타입(음수)으로 쉬프트 연산을 하는 것이 금지되었습니다.
  • finney, szabo 단위 제거: 거의 사용되지 않는 이더 단위들이 제거되었습니다.
  • var 키워드 완전 제거: 이제 var 키워드를 사용하면 파서 에러가 발생합니다.

142. 솔리디티 v0.8.0 브레이킹 체인지 (동작 변경)

컴파일러 경고 없이 기존 코드의 동작이 크게 변경된, 매우 중요한 업데이트입니다.

  • Checked 산술 연산 기본 적용: 이제 모든 산술 연산은 오버플로우/언더플로우 발생 시 자동으로 revert됩니다. 이전처럼 순환(wrapping)하는 동작을 원하면 unchecked { ... } 블록을 사용해야 합니다.
  • ABI 인코더 v2 기본 활성화: 더 복잡한 타입을 처리할 수 있는 ABI 인코더 v2가 기본값이 되었습니다.
  • 제곱 연산 순서 변경: a**b**c는 이제 a**(b**c)로 계산됩니다. (우측 결합)
  • assert 실패 시 revert 사용: assert 실패나 0으로 나누기 등 내부 에러 발생 시, 가스 소모가 큰 invalid 옵코드 대신 Panic(uint256) 에러를 발생시키는 revert 옵코드를 사용하도록 변경되어 가스 효율이 개선되었습니다.
  • byte 타입 제거: bytes1의 별칭이었던 byte 타입이 제거되었습니다.

143. 솔리디티 v0.8.0 신규 제약사항

v0.8.0부터 기존 코드가 컴파일되지 않을 수 있는 문법적 제약사항들입니다.

  • address로의 명시적 형 변환 강화: 음수 리터럴 등을 address로 변환하는 것이 금지되었습니다.
  • address 리터럴 타입 변경: 0x...와 같은 주소 리터럴은 이제 address payable이 아닌 address 타입을 가집니다. payable로 사용하려면 payable(0x...)과 같이 명시적으로 변환해야 합니다.
  • tx.origin, msg.sender 타입 변경: 이제 address payable이 아닌 address 타입을 가집니다.
  • log0~log4 함수 제거: 거의 사용되지 않는 로우레벨 로그 함수들이 제거되었습니다.
  • enum 멤버 수 제한: enum은 최대 256개의 멤버만 가질 수 있도록 제한됩니다.

144. 0번 주소 체크 (Zero Address Check)

address(0) (모든 바이트가 0인 주소)는 개인키가 알려져 있지 않아 특별하게 다뤄져야 합니다.

  • 이 주소로 전송된 이더나 토큰은 영원히 찾을 수 없습니다.
  • 접근 제어 권한을 이 주소에 부여하면 아무도 해당 권한을 사용할 수 없게 됩니다.
  • 따라서 사용자가 주소를 입력하는 기능에는 항상 address(0)이 아닌지 확인하는 로직을 구현해야 합니다.

145. tx.origin 체크

tx.origin == msg.sender인지 확인하는 것은 현재 호출자가 외부 소유 계정(EOA)인지 컨트랙트인지 판별하는 효과적인 방법입니다.

  • tx.origin은 트랜잭션을 최초로 발생시킨 EOA 주소이고, msg.sender는 바로 직전 호출자입니다.
  • 만약 두 주소가 같다면, 이는 컨트랙트를 거치지 않고 EOA가 직접 함수를 호출했음을 의미합니다.

146. 오버플로우/언더플로우 체크

솔리디티 0.8.0 이전 버전에서는 산술 연산이 기본적으로 체크되지 않아 오버플로우/언더플로우에 취약했습니다.

  • 해당 버전의 컨트랙트에서는 OpenZeppelin의 SafeMath 라이브러리를 사용하여 안전하게 산술 연산을 처리하는 것이 권장되는 모범 사례였습니다.

147. OpenZeppelin 라이브러리

OpenZeppelin은 스마트 컨트랙트 프로젝트에서 가장 널리 사용되는 라이브러리입니다.

  • ERC20, ERC721 등 표준 토큰, 접근 제어, 보안 유틸리티, 프록시 등 감사받고 검증된 안전한 코드 모음집을 제공합니다.

148. OpenZeppelin ERC20

널리 사용되는 ERC20 토큰 표준을 구현한 라이브러리입니다.

  • 주요 함수: name, symbol, decimals, totalSupply, balanceOf, transfer, approve, transferFrom 등 표준 함수를 제공합니다.
  • ⚠️ approve 경고: approve 함수는 레이스 컨디션 취약점이 있을 수 있어, 대안으로 increaseAllowance와 decreaseAllowance 사용이 권장됩니다.
  • 확장 기능:
    • ERC20Burnable: 토큰 소각 기능 추가.
    • ERC20Capped: 토큰 최대 발행량 제한 기능 추가.
    • ERC20Pausable: 토큰 전송을 일시 중지/재개하는 기능 추가.
    • ERC20Snapshot: 특정 시점의 잔액과 총 공급량을 기록(스냅샷)하는 기능 추가. (가중치 투표, 배당 등에 활용)

149. OpenZeppelin SafeERC20

ERC20 함수 호출을 더 안전하게 감싸주는(wrapper) 라이브러리입니다.

  • 일반적인 ERC20 함수는 실패 시 false를 반환하지만, SafeERC20은 실패 시 revert를 발생시켜 에러 처리를 더 명확하고 안전하게 만들어 줍니다.

150. OpenZeppelin TokenTimelock

토큰을 특정 기간 동안 잠그는(lock) 간단한 베스팅(vesting) 컨트랙트입니다.

  • 수혜자는 정해진 시간이 지난 후에야 컨트랙트로부터 토큰을 인출할 수 있습니다. (예: "자문위원은 1년 후에 토큰을 받을 수 있다.")

151. OpenZeppelin ERC721

대체 불가능 토큰(Non-Fungible Token, NFT)의 표준인 ERC721을 구현한 라이브러리입니다.

  • 주요 함수:
    • ownerOf(tokenId): 특정 토큰 ID의 소유자 주소를 반환합니다.
    • transferFrom(from, to, tokenId): 토큰을 전송합니다.
    • safeTransferFrom(from, to, tokenId): ✅ 사용이 권장되는 안전한 전송 함수입니다. 수신자가 컨트랙트일 경우, 해당 컨트랙트가 ERC721 토큰을 받을 수 있는지(onERC721Received 구현 여부)를 확인하여 토큰이 영원히 잠기는 것을 방지합니다.
    • approve(to, tokenId): 특정 토큰 ID에 대한 전송 권한을 다른 주소에 부여합니다.
    • setApprovalForAll(operator, approved): 내 소유의 모든 NFT에 대한 전송 권한을 특정 주소(operator)에게 부여하거나 철회합니다.
  • 주요 확장 기능:
    • ERC721Enumerable: 모든 토큰 ID 목록이나 특정 주소가 소유한 토큰 ID 목록을 조회할 수 있는 기능을 추가합니다.
    • ERC721URIStorage: 각 토큰의 URI를 온체인 스토리지에 직접 저장하고 관리하는 기능을 추가합니다.
    • ERC721Pausable: 토큰 전송, 민팅, 소각을 일시 중지하는 기능을 추가합니다.

152. OpenZeppelin ERC777

ERC20을 개선한 대체 가능한 토큰(Fungible Token) 표준으로, 훅(Hooks)이라는 강력한 기능을 도입했습니다.

  • 훅(Hook): 토큰을 받거나 보낼 때 특정 함수(tokensReceived, tokensToSend)가 자동으로 호출되는 기능입니다. 이를 통해 수신자는 토큰을 받는 즉시 특정 동작을 실행하거나, 원치 않는 토큰 수신을 거부(revert)할 수 있습니다.
  • 장점:
    • ERC20의 approve + transferFrom 2단계 과정을 단일 트랜잭션으로 처리할 수 있습니다.
    • 수신 컨트랙트가 훅을 구현해야만 토큰을 받을 수 있으므로, ERC20처럼 의도치 않게 토큰이 컨트랙트에 잠기는 사고를 방지합니다.
    • decimals는 항상 18로 고정되어 혼란을 줄입니다.
  • ERC1820이라는 범용 레지스트리 컨트랙트를 통해 훅 기능을 구현한 주소를 등록합니다.

153. OpenZeppelin ERC1155

멀티 토큰 표준(Multi-Token Standard)으로, 단일 컨트랙트에서 여러 종류의 토큰을 동시에 관리할 수 있게 설계되었습니다.

  • 핵심 특징: 하나의 컨트랙트 안에 여러 개의 토큰 ID를 두고, 각 주소는 ID별로 다른 잔액을 가집니다.
  • 대체 가능/불가능 토큰 동시 지원: 특정 ID의 토큰을 여러 개 발행하면 대체 가능 토큰(Fungible)이 되고, 단 하나만 발행하면 대체 불가능 토큰(NFT)이 됩니다.
  • 장점:
    • 토큰 종류마다 새 컨트랙트를 배포할 필요가 없어 가스비를 대폭 절약할 수 있습니다.
    • balanceOfBatch, safeBatchTransferFrom 등 여러 토큰을 한 번에 조회하고 전송하는 배치(batch) 기능을 지원하여 효율적입니다.

154. OpenZeppelin Ownable

가장 기본적인 단일 소유자 기반의 접근 제어 메커니즘을 제공합니다.

  • 컨트랙트를 배포한 주소가 기본 owner가 되며, transferOwnership 함수로 소유권을 이전할 수 있습니다.
  • onlyOwner 수정자를 함수에 추가하여 오직 owner만 해당 함수를 호출할 수 있도록 제한합니다.

155. OpenZeppelin AccessControl

Ownable보다 더 유연하고 일반적인 역할 기반 접근 제어(Role-Based Access Control, RBAC) 메커니즘을 제공합니다.

  • MINTER_ROLE, PAUSER_ROLE 등 여러 역할(Role)을 만들고, 각 역할에 여러 주소를 할당할 수 있습니다.
  • hasRole 함수나 수정자를 통해 특정 역할을 가진 주소만 함수를 호출하도록 제한합니다.
  • grantRole과 revokeRole 함수를 통해 동적으로 역할을 부여하거나 철회할 수 있습니다.
  • 복잡한 권한 관리가 필요할 때 Ownable 대신 사용됩니다.

156. OpenZeppelin Pausable

권한 있는 계정이 컨트랙트의 특정 기능을 일시적으로 중지(pause)하고 재개(unpause)할 수 있는 비상 정지 메커니즘입니다.

  • whenNotPaused 와 whenPaused 수정자를 함수에 적용하여, 컨트랙트가 중지 상태일 때 또는 아닐 때만 함수가 실행되도록 제어할 수 있습니다.

157. OpenZeppelin ReentrancyGuard

재진입 공격(Re-entrancy Attack)을 방지하는 보안 유틸리티입니다.

  • 재진입 공격은 하나의 함수 실행이 끝나기 전에 공격자가 악의적인 코드를 통해 해당 함수를 다시 호출하여 컨트랙트의 상태를 조작하는 공격입니다.
  • nonReentrant 수정자를 이더 전송 등 외부 호출이 포함된 함수에 적용하면, 한 번에 하나의 호출만 실행되도록 보장하여 재진입 공격을 원천적으로 차단합니다.

158. OpenZeppelin PullPayment

"Push" 방식 대신 "Pull-over-Push" 결제 전략을 구현합니다.

  • Push (위험): 보내는 컨트랙트가 받는 측의 함수를 직접 호출하여 이더를 전송하는 방식. (재진입 등 보안 문제 발생 가능)
  • Pull (안전): 보내는 컨트랙트는 지급할 금액만 기록해두고, 받는 측이 직접 withdraw와 같은 함수를 호출하여 돈을 "가져가도록(pull)" 하는 방식입니다. 보안상 이더 전송의 모범 사례로 간주됩니다.

159. OpenZeppelin Address

address 타입과 관련된 유용한 헬퍼 함수 모음입니다.

  • isContract(address account): 해당 주소가 컨트랙트인지 확인합니다. (단, false라고 해서 반드시 EOA인 것은 아님)
  • sendValue(recipient, amount): ✅ 솔리디티의 .transfer()를 대체하는 안전한 이더 전송 함수입니다. 2300 가스 제한 없이 사용 가능한 모든 가스를 전달하므로, 수신 컨트랙트가 복잡한 로직을 가져도 가스 부족으로 실패할 위험이 없습니다.
  • functionCall(...) 등: 로우레벨 call을 안전하게 사용할 수 있도록 감싸주는 래퍼(wrapper) 함수들입니다. 실패 시 revert 이유를 제대로 전파해 줍니다.

160. OpenZeppelin Arrays

배열 관련 유틸리티 함수 모음입니다.

  • findUpperBound(array, element): 정렬된 배열에서 특정 요소보다 크거나 같은 첫 번째 인덱스를 이진 탐색(O(log n))으로 효율적으로 찾아냅니다.

161. OpenZeppelin Context

msg.sendermsg.data에 직접 접근하는 대신, 현재 실행 컨텍스트에 대한 정보를 제공하는 추상 컨트랙트입니다.

  • 주요 사용처: 메타 트랜잭션(Meta-transactions)
  • 메타 트랜잭션에서는 사용자를 대신해 가스비를 내주는 중계자(relayer)가 트랜잭션을 실행하므로, msg.sender는 실제 사용자가 아닌 중계자의 주소가 됩니다.
  • Context는 _msgSender()와 같은 내부 함수를 제공하며, 이 함수를 오버라이드하여 메타 트랜잭션 상황에서도 실제 사용자의 주소를 올바르게 식별할 수 있도록 돕습니다.

162. OpenZeppelin Counters

오직 1씩만 증가하거나 감소할 수 있는 안전한 카운터를 제공하는 라이브러리입니다.

  • 일반 uint 변수를 카운터로 사용하면 오버플로우/언더플로우의 위험이 있지만, Counters를 사용하면 이를 방지할 수 있습니다.
  • 주요 사용처: ERC721 토큰 ID 발급, 매핑의 요소 개수 추적, 요청 ID 계산 등.
  • current()로 현재 값을 읽고, increment() / decrement()로 값을 변경합니다.

163. OpenZeppelin Create2

EVM의 CREATE2 옵코드를 더 쉽고 안전하게 사용할 수 있도록 돕는 라이브러리입니다.

  • CREATE2: 스마트 컨트랙트가 배포될 주소를 미리 계산할 수 있게 해주는 기능입니다.
  • 이를 통해 아직 배포되지 않은 컨트랙트와 상호작용하는 카운터팩추얼 인터랙션(counterfactual interactions)과 같은 고급 패턴을 구현할 수 있습니다.
  • computeAddress()로 주소를 미리 계산하고, deploy()로 해당 주소에 컨트랙트를 배포합니다.

164. OpenZeppelin Multicall

여러 개의 함수 호출을 하나의 트랜잭션으로 묶어서(batch) 실행할 수 있는 기능을 제공합니다.

  • multicall 함수에 실행할 함수 호출 데이터 배열을 전달하면, 해당 호출들을 순차적으로 실행합니다.
  • 사용자는 여러 번의 트랜잭션을 보낼 필요가 없어지고, 전체적인 가스비를 절약할 수 있어 DApp의 사용자 경험을 크게 향상시킵니다.

165. OpenZeppelin Strings

솔리디티에서 부족한 문자열 관련 연산을 지원하는 유틸리티 라이브러리입니다.

  • toString(uint256 value): 부호 없는 정수를 10진수 문자열로 변환합니다.
  • toHexString(uint256 value): 부호 없는 정수를 16진수 문자열로 변환합니다.

166. OpenZeppelin ECDSA

타원 곡선 디지털 서명(ECDSA)을 복구하고 관리하는 함수를 제공합니다.

  • 핵심 기능: ecrecover 옵코드의 서명 가변성(signature malleability) 취약점을 해결합니다. 서명값 s와 v를 특정 범위로 제한하여, 동일한 메시지에 대해 오직 하나의 유효한 서명만 인정되도록 보장합니다.
  • toEthSignedMessageHash: 일반적인 지갑(예: MetaMask)이 서명 시 추가하는 \x19Ethereum Signed Message:\n 접두사를 포함하여 해시를 계산해 주므로, 지갑 서명을 검증할 때 매우 유용합니다.

167. OpenZeppelin MerkleProof

머클 트리(Merkle Tree)의 증명(proof)을 검증하는 기능을 제공합니다.

  • 머클 트리를 사용하면, 방대한 데이터셋 전체를 온체인에 올리지 않고도 특정 데이터(leaf)가 그 데이터셋에 포함되어 있다는 것을 단 하나의 루트 해시(root)와 간결한 증명 데이터(proof)만으로 효율적으로 증명할 수 있습니다.
  • 주요 사용처: 대규모 에어드랍 대상자 검증, 화이트리스트 관리 등.

168. OpenZeppelin SignatureChecker

EOA(개인키) 서명과 ERC1271 기반의 컨트랙트 서명을 모두 검증할 수 있는 통합된 메커니즘을 제공합니다.

  • 문제점: 개인키가 없는 컨트랙트는 직접 서명을 생성할 수 없습니다.
  • 해결책 (ERC1271): 컨트랙트가 자신의 서명을 어떻게 검증할 것인지 표준화된 방법을 정의하는 표준입니다.
  • SignatureChecker를 사용하면 DApp이 일반 지갑뿐만 아니라 Argent, Gnosis Safe와 같은 스마트 컨트랙트 지갑과도 원활하게 호환될 수 있습니다.

169. OpenZeppelin EIP712

타입이 있는 구조화된 데이터의 서명을 위한 표준인 EIP-712 구현을 돕는 라이브러리입니다.

  • EIP-712의 목적: 사용자가 0x...와 같은 의미 없는 해시값에 서명하는 대신, 자신이 무엇에 서명하는지 명확하게 인지할 수 있도록 구조화된 데이터를 알아보기 쉽게 표시해 줍니다.
  • 이 라이브러리는 리플레이 공격을 방지하는 도메인 구분자(domain separator)와 최종 서명 해시를 생성하는 로직을 제공하여 EIP-712 구현을 간소화합니다.

170. OpenZeppelin Escrow

수취인이 자금을 인출할 때까지 안전하게 보관하는 간단한 에스크로(Escrow) 컨트랙트입니다.

  • 결제 대금을 즉시 수취인에게 보내는 대신, 에스크로 컨트랙트에 예치(deposit)합니다.
  • 수취인은 나중에 자신의 의지로 에스크로 컨트랙트에서 자금을 인출(withdraw)해 갑니다.
  • 이는 Pull-over-Push 패턴의 일종으로, 안전한 자금 전달 방식으로 사용됩니다.

 

171. OpenZeppelin ConditionalEscrow

Escrow를 상속받아, 특정 조건이 충족될 때만 자금 인출을 허용하는 에스크로 컨트랙트입니다.

  • withdrawalAllowed()라는 함수를 핵심으로 가지며, 이 함수를 자식 컨트랙트에서 직접 구현하여 인출 허용 조건을 정의해야 합니다.

172. OpenZeppelin RefundEscrow

ConditionalEscrow를 상속받아, 크라우드펀딩과 같은 시나리오에 특화된 에스크로 컨트랙트입니다.

  • 여러 참여자가 한 명의 수혜자를 위해 자금을 예치합니다.
  • 컨트랙트의 소유자는 모금 기간을 종료한 후, 목표 달성 시 수혜자에게 자금 인출을 허용하거나, 목표 미달 시 참여자들에게 자금을 환불할 수 있도록 설정할 수 있습니다.

173. OpenZeppelin ERC165

컨트랙트가 특정 인터페이스를 지원하는지 런타임에 확인할 수 있도록 하는 표준입니다.

  • supportsInterface(bytes4 interfaceId) 함수를 구현하여, 외부에서 해당 컨트랙트가 어떤 함수들의 집합(인터페이스)을 가지고 있는지 조회할 수 있게 해줍니다.
  • OpenZeppelin의 ERC165 유틸리티는 _registerInterface 함수를 통해 이 표준을 쉽게 구현할 수 있도록 돕습니다.

174. OpenZeppelin Math

솔리디티 언어 자체에는 없는 기본적인 수학 유틸리티 함수를 제공합니다.

  • max(a, b): 두 숫자 중 더 큰 값을 반환합니다.
  • min(a, b): 두 숫자 중 더 작은 값을 반환합니다.
  • average(a, b): 두 숫자의 평균값을 반환합니다. (결과는 0에 가까운 쪽으로 버림)

175. OpenZeppelin SafeMath

솔리디티 0.8.0 이전 버전에서 산술 연산의 오버플로우/언더플로우를 방지하기 위해 사용되던 필수 라이브러리입니다.

  • add, sub, mul, div, mod 등의 함수를 제공하며, 연산 결과가 오버플로우/언더플로우를 일으키면 트랜잭션을 revert 시킵니다.
  • 참고: 솔리디티 0.8.0 버전부터는 언어 자체에 checked 산술 연산이 기본으로 내장되어, SafeMath 라이브러리는 더 이상 필수가 아닙니다.

176. OpenZeppelin SignedSafeMath

SafeMath와 동일한 기능을 하지만, 부호 있는 정수(int) 타입을 위해 만들어진 라이브러리입니다.


177. OpenZeppelin SafeCast

정수 타입을 더 작은 타입으로 안전하게 다운캐스팅(downcasting)하는 함수들을 제공합니다.

  • 문제점: 솔리디티의 기본 다운캐스팅(예: uint128(myUint256))은 값이 너무 커서 오버플로우가 발생해도 에러 없이 값이 잘려나가 심각한 버그를 유발할 수 있습니다.
  • 해결책: SafeCast의 toUint128()와 같은 함수들은 다운캐스팅 시 오버플로우가 발생하면 트랜잭션을 revert 시켜 데이터의 무결성을 보장합니다.

178. OpenZeppelin EnumerableMap

솔리디티의 기본 mapping에 열거(enumeration) 기능을 추가한 자료구조입니다.

  • 문제점: 기본 mapping은 저장된 키-값 쌍을 순회할 수 없습니다.
  • 해결책: EnumerableMap은 set, remove, contains와 같은 일반적인 매핑 기능과 더불어, length()와 at(index) 함수를 제공하여 저장된 모든 항목을 반복문으로 순회할 수 있게 해줍니다.

179. OpenZeppelin EnumerableSet

집합(Set) 자료구조를 구현한 라이브러리로, 열거 기능을 지원합니다.

  • 집합의 특징: 중복된 요소를 허용하지 않습니다.
  • add, remove, contains 함수를 통해 집합에 요소를 추가, 제거, 포함 여부를 확인할 수 있습니다.
  • EnumerableMap과 마찬가지로, length()와 at(index) 함수를 통해 집합에 포함된 모든 요소를 순회할 수 있습니다.

180. OpenZeppelin BitMaps

연속된 uint256 키와 bool 값을 매핑할 때, 데이터를 컴팩트하고 효율적으로 관리하는 라이브러리입니다.

  • 핵심 원리: 여러 개의 bool 값(1비트)을 하나의 uint256 스토리지 슬롯에 비트 단위로 묶어서 저장합니다.
  • 주요 사용처: 수많은 토큰 ID 중 특정 ID가 에어드랍을 받았는지 여부를 추적하는 등, 대량의 boolean 플래그를 관리할 때 가스비를 크게 절약할 수 있습니다.

181. OpenZeppelin PaymentSplitter

여러 계정 그룹 간에 이더(Ether) 결제를 지분에 따라 자동으로 분배하는 컨트랙트입니다.

  • 각 계정에 '주식(share)'처럼 지분을 할당하면, 컨트랙트가 받은 이더를 해당 지분 비율에 따라 분배합니다.
  • 이 과정은 컨트랙트 내부에서 투명하게 처리되므로, 보내는 사람은 이더가 분배된다는 사실을 알 필요가 없습니다.
  • 풀 페이먼트(Pull Payment) 모델을 따르므로, 분배된 금액은 각 수령인이 직접 release 함수를 호출하여 인출해야 합니다.

182. OpenZeppelin TimelockController

컨트랙트의 주요 관리 기능에 시간 지연(timelock)을 강제하는 컨트롤러 컨트랙트입니다.

  • 목적: Ownable 컨트랙트의 소유자로 이 타임락 컨트랙트를 설정하면, 소유자만 실행할 수 있는 위험한 관리 작업(예: 컨트랙트 업그레이드)이 즉시 실행되지 않고, 설정된 최소 지연 시간(minDelay)이 지난 후에만 실행 가능하게 됩니다. 이는 사용자들에게 변경사항에 대응할 시간을 줍니다.
  • 역할 분리:
    • proposer: 관리 작업을 제안(schedule)하는 역할.
    • executor: 제안된 작업이 최소 지연 시간을 거친 후 실행(execute)하는 역할.
  • 주요 사용례: 다중서명 지갑이나 DAO가 proposer 역할을 맡고, 이 타임락 컨트랙트가 실제 운영 컨트랙트의 최종 소유자가 되는 거버넌스 구조.

183. OpenZeppelin ERC2771Context

ERC-2771 표준을 지원하는 Context의 변형입니다. ERC-2771은 메타 트랜잭션을 위한 표준 프로토콜입니다.

  • 메타 트랜잭션의 구성 요소:
    1. 서명자 (Transaction Signer): 실제 사용자. 트랜잭션에 서명하지만 가스는 내지 않습니다.
    2. 중계자 (Gas Relay): 서명을 대신 받아, 자신의 가스를 소모하여 트랜잭션을 실행하는 제3자.
    3. 신뢰할 수 있는 전달자 (Trusted Forwarder): 서명과 논스를 검증한 후 실제 수신 컨트랙트로 요청을 전달하는 중간 컨트랙트.
    4. 수신자 (Recipient): 메타 트랜잭션을 수신하는 최종 목적지 컨트랙트.
  • ERC2771Context는 Recipient 컨트랙트가 Trusted Forwarder를 통해 msg.sender가 아닌 실제 서명자를 식별할 수 있도록 돕습니다.

184. OpenZeppelin MinimalForwarder

위에서 설명한 신뢰할 수 있는 전달자(Trusted Forwarder) 역할을 하는, 간단하고 가스 효율적인 최소 기능의 구현체입니다.

  • 이 컨트랙트는 서명과 논스(nonce)를 검증한 후, 목적지 컨트랙트로 호출을 전달하는 역할만 수행합니다.

185. OpenZeppelin Proxy

프록시 패턴을 구현하는 추상 컨트랙트입니다.

  • 핵심 원리: 모든 함수 호출을 delegatecall을 통해 다른 "구현(implementation)" 컨트랙트로 위임하는 폴백 함수를 가집니다.
  • delegatecall의 특징 덕분에, 로직은 구현 컨트랙트에 있지만 모든 상태(데이터)는 프록시 컨트랙트의 스토리지에 저장됩니다. 이는 로직은 업그레이드하면서 데이터는 유지하는 업그레이드 가능 패턴의 기반이 됩니다.

186. OpenZeppelin ERC1967Proxy

업그레이드 가능한 프록시를 구현한 표준입니다.

  • 로직을 담고 있는 구현 컨트랙트의 주소를 변경하여 컨트랙트를 업그레이드할 수 있습니다.
  • 핵심: 구현 컨트랙트의 주소는 EIP-1967에서 정의한 특정 스토리지 슬롯에 저장됩니다. 이는 프록시의 관리 데이터와 구현 컨트랙트의 상태 변수들이 서로의 스토리지 공간을 침범하는 스토리지 충돌(storage collision)을 방지합니다.

187. OpenZeppelin TransparentUpgradeableProxy

투명한 프록시 패턴(Transparent Proxy Pattern)을 사용하여 "프록시 함수 충돌" 문제를 해결하는, 가장 널리 쓰이는 업그레이드 가능 프록시입니다.

  • 문제점: 만약 프록시의 관리자 함수와 구현 컨트랙트의 일반 함수 이름이 같다면?
  • 해결책 (투명 프록시 패턴):
    1. 일반 사용자가 프록시를 호출하면, 모든 호출은 항상 구현 컨트랙트로 전달됩니다.
    2. 관리자가 프록시를 호출하면, 모든 호출은 절대 구현 컨트랙트로 전달되지 않고 프록시 자체의 관리 기능만 실행할 수 있습니다.
  • 이 패턴 때문에 관리자 계정은 업그레이드와 같은 관리 작업에만 사용해야 하며, 일반적인 DApp 이용에는 다른 계정을 사용하는 것이 좋습니다.

188. OpenZeppelin ProxyAdmin

TransparentUpgradeableProxy의 관리자 역할을 하도록 설계된 보조 컨트랙트입니다.

  • 여러 개의 프록시 컨트랙트를 EOA나 다중서명 지갑이 직접 관리하는 대신, 이 ProxyAdmin 컨트랙트를 단일 관리자로 지정합니다.
  • 실제 소유자는 이 ProxyAdmin 컨트랙트 하나만 제어하면, 그 아래에 있는 모든 프록시의 업그레이드와 관리를 중앙에서 효율적으로 수행할 수 있습니다.

189. OpenZeppelin BeaconProxy

비콘(Beacon)을 사용하는 또 다른 업그레이드 패턴입니다.

  • 핵심 원리: 여러 프록시 컨트랙트가 각각의 구현 주소를 저장하는 대신, 모두 하나의 '비콘' 컨트랙트 주소를 가리킵니다.
  • 프록시는 호출이 있을 때마다 비콘에게 현재 구현 컨트랙트 주소가 어디인지 물어본 후, 그 주소로 delegatecall을 보냅니다.
  • 장점: 단 한 번의 트랜잭션으로 비콘의 구현 주소만 변경하면, 이 비콘을 가리키는 모든 프록시를 한꺼번에 업그레이드할 수 있어 매우 효율적입니다.

190. OpenZeppelin UpgradeableBeacon

BeaconProxy들이 가리키는 중앙 컨트롤러 역할을 하는 비콘 컨트랙트입니다.

  • 이 컨트랙트의 소유자는 upgradeTo 함수를 호출하여 비콘이 가리키는 구현 컨트랙트의 주소를 변경할 수 있으며, 이 변경 사항은 모든 관련 프록시에 즉시 반영됩니다.

191. OpenZeppelin Clones

EIP-1167 표준을 사용하여, 최소한의 비용으로 컨트랙트를 복제(clone)하는 기능을 제공합니다.

  • 핵심 원리: 모든 함수 호출을 단 하나의 마스터 "구현" 컨트랙트로 위임(delegatecall)하는, 최소한의 바이트코드를 가진 프록시를 배포합니다. 이는 기능은 동일하지만 상태는 독립적인 수많은 복제품을 매우 저렴한 가스비로 생성할 수 있게 해줍니다.
  • clone(implementation): create 옵코드를 사용하여 복제품을 배포합니다.
  • cloneDeterministic(implementation, salt): create2 옵코드를 사용하여, salt 값을 통해 예측 가능한 주소에 복제품을 배포합니다.

192. OpenZeppelin Initializable

업그레이드 가능한 컨트랙트의 초기화 로직 작성을 돕는 유틸리티입니다.

  • 문제점: 프록시 뒤에 배포되는 구현 컨트랙트는 constructor를 사용할 수 없습니다. 따라서 생성자 로직을 일반 함수(보통 initialize라는 이름의)로 옮겨야 합니다.
  • 해결책: Initializable 컨트랙트는 initializer라는 수정자를 제공합니다. 이 수정자는 initialize 함수가 생성자처럼 단 한 번만 호출되도록 보장합니다.
  • ⚠️ 주의: 일반 생성자와 달리, 컴파일러가 부모 컨트랙트의 initialize 함수 호출 여부를 자동으로 확인해주지 않으므로, 개발자가 상속 체인의 초기화 순서를 수동으로 신중하게 관리해야 합니다.

193. Dappsys DSProxy

Dappsys에서 제공하는 독특한 방식의 프록시 컨트랙트입니다.

  • 동작 방식: 사용자는 실행하고 싶은 컨트랙트의 바이트코드와 함수 호출 데이터(calldata)를 이 DSProxy에 전달합니다. 그러면 프록시는 즉석에서 해당 바이트코드로 임시 컨트랙트를 만든 후, 함수 호출 데이터를 delegatecall하여 실행합니다.

194. Dappsys DSMath

Dappsys에서 제공하는 안전한 산술 연산 라이브러리로, OpenZeppelin의 SafeMath와 유사하지만 고정 소수점 연산 기능이 추가된 것이 특징입니다.

  • wad: 18자리 소수 정밀도를 가지는 고정 소수점 숫자.
  • ray: 27자리 소수 정밀도를 가지는 고정 소수점 숫자.
  • DeFi 프로토콜에서 복잡한 이자율이나 비율을 정밀하게 계산하는 데 필수적으로 사용됩니다.

195. Dappsys DSAuth

Dappsys에서 제공하는 유연하고 업데이트 가능한 인증(auth) 패턴입니다.

  • auth 수정자는 함수 호출 시 isAuthorized 함수를 통해 msg.sender가 권한을 가졌는지 확인합니다.
  • 기본적으로 컨트랙트 소유자와 컨트랙트 자신에게 권한을 부여하며, 별도의 "Authority" 컨트랙트를 지정하여 권한 관리 로직을 분리하고 확장할 수 있습니다.

196. Dappsys DSGuard

DSAuth의 "Authority" 컨트랙트로 사용되도록 설계된, 접근 제어 목록(Access Control List, ACL) 기반의 권한 관리 컨트랙트입니다.

  • [호출자 주소][대상 컨트랙트 주소][함수 시그니처]를 키로, 허용 여부(boolean)를 값으로 매핑하여 매우 세분화된 함수 단위의 권한 제어가 가능합니다.

197. Dappsys DSRoles

DSAuth의 "Authority" 컨트랙트로 사용되도록 설계된 역할 기반(Role-Based) 권한 관리 컨트랙트입니다.

  • DSGuard보다 더 추상적인 방식으로, '루트 사용자', '공개 기능', '역할별 기능' 등 여러 계층의 역할을 정의하여 사용자의 접근 권한을 관리합니다.

198. WETH (Wrapped Ether)

WETH는 "포장된 이더"를 의미하며, 이더리움의 네이티브 통화인 ETH를 ERC-20 토큰 표준에 맞게 변환한 것입니다.

  • 필요성: 수많은 DeFi 프로토콜은 ERC-20 토큰과 상호작용하도록 설계되었기 때문에, ETH를 직접 사용할 수 없습니다.
  • 래핑(Wrapping): 사용자는 WETH 컨트랙트에 ETH를 보내고, 1:1 비율로 ERC-20 규격의 WETH 토큰을 받습니다.
  • 언래핑(Unwrapping): WETH 토큰을 WETH 컨트랙트에 다시 보내고, 1:1 비율로 원래의 ETH를 돌려받습니다.

199. Uniswap V2 (유니스왑 V2)

자동화된 마켓 메이커(Automated Market Maker, AMM) 탈중앙화 거래소(DEX)의 시초 격인 프로토콜입니다.

  • 핵심 알고리즘: x * y = k 라는 불변의 곱 공식을 사용합니다. (x와 y는 한 쌍의 토큰 수량)
  • 유동성 풀(Pools): 사용자들이 ERC-20 토큰 쌍을 예치하여 유동성 풀을 만들고, 거래 수수료를 보상으로 받습니다.
  • 스왑(Swaps): 사용자들은 이 유동성 풀을 상대로 토큰을 교환(swap)합니다.
  • 플래시 스왑(Flash Swaps): 담보 없이 유동성 풀의 모든 토큰을 빌린 후, 하나의 트랜잭션 안에서 빌린 토큰을 갚거나 그에 상응하는 다른 토큰으로 지불하는 혁신적인 기능입니다.
  • 오라클(Oracles): 조작에 강한 시간 가중 평균 가격(Time-Weighted Average Price, TWAP) 오라클을 제공하여, 다른 스마트 컨트랙트들이 신뢰할 수 있는 가격 정보를 얻을 수 있도록 합니다.

200. Uniswap V3 (유니스왑 V3)

Uniswap V2를 크게 개선한 버전입니다.

  • 집중화된 유동성(Concentrated Liquidity): 유동성 공급자가 특정 가격 범위에만 유동성을 집중하여 제공할 수 있게 하여, 자본 효율성을 극대화했습니다.
  • 다중 수수료 등급(Multiple Fee Tiers): 토큰 쌍의 변동성에 따라 다른 수수료 등급(0.05%, 0.3%, 1% 등)을 선택할 수 있게 하여, 유동성 공급자에게 더 적절한 보상을 제공합니다.
  • 개선된 오라클: 더 사용하기 쉽고 강력한 TWAP 오라클을 제공합니다.

201. Chainlink Oracles & Price Feeds (체인링크 오라클)

체인링크는 가장 널리 사용되는 탈중앙화 오라클 네트워크(Decentralized Oracle Network)입니다.

  • 오라클: 블록체인 외부의 데이터(예: 주식 가격, 날씨 정보 등)를 스마트 컨트랙트 안으로 가져오는 역할을 합니다.
  • 가격 피드(Price Feeds): 체인링크의 가장 대표적인 서비스로, 여러 데이터 제공자로부터 가격 데이터를 수집하고 이를 탈중앙화된 오라클 네트워크를 통해 집계하여, 신뢰할 수 있는 자산 가격 정보를 온체인에 제공합니다.

https://secureum.substack.com/p/solidity-201

 

Solidity 201

100 more key aspects of Solidity

secureum.substack.com

 

'Web3 > Solidity' 카테고리의 다른 글

[Secureum] Solidity 101  (8) 2025.07.11
[Solidity] Basic Syntax  (2) 2025.06.06