Solidity는 풍부한 표현식(Expression)과 제어 구조(Control Structure)를 제공하며, 이를 숙지하는 것은 효율적이고 읽기 쉬우며 견고한 스마트 계약을 작성하는 데 필수적입니다. 이 가이드는 Solidity의 모든 표현식과 제어 구조를 체계적으로 소개하고, 상세한 예제와 함께 모범 사례를 제시합니다.
1. 표현식의 기초
표현식은 Solidity 코드의 가장 기본적인 구성 요소로, 하나의 값으로 평가(계산)됩니다. 다양한 유형의 표현식과 기본 사용법을 먼저 살펴보겠습니다.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.17;
contract ExpressionBasics {
// 상태 변수 선언 시 표현식 사용
uint256 public constant SECONDS_PER_DAY = 24 * 60 * 60;
uint256 public initialValue = 100;
string public greeting = "Hello, Solidity!";
// 기본 표현식 예제
function basicExpressions() public pure returns (uint256, bool, int256) {
// 리터럴 표현식
uint256 a = 5; // 정수 리터럴
bool b = true; // 불리언 리터럴
string memory c = "test"; // 문자열 리터럴
// 변수 표현식 (변수 참조)
uint256 d = a;
// 단항 표현식
int256 e = -7; // 음수 부호 연산자
bool f = !true; // 논리 NOT
uint256 g = ~uint256(0); // 비트 단위 NOT
// 이항 표현식
uint256 h = 10 + 5; // 덧셈
bool i = (10 >= 5); // 비교
bool j = true && false; // 논리 AND
// 조건 표현식 (삼항 연산자)
uint256 k = (a > 3) ? 10 : 5;
// 타입 변환
int256 l = int256(a); // 명시적 타입 변환
return (a + h, b && !f, e + l);
}
// 부수 효과(Side Effect)를 가진 표현식
function expressionsWithSideEffects() public returns (uint256) {
uint256 a = 5;
// 상태를 변경하는 표현식들
a++; // 후위 증가 (a = 6)
++a; // 전위 증가 (a = 7)
a += 3; // 복합 할당 (a = 10)
a = a * 2; // 할당 (a = 20)
// 함수 호출은 부수 효과를 가질 수 있음
initialValue = updateValue(a); // 상태 변수 변경
return a;
}
// 부수 효과를 가진 내부 함수
function updateValue(uint256 _value) internal returns (uint256) {
initialValue = _value;
return _value * 2;
}
// 복잡한 표현식
function complexExpressions(uint256 a, uint256 b) public pure returns (uint256) {
// 여러 연산자가 조합된 복잡한 표현식
uint256 result = (a + b) * (a - b) / (a > b ? a : b);
return result;
}
// 표현식 계산 순서 (연산자 우선순위)
function evaluationOrder() public pure returns (uint256) {
uint256 a = 5;
uint256 b = 10;
uint256 c = 2;
// 연산자 우선순위와 결합법칙에 따라 계산
// 1. a * b = 50
// 2. 50 + c = 52
// 3. 52 / 2 = 26
return a * b + c / 2;
}
// 비트 연산 표현식
function bitwiseExpressions(uint256 a, uint256 b) public pure returns (uint256, uint256, uint256, uint256) {
uint256 bitwiseAnd = a & b;
uint256 bitwiseOr = a | b;
uint256 bitwiseXor = a ^ b;
uint256 leftShift = a << 2; // 2비트 왼쪽 이동 (4배)
return (bitwiseAnd, bitwiseOr, bitwiseXor, leftShift);
}
// 문자열 표현식
function stringExpressions() public pure returns (string memory) {
string memory a = "Hello, ";
string memory b = "Solidity!";
return string(abi.encodePacked(a, b));
}
// 표현식의 타입 추론
function typeInference() public pure returns (uint8, uint256) {
var1 = 100; // uint8 - 256보다 작으므로
var2 = 1000000; // uint256 - uint16 최대값보다 크므로
var3 = var1 + 200; // uint16 - 결과가 uint8에 맞지 않음
return (var1, var2);
}
uint8 var1;
uint256 var2;
uint16 var3;
// 리터럴의 특별한 표기법
function literalNotations() public pure returns (uint256, uint256, uint256, uint256) {
uint256 decimal = 123;
uint256 hex = 0x7B; // 16진수 (123)
uint256 scientific = 1.23e2; // 과학적 표기법 (123)
uint256 withUnderscores = 1_000_000; // 가독성을 위한 밑줄
return (decimal, hex, scientific, withUnderscores);
}
}
핵심 사항
- 표현식 유형: 리터럴(숫자, 문자열, 불리언 등), 변수 참조, 연산자(단항, 이항, 삼항), 함수 호출, 타입 변환 등이 있습니다.
- 부수 효과: 일부 표현식(할당, 증감 등)은 상태를 변경합니다. 순수 표현식은 값만 계산합니다.
- 계산 순서: 연산자 우선순위와 결합법칙을 따르며, 괄호를 사용하여 순서를 명시할 수 있습니다.
- 타입 추론: Solidity는 컨텍스트에 따라 표현식의 타입을 추론합니다. 암시적 타입 변환과 정밀도 손실 가능성에 주의해야 합니다.
2. 산술 및 논리 표현식
산술 및 논리 표현식은 스마트 계약에서 가장 많이 사용되는 표현식 유형으로, 수학적 계산과 논리적 판단에 사용됩니다.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.17;
contract ArithmeticAndLogicalExpressions {
// 기본 산술 연산
function basicArithmetic(uint256 a, uint256 b) public pure returns (
uint256 addition, uint256 subtraction, uint256 multiplication,
uint256 division, uint256 modulo
) {
addition = a + b;
subtraction = a - b;
multiplication = a * b;
require(b != 0, "0으로 나눌 수 없음");
division = a / b; // 정수 나눗셈 (소수점 이하 버림)
modulo = a % b; // 나머지
return (addition, subtraction, multiplication, division, modulo);
}
// 고급 산술 연산
function advancedArithmetic(uint256 a, uint256 b, uint256 modulus) public pure returns (
uint256 exponentiation, uint256 addmod_result, uint256 mulmod_result
) {
exponentiation = a ** b;
require(modulus != 0, "모듈러스는 0이 될 수 없음");
// 중간 오버플로우를 방지하는 모듈러 연산
addmod_result = addmod(a, b, modulus); // (a + b) % modulus
mulmod_result = mulmod(a, b, modulus); // (a * b) % modulus
return (exponentiation, addmod_result, mulmod_result);
}
// 오버플로우 검사가 포함된 산술 (Solidity 0.8.0+ 자동 수행)
function arithmeticWithOverflowCheck(uint8 a, uint8 b) public pure returns (
uint8, uint8, uint8, bool
) {
uint8 sum = a + b; // 결과가 255보다 크면 트랜잭션 롤백
// unchecked 블록을 사용하여 오버플로우 검사 회피 (주의해서 사용)
uint8 unsafeSum;
unchecked { unsafeSum = a + b; } // (a + b) % 256 으로 오버플로우 가능
bool willOverflow = (255 - a) < b;
uint8 diff;
if (a >= b) { diff = a - b; }
else { diff = 0; } // 언더플로우 방지
return (sum, unsafeSum, diff, willOverflow);
}
// 복합 할당 연산자
function compoundAssignmentOperators() public pure returns (uint256) {
uint256 x = 10;
x += 5; x -= 3; x *= 2; x /= 4; x %= 3; x **= 2;
x |= 0x0F; x &= 0xF0; x ^= 0xFF; x <<= 2; x >>= 1;
return x;
}
// 증감 연산자 (전위/후위)
function incrementDecrementOperators() public pure returns (
uint256, uint256, uint256, uint256
) {
uint256 a = 5, b = 5, c = 5, d = 5;
uint256 preInc = ++a; // a=6, preInc=6
uint256 postInc = b++; // b=6, postInc=5
uint256 preDec = --c; // c=4, preDec=4
uint256 postDec = d--; // d=4, postDec=5
return (preInc, postInc, preDec, postDec);
}
// 비교 연산자
function comparisonOperators(uint256 a, uint256 b) public pure returns (
bool isEqual, bool isNotEqual, bool isGreater, bool isLesser,
bool isGreaterOrEqual, bool isLesserOrEqual
) {
isEqual = (a == b); isNotEqual = (a != b);
isGreater = (a > b); isLesser = (a < b);
isGreaterOrEqual = (a >= b); isLesserOrEqual = (a <= b);
return (isEqual, isNotEqual, isGreater, isLesser, isGreaterOrEqual, isLesserOrEqual);
}
// 논리 연산자
function logicalOperators(bool a, bool b) public pure returns (
bool logicalAnd, bool logicalOr, bool logicalNot, bool exclusive
) {
logicalAnd = a && b;
logicalOr = a || b;
logicalNot = !a;
exclusive = a != b; // 논리 XOR
return (logicalAnd, logicalOr, logicalNot, exclusive);
}
// 비트 연산자
function bitwiseOperators(uint256 a, uint256 b) public pure returns (
uint256 bitAnd, uint256 bitOr, uint256 bitXor,
uint256 bitNot, uint256 leftShift, uint256 rightShift
) {
bitAnd = a & b; bitOr = a | b; bitXor = a ^ b;
bitNot = ~a;
leftShift = a << 3; // 2^3 곱하기
rightShift = a >> 2; // 2^2 나누기 (내림)
return (bitAnd, bitOr, bitXor, bitNot, leftShift, rightShift);
}
// 논리 연산자의 단락 평가(Short-circuit Evaluation)
function shortCircuitEvaluation(uint256 a, uint256 b) public pure returns (bool) {
// a >= 100 이면, b / a > 2 는 평가되지 않음
bool result = (a < 100) && (b / a > 2);
// a < 50 이면, b / (a - 50) > 0 는 평가되지 않음
bool anotherResult = (a >= 50) || (b / (a - 50) > 0);
return result && anotherResult;
}
// 연산자 우선순위 예제
function operatorPrecedence() public pure returns (uint256, uint256) {
uint256 result1 = 2 + 3 * 4; // 3*4=12, 2+12=14
uint256 result2 = (2 + 3) * 4; // 2+3=5, 5*4=20
return (result1, result2);
}
// 고정 소수점 연산 시뮬레이션 (정수와 정밀도 인자 사용)
function fixedPointArithmetic(uint256 a, uint256 b) public pure returns (
uint256 fpAdd, uint256 fpSub, uint256 fpMul, uint256 fpDiv
) {
uint256 precision = 10**18; // 18자리 정밀도 (ETH wei와 유사)
fpAdd = a + b;
fpSub = a - b;
fpMul = (a * b) / precision;
fpDiv = (a * precision) / b;
return (fpAdd, fpSub, fpMul, fpDiv);
}
}
핵심 사항
- 산술 연산자: 기본 연산자(+, -, *, /, %, **)와 특수 함수(addmod, mulmod)가 있습니다. Solidity 0.8.0+ 부터는 기본적으로 오버플로우/언더플로우를 검사합니다.
- 복합 할당: 산술(+=, -= 등) 및 비트 연산(&=, |= 등)에 대한 복합 할당자를 제공합니다.
- 증감 연산자: 전위(++a)는 값을 먼저 변경하고 반환하며, 후위(a++)는 원래 값을 반환한 후 변경합니다.
- 비교 연산자: ==, !=, <, >, <=, >=를 지원합니다.
- 논리 연산자: &&(AND), ||(OR), !(NOT)을 지원하며, 단락 평가(Short-circuit Evaluation)를 통해 가스를 절약할 수 있습니다.
- 비트 연산자: &(AND), |(OR), ^(XOR), ~(NOT), <<(왼쪽 시프트), >>(오른쪽 시프트)를 지원합니다.
- 주의사항: 정수 나눗셈은 소수점 이하를 버립니다. 0으로 나누면 트랜잭션이 롤백됩니다. unchecked 블록을 사용하여 오버플로우 검사를 우회할 수 있지만, 신중하게 사용해야 합니다.
3. 조건 표현식 (삼항 연산자)
조건 표현식(삼항 연산자)은 조건에 따라 다른 값을 선택할 수 있게 해주며, 간단한 if-else 문을 대체하는 간결한 방법입니다.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.17;
contract ConditionalExpressions {
// 기본 조건 표현식
function basicConditional(uint256 a, uint256 b) public pure returns (uint256) {
uint256 max = (a > b) ? a : b; // 더 큰 값 선택
return max;
}
// 중첩 조건 표현식
function nestedConditional(uint256 value) public pure returns (string memory) {
string memory description =
(value < 10) ? "Small" :
(value < 100) ? "Medium" :
(value < 1000) ? "Large" : "Huge";
return description;
}
// 조건 표현식 vs if-else
function conditionalVsIfElse(uint256 value) public pure returns (uint256) {
uint256 result1 = (value % 2 == 0) ? value / 2 : value * 3 + 1;
uint256 result2;
if (value % 2 == 0) { result2 = value / 2; }
else { result2 = value * 3 + 1; }
assert(result1 == result2);
return result1;
}
// 안전한 연산을 위한 조건 표현식
function safeOperation(uint256 a, uint256 b) public pure returns (uint256, bool) {
bool isSafe = (b != 0);
uint256 result = isSafe ? (a / b) : 0;
return (result, isSafe);
}
// 기본값 처리
function handleDefaultValue(uint256 value, uint256 defaultValue) public pure returns (uint256) {
return (value == 0) ? defaultValue : value;
}
// 연산 선택
function selectOperation(uint256 a, uint256 b, bool isAddition) public pure returns (uint256) {
return isAddition ? (a + b) : (a * b);
}
// 다중 조건 표현식
function multiConditionExpression(uint256 value, bool isActive, bool isPremium) public pure returns (uint256) {
return (isActive && (isPremium || value > 100)) ? value * 2 : value / 2;
}
// 조건 표현식과 타입 변환
function conditionalWithCast(int256 value) public pure returns (uint256) {
return value >= 0 ? uint256(value) : uint256(-value);
}
// 주소 선택
function conditionalAddress(bool useFirst, address first, address second) public pure returns (address) {
return useFirst ? first : second;
}
}
핵심 사항
- 기본 문법:
조건 ? 참_표현식 : 거짓_표현식형태입니다. 조건이 참이면 첫 번째 표현식, 거짓이면 두 번째 표현식을 반환합니다. - 중첩 사용:
조건1 ? 값1 : (조건2 ? 값2 : 값3)과 같이 중첩하여 사용할 수 있습니다. 가독성을 위해 괄호를 사용하는 것이 좋습니다. - 장점: if-else 보다 간결하며, 다른 표현식 내에 직접 포함시킬 수 있습니다.
- 제한사항: 복잡한 로직에는 적합하지 않으며, 여러 문장을 실행할 수 없고, 두 분기는 반환 타입이 호환되어야 합니다.
4. if 문
if 문은 조건에 따라 코드 블록을 실행하는 가장 기본적인 제어 구조입니다.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.17;
contract IfStatements {
// 기본 if
function basicIf(uint256 value) public pure returns (string memory) {
string memory result;
if (value > 100) { result = "100보다 큼"; }
return result;
}
// if-else
function ifElse(uint256 value) public pure returns (string memory) {
string memory result;
if (value > 100) { result = "100보다 큼"; }
else { result = "100보다 작거나 같음"; }
return result;
}
// if-else if-else 체인
function ifElseIfChain(uint256 value) public pure returns (string memory) {
string memory result;
if (value < 10) { result = "한 자리 수"; }
else if (value < 100) { result = "두 자리 수"; }
else if (value < 1000) { result = "세 자리 수"; }
else { result = "네 자리 이상"; }
return result;
}
// 중첩 if
function nestedIf(uint256 value, bool isActive) public pure returns (string memory) {
string memory result;
if (isActive) {
if (value < 100) { result = "활성 및 작음"; }
else { result = "활성 및 큼"; }
} else {
if (value < 100) { result = "비활성 및 작음"; }
else { result = "비활성 및 큼"; }
}
return result;
}
// 복합 조건
function compoundConditions(uint256 value, bool flag1, bool flag2) public pure returns (string memory) {
string memory result;
if (value > 100 && flag1) { result = "조건 1 충족"; }
else if (value > 50 || flag2) { result = "조건 2 충족"; }
else if (!(value > 20)) { result = "조건 3 충족"; }
else { result = "충족된 조건 없음"; }
return result;
}
// 입력 검증
function validateInput(uint256 value) public pure returns (bool, string memory) {
if (value == 0) { return (false, "값은 0이 될 수 없음"); }
if (value > 1000) { return (false, "값이 너무 큼"); }
return (true, "입력 유효");
}
// if 조건에서의 단락 평가
function shortCircuit(uint256 a, uint256 b) public pure returns (string memory) {
// a가 0이면 두 번째 조건은 검사되지 않아 0으로 나누기 오류 방지
if (a == 0 || (b / a) > 10) { return "조건 충족"; }
else { return "조건 불충족"; }
}
// if 문과 블록 스코프
function blockScope(uint256 value) public pure returns (uint256) {
uint256 result = value;
if (value > 100) {
uint256 bonus = value / 10; // 이 변수는 if 블록 내에서만 사용 가능
result += bonus;
}
// result += bonus; // 오류: bonus 변수에 접근 불가
return result;
}
// 가스 최적화 검증
function gasOptimizedValidation(uint256 value) public pure returns (bool) {
if (value == 0) { return false; } // 가장 실패할 가능성이 높은 조건 먼저 검사
if (value > 1000) { return false; }
if (value % 7 == 0 && value % 11 == 0) { return false; }
return true;
}
}
핵심 사항
- 기본 문법:
if (조건) { ... } else { ... }형태입니다. 'else if'를 사용하여 여러 조건을 연결할 수 있습니다. - 조건 평가: 조건은 반드시 불리언 값이어야 하며, &&, ||, ! 와 같은 논리 연산자를 사용하여 복합 조건을 만들 수 있습니다.
- 중첩과 스코프: if 문은 중첩될 수 있으며, 각 코드 블록은 자체적인 변수 스코프를 생성합니다.
- 모범 사례: 코드 블록을 명확히 구분하기 위해 중괄호를 사용하고, 가독성을 위해 적절한 들여쓰기를 유지하며, 가스 절약을 위해 단락될 가능성이 높은 조건을 앞에 배치합니다.
5. for 루프
for 루프는 가장 일반적으로 사용되는 반복 구조로, 반복 횟수가 알려진 경우에 특히 적합합니다.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.17;
contract ForLoops {
// 기본 for 루프
function basicForLoop() public pure returns (uint256) {
uint256 sum = 0;
for (uint256 i = 0; i < 10; i++) { sum += i; }
return sum; // 0+1+...+9 = 45
}
// 배열 순회
function iterateArray(uint256[] memory arr) public pure returns (uint256) {
uint256 sum = 0;
for (uint256 i = 0; i < arr.length; i++) { sum += arr[i]; }
return sum;
}
// 역방향 순회
function reverseIteration() public pure returns (uint256[] memory) {
uint256[] memory result = new uint256[](10);
for (uint256 i = 9; i >= 0; i--) {
result[9 - i] = i;
if (i == 0) break; // i가 0일 때 i--는 언더플로우를 일으키므로 break
}
return result;
}
// 사용자 정의 스텝
function customStepIteration() public pure returns (uint256[] memory) {
uint256[] memory result = new uint256[](5);
uint256 index = 0;
for (uint256 i = 0; i < 10; i += 2) { result[index] = i; index++; }
return result; // [0, 2, 4, 6, 8]
}
// 무한 루프와 수동 종료
function controlledIteration(uint256 maxIterations) public pure returns (uint256) {
uint256 result = 0;
uint256 i = 0;
for (;;) {
result += i;
i++;
if (i >= maxIterations || result > 1000) { break; }
}
return result;
}
// 중첩 for 루프
function nestedForLoops(uint256 n) public pure returns (uint256) {
uint256 sum = 0;
for (uint256 i = 0; i < n; i++) {
for (uint256 j = 0; j < n; j++) { sum += i * j; }
}
return sum;
}
// 가스 최적화 for 루프
function gasOptimizedForLoop(uint256[] memory arr) public pure returns (uint256) {
uint256 sum = 0;
uint256 length = arr.length; // 배열 길이 캐싱
for (uint256 i = 0; i < length;) {
sum += arr[i];
unchecked { i++; } // 오버플로우 검사 회피
}
return sum;
}
// 대규모 배열 처리를 위한 배치 반복
function batchedIteration(uint256[] memory arr, uint256 batchSize) public pure returns (uint256[] memory) {
uint256 length = arr.length;
uint256 batches = (length + batchSize - 1) / batchSize; // 올림 나눗셈
uint256[] memory batchSums = new uint256[](batches);
for (uint256 batchIndex = 0; batchIndex < batches; batchIndex++) {
uint256 startIdx = batchIndex * batchSize;
uint256 endIdx = (startIdx + batchSize) > length ? length : (startIdx + batchSize);
uint256 batchSum = 0;
for (uint256 i = startIdx; i < endIdx; i++) { batchSum += arr[i]; }
batchSums[batchIndex] = batchSum;
}
return batchSums;
}
}
핵심 사항
- 기본 문법:
for (초기화; 조건; 업데이트) { ... }형태입니다. 각 구성 요소는 선택 사항입니다. - 구성 요소의 유연성: 모든 구성 요소를 생략할 수 있으며(
for (;;)), 초기화 및 업데이트 부분에서 쉼표로 구분하여 여러 표현식을 사용할 수 있습니다. - 최적화 팁: 배열 길이를 캐싱하고, 카운터 증가에
unchecked블록을 사용하며, 불변 계산을 루프 밖으로 이동하는 것이 좋습니다. - 주의사항: 무한 루프를 방지하고, 인덱스 경계(특히 역방향 순회 시)에 주의하며, 중첩 루프는 계산 복잡도를 급격히 증가시킵니다.
6. while 및 do-while 루프
while과 do-while 루프는 코드를 반복 실행하는 또 다른 방법을 제공하며, 반복 횟수를 미리 알 수 없는 경우에 특히 유용합니다.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.17;
contract WhileLoops {
// 기본 while 루프
function basicWhileLoop() public pure returns (uint256) {
uint256 sum = 0;
uint256 i = 0;
while (i < 10) { sum += i; i++; }
return sum; // 45
}
// 기본 do-while 루프
function basicDoWhileLoop() public pure returns (uint256) {
uint256 sum = 0;
uint256 i = 0;
do { sum += i; i++; } while (i < 10);
return sum; // 45
}
// while과 do-while 비교
function compareWhileAndDoWhile(uint256 limit) public pure returns (uint256, uint256) {
uint256 whileSum = 0;
uint256 doWhileSum = 0;
uint256 i = 10;
while (i < limit) { whileSum += i; i++; } // 조건이 거짓이면 실행되지 않음
i = 10;
do { doWhileSum += i; i++; } while (i < limit); // 최소 한 번은 실행됨
return (whileSum, doWhileSum);
// limit <= 10 이면, whileSum은 0, doWhileSum은 10
}
// 배열에서 대상 검색 (길이를 모르는 경우)
function findInArray(uint256[] memory arr, uint256 target) public pure returns (bool, uint256) {
uint256 i = 0;
while (i < arr.length) {
if (arr[i] == target) { return (true, i); }
i++;
}
return (false, 0);
}
// while 루프를 사용한 정수 제곱근 계산 (뉴턴 방법)
function integerSquareRoot(uint256 x) public pure returns (uint256) {
if (x == 0) return 0;
uint256 r = x;
while (r > x / r) { r = (r + x / r) / 2; }
return r;
}
// while 루프를 사용한 체인 길이 계산 (3n+1 문제 예시)
function countChainLength(uint256 start) public pure returns (uint256) {
uint256 count = 1;
uint256 current = start;
while (current > 1) {
if (current % 2 == 0) { current = current / 2; }
else { current = 3 * current + 1; }
count++;
}
return count;
}
// while 루프를 사용한 이진 탐색
function binarySearch(uint256[] memory arr, uint256 target) public pure returns (bool, uint256) {
if (arr.length == 0) return (false, 0);
uint256 left = 0;
uint256 right = arr.length - 1;
while (left <= right) {
uint256 mid = left + (right - left) / 2;
if (arr[mid] == target) { return (true, mid); }
if (arr[mid] < target) { left = mid + 1; }
else {
if (mid == 0) break;
right = mid - 1;
}
}
return (false, 0);
}
// while 루프를 사용한 피보나치 수열
function fibonacciWithWhile(uint256 n) public pure returns (uint256) {
if (n <= 1) return n;
uint256 fib1 = 0, fib2 = 1, fibN = 0, i = 2;
while (i <= n) {
fibN = fib1 + fib2;
fib1 = fib2;
fib2 = fibN;
i++;
}
return fibN;
}
}
핵심 사항
- while 루프:
while (조건) { ... }형태로, 조건이 거짓이면 루프 본문이 한 번도 실행되지 않을 수 있습니다. - do-while 루프:
do { ... } while (조건);형태로, 루프 본문이 최소 한 번 실행됩니다. - 선택 기준: 루프 실행이 필요하지 않을 수 있는 경우 while을, 최소 한 번은 실행해야 하는 경우 do-while을 사용합니다.
- 일반적인 용도: 알 수 없는 길이의 데이터 처리, 조건이 충족될 때까지 반복해야 하는 알고리즘, 재귀 알고리즘 시뮬레이션 등에 사용됩니다.
7. continue 및 break 문
continue와 break 문은 루프의 실행 흐름을 제어하여, 루프 내에서 조건을 더 유연하게 처리할 수 있게 해줍니다.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.17;
contract ContinueAndBreak {
// continue를 사용하여 특정 반복 건너뛰기
function skipEvenNumbers() public pure returns (uint256) {
uint256 sum = 0;
for (uint256 i = 0; i < 10; i++) {
if (i % 2 == 0) { continue; } // 짝수는 건너뜀
sum += i;
}
return sum; // 1+3+5+7+9 = 25
}
// break를 사용하여 루프 조기 종료
function findFirstDivisor(uint256 n, uint256 start) public pure returns (uint256) {
for (uint256 i = start; i <= n; i++) {
if (n % i == 0) { return i; } // 찾으면 즉시 함수 종료
}
return n;
}
// 중첩 루프에서 break 사용
function breakInNestedLoops() public pure returns (uint256, uint256) {
uint256 i; uint256 j;
for (i = 0; i < 5; i++) {
for (j = 0; j < 5; j++) {
if (i + j >= 5) { break; } // 내부 루프만 종료
}
if (i == 3) { break; } // 외부 루프 종료
}
return (i, j);
}
// continue와 break 조합
function combineBreakAndContinue(uint256[] memory arr) public pure returns (uint256) {
uint256 sum = 0;
for (uint256 i = 0; i < arr.length; i++) {
if (arr[i] > 100) { break; } // 100 초과값 만나면 중단
if (arr[i] == 0) { continue; } // 0은 건너뜀
sum += arr[i];
}
return sum;
}
// continue와 break를 사용한 필터링
function filterAndSum(uint256[] memory arr) public pure returns (uint256, uint256) {
uint256 sum = 0; uint256 count = 0;
for (uint256 i = 0; i < arr.length; i++) {
if (arr[i] < 10 || arr[i] > 50) { continue; } // 범위 밖은 건너뜀
sum += arr[i];
count++;
if (count >= 10) { break; } // 10개 찾으면 중단
}
return (sum, count);
}
// 가스 최적화된 검색 (빠른 반환)
function findWithGasOptimization(uint256[] memory arr, uint256 target) public pure returns (bool, uint256) {
uint256 length = arr.length;
for (uint256 i = 0; i < length;) {
if (arr[i] == target) { return (true, i); }
unchecked { i++; }
}
return (false, 0);
}
}
핵심 사항
- continue: 현재 반복의 나머지 부분을 건너뛰고 다음 반복으로 넘어갑니다. 특정 조건을 건너뛰어야 할 때 유용합니다.
- break: 루프 전체를 즉시 종료합니다. 목표 값을 찾은 후 더 이상 검색할 필요가 없을 때 유용합니다.
- 중첩 루프 주의: break는 가장 안쪽 루프만 종료시킵니다.
- 가스 최적화: 결과를 찾은 후 break나 return으로 루프를 조기 종료하면 가스를 절약할 수 있습니다.
8. return 문
return 문은 함수에서 값을 반환하고 함수의 실행을 종료합니다.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.17;
contract ReturnStatements {
// 기본 return
function basicReturn(uint256 value) public pure returns (uint256) {
return value;
}
// 여러 값 반환 (명명된 반환 변수 사용)
function multipleReturns(uint256 a, uint256 b) public pure returns (
uint256 sum, uint256 product, bool isEqual
) {
sum = a + b;
product = a * b;
isEqual = (a == b);
return; // 선택 사항, sum, product, isEqual 을 자동으로 반환
}
// 명시적 다중 값 반환
function explicitMultipleReturns(uint256 a, uint256 b) public pure returns (uint256, uint256, bool) {
uint256 sum = a + b;
uint256 product = a * b;
bool isEqual = (a == b);
return (sum, product, isEqual);
}
// 조기 반환 (특별한 경우 처리)
function earlyReturn(uint256 a, uint256 b) public pure returns (uint256, string memory) {
if (b == 0) { return (0, "0으로 나눌 수 없음"); }
return (a / b, "나눗셈 성공");
}
// 루프 내에서 조기 반환
function findInArray(uint256[] memory arr, uint256 target) public pure returns (bool, uint256) {
for (uint256 i = 0; i < arr.length; i++) {
if (arr[i] == target) { return (true, i); }
}
return (false, 0);
}
// 명명된 반환 변수를 사용한 고급 패턴
function complexNamedReturns(uint256 a, uint256 b) public pure returns (
uint256 result, bool isValid, string memory description
) {
result = 0; isValid = false; description = "잘못된 입력";
if (a == 0 || b == 0) { return; } // 설정된 변수로 조기 반환
result = a * b;
isValid = true;
if (result < 100) { description = "작은 결과"; }
else if (result < 1000) { description = "중간 결과"; }
else { description = "큰 결과"; }
return;
}
// 반환 값 구조 분해 (Destructuring)
function getPersonInfo() public pure returns (string memory, uint256, bool) {
return ("Alice", 30, true);
}
function useDestructuring() public pure returns (uint256) {
(string memory name, uint256 age, bool active) = getPersonInfo();
return active ? age : 0;
}
// 가스 인식 반환
function gasAwareReturn(uint256[] memory values) public view returns (uint256[] memory, bool) {
uint256 startGas = gasleft();
uint256[] memory processed = new uint256[](values.length);
for (uint256 i = 0; i < values.length; i++) {
processed[i] = values[i] * values[i];
if (gasleft() < startGas / 2) {
uint256[] memory partial = new uint256[](i + 1);
for (uint256 j = 0; j <= i; j++) { partial[j] = processed[j]; }
return (partial, false); // 가스 부족 시 부분 결과 반환
}
}
return (processed, true);
}
}
핵심 사항
- 기본 사용법:
return 표현식;형태로 값을 반환하고 함수를 종료합니다. 반환 타입이 없는 함수는return;을 사용할 수 있습니다. - 다중 값 반환:
return (값1, 값2, ...);형태로 여러 값을 반환합니다. 명명된 반환 변수를 사용하면 암시적으로 반환할 수 있습니다. - 조기 반환: 오류 조건이나 특별한 경우를 처리할 때 유용합니다.
- 명명된 반환 변수: 함수 선언 시 미리 정의하여 가독성을 높일 수 있습니다.
- 주의사항: 모든 코드 경로에 적절한 return 문이 있는지 확인해야 합니다. return은 함수 실행을 즉시 중단시킵니다.
9. 예외 제어: require, assert, revert
Solidity는 require, assert, revert의 세 가지 주요 예외 제어 메커니즘을 제공하며, 오류 조건을 처리하고 계약의 안전한 작동을 보장하는 데 사용됩니다.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.17;
contract ExceptionControl {
uint256 public value;
address public owner;
bool public locked;
// 사용자 정의 오류 (가스 효율적)
error InsufficientValue(uint256 available, uint256 required);
error Unauthorized(address caller);
error ContractLocked();
constructor() { owner = msg.sender; }
// require를 사용한 입력 검증
function deposit(uint256 amount) public payable {
require(msg.value == amount, "보낸 금액이 지정된 금액과 다름");
require(!locked, "계약이 잠김");
value += amount;
}
// assert를 사용한 불변 조건 검증
function withdraw(uint256 amount) public {
require(amount > 0, "금액은 0보다 커야 함");
require(amount <= value, "잔액 부족");
require(msg.sender == owner, "소유자만 출금 가능");
require(!locked, "계약이 잠김");
locked = true; // 재진입 방지
value -= amount;
(bool success, ) = msg.sender.call{value: amount}("");
require(success, "전송 실패");
assert(address(this).balance >= value); // 불변 조건 확인
locked = false;
}
// revert와 사용자 정의 오류 사용
function safeWithdraw(uint256 amount) public {
if (amount > value) { revert InsufficientValue(value, amount); }
if (msg.sender != owner) { revert Unauthorized(msg.sender); }
if (locked) { revert ContractLocked(); }
// 출금 로직...
}
// 예외 처리의 가스 비용 고려
function gasConsiderations(uint256 amount) public {
// 가장 가스 효율적: 사용자 정의 오류
if (amount == 0) { revert InsufficientValue(0, 1); }
// 중간 가스 소모: 메시지 없는 revert
if (amount > value) { revert(); }
// 높은 가스 소모: 메시지가 있는 require
require(msg.sender == owner, "소유자만 가능");
}
// receive 함수
receive() external payable { value += msg.value; }
}
핵심 사항
- require: 입력 검증, 조건 확인, 상태 검증에 사용됩니다. 조건이 거짓이면 트랜잭션을 롤백하고 오류 메시지를 반환합니다.
- assert: 불변 조건(Invariant) 검증, 내부 일관성 확인에 사용됩니다. 발생하면 안 되는 내부 오류를 나타냅니다.
- revert: 조건 없이 실행을 중단하거나, 사용자 정의 오류를 발생시킬 때 사용됩니다. 가스 효율이 좋습니다.
- 사용자 정의 오류:
error 오류이름(매개변수);형태로 정의하며,revert 오류이름(인자);로 호출합니다. 문자열 오류 메시지보다 가스 효율이 높습니다. - 모범 사례: require는 외부 입력 오류에, assert는 내부 논리 오류에, revert는 복잡한 조건이나 사용자 정의 오류에 사용합니다.
10. try/catch 구조
Solidity 0.6.0에서 도입된 try/catch 구조는 외부 호출의 실패를 처리하여 전체 트랜잭션을 롤백하지 않고도 복구할 수 있게 해줍니다.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.17;
contract TryCatch {
event ErrorCaught(string errorType, string errorMsg);
event SuccessfulCall(bytes returnData);
// 외부 호출을 위한 함수 (this를 통해 호출)
function divide(uint256 a, uint256 b) public pure returns (uint256) {
return a / b; // b == 0 이면 예외 발생
}
// 기본 try/catch
function tryCatchDivide(uint256 a, uint256 b) public returns (uint256, bool) {
try this.divide(a, b) returns (uint256 result) {
return (result, true);
} catch {
emit ErrorCaught("Unknown", "나눗셈 실패");
return (0, false);
}
}
// 오류 유형별 catch
function tryCatchWithErrorTypes(uint256 a, uint256 b) public returns (uint256, bool) {
try this.divide(a, b) returns (uint256 result) {
return (result, true);
} catch Error(string memory reason) {
// require/revert(string) 으로 인한 오류
emit ErrorCaught("Error", reason);
return (0, false);
} catch Panic(uint256 errorCode) {
// assert 또는 산술 오류로 인한 패닉
string memory panicReason = getPanicReason(errorCode);
emit ErrorCaught("Panic", panicReason);
return (0, false);
} catch (bytes memory lowLevelData) {
// 기타 모든 저수준 오류
emit ErrorCaught("Low-level", string(lowLevelData));
return (0, false);
}
}
// 중첩 try/catch (대체 전략)
function nestedTryCatch(uint256 a, uint256 b) public returns (uint256) {
try this.divide(a, b) returns (uint256 result) {
return result;
} catch {
try this.divide(a, 1) returns (uint256 fallbackResult) {
return fallbackResult;
} catch {
return 0; // 모든 시도 실패
}
}
}
// 패닉 오류 코드 설명 반환
function getPanicReason(uint256 errorCode) internal pure returns (string memory) {
if (errorCode == 0x01) return "Assertion failed";
if (errorCode == 0x11) return "Arithmetic overflow/underflow";
if (errorCode == 0x12) return "Division by zero";
return "Unknown panic";
}
receive() external payable {}
}
핵심 사항
- 사용 가능 대상: 외부 함수 호출과 계약 생성(
new Contract())에만 사용할 수 있습니다. 내부 함수 호출에는 사용할 수 없습니다. - 오류 유형:
catch Error(string memory reason)는 require/revert로 인한 오류를,catch Panic(uint256 errorCode)는 assert나 산술 오류를,catch (bytes memory lowLevelData)는 기타 모든 오류를 처리합니다. - 용도: 일부 작업이 실패해도 전체 트랜잭션이 중단되지 않아야 할 때, 대체 전략을 구현할 때, 배치 작업 중 오류를 기록하고 계속 진행해야 할 때 유용합니다.
11. 함수 호출과 중첩 표현식
함수 호출과 중첩 표현식은 Solidity 코드의 중요한 구성 요소입니다.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.17;
contract FunctionCallsAndNestedExpressions {
uint256 public counter;
event FunctionCalled(string name, address caller, uint256 timestamp);
// 기본 함수 호출
function basicCall(uint256 a, uint256 b) public returns (uint256) {
emit FunctionCalled("basicCall", msg.sender, block.timestamp);
uint256 result = add(a, b);
counter++;
return result;
}
function add(uint256 a, uint256 b) internal pure returns (uint256) {
return a + b;
}
// 중첩 함수 호출
function nestedCalls(uint256 value) public returns (uint256) {
return add(multiplyByTwo(value), divideByTwo(value));
}
function multiplyByTwo(uint256 value) internal pure returns (uint256) {
return value * 2;
}
function divideByTwo(uint256 value) internal pure returns (uint256) {
return value / 2;
}
// 인자 평가 순서 (왼쪽에서 오른쪽)
function parameterEvaluation(uint256 a, uint256 b) public returns (uint256) {
return complexFunction(incrementAndReturn(a), incrementAndReturn(b));
}
function incrementAndReturn(uint256 val) public returns (uint256) {
counter++;
return val + 1;
}
function complexFunction(uint256 x, uint256 y) public pure returns (uint256) {
return x * y;
}
// 단락 평가와 함수 호출
function shortCircuitWithFunctions(uint256 a) public returns (bool) {
return isZero(a) || isGreaterThanHundred(a); // isZero가 참이면 isGreaterThanHundred는 호출되지 않음
}
function isZero(uint256 value) public returns (bool) {
counter++;
return value == 0;
}
function isGreaterThanHundred(uint256 value) public returns (bool) {
counter++;
return value > 100;
}
// 재귀 호출
function factorial(uint256 n) public pure returns (uint256) {
if (n <= 1) { return 1; }
return n * factorial(n - 1);
}
// 반환 값 구조 분해
function destructureReturns() public pure returns (uint256, bool) {
(uint256 value, bool success, string memory message) = multiReturn(42);
if (success) { return (value, true); }
else { return (0, false); }
}
function multiReturn(uint256 input) internal pure returns (uint256, bool, string memory) {
if (input > 0) { return (input * 2, true, "성공"); }
else { return (0, false, "입력은 양수여야 함"); }
}
// 가스 최적화된 반복 호출
function gasOptimizedCalls(uint256[] memory values) public returns (uint256) {
uint256 sum = 0;
uint256 length = values.length;
for (uint256 i = 0; i < length;) {
uint256 processed = processValue(values[i]);
sum += processed;
unchecked { i++; }
}
return sum;
}
function processValue(uint256 value) internal pure returns (uint256) {
return (value % 2 == 0) ? value * value : value * 3 + 1;
}
}
핵심 사항
- 내부 vs 외부 호출: 내부 호출(internal)은 직접 호출되어 가스가 적게 들고, 외부 호출(external)은 메시지를 통해 호출되어 가스가 더 많이 듭니다.
this.function()은 외부 호출입니다. - 중첩 호출:
f(g(x), h(y))형태로 중첩 가능하며, 안쪽에서 바깥쪽으로 평가됩니다. - 단락 평가: 논리 연산자(&&, ||)는 단락 평가를 사용하므로, 함수 호출이 포함된 경우 예상치 못한 동작이 발생할 수 있습니다.
- 재귀: Solidity는 재귀를 지원하지만, 스택 깊이 제한(보통 1024)에 주의해야 합니다.
12. 복잡한 제어 흐름 패턴
복잡한 제어 흐름 패턴은 여러 제어 구조를 결합하여 고급 로직과 알고리즘을 구현합니다.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.17;
contract ComplexControlFlowPatterns {
uint256 public counter;
bool public paused;
// 상태 머신 패턴
enum State { Pending, Active, Completed, Failed }
struct Task {
uint256 id;
State state;
uint256 value;
uint256 startTime;
uint256 completionTime;
}
mapping(uint256 => Task) public tasks;
mapping(uint256 => bool) public processedIds;
// 상태 전환을 처리하는 함수
function processTask(uint256 taskId) public returns (bool) {
Task storage task = tasks[taskId];
require(task.id == taskId, "작업이 존재하지 않음");
if (task.state == State.Pending) {
task.state = State.Active;
task.startTime = block.timestamp;
return true;
} else if (task.state == State.Active) {
if (block.timestamp >= task.startTime + 1 days) {
task.state = State.Completed;
task.completionTime = block.timestamp;
return true;
} else { return false; }
} else { return false; }
}
// Guard 패턴 (조기 반환)
function guardedOperation(uint256 taskId, uint256 val) public returns (bool) {
if (paused) { return false; }
Task storage task = tasks[taskId];
if (task.id == 0 || task.state != State.Active) { return false; }
if (val == 0 || val > task.value) { return false; }
task.value -= val;
if (task.value == 0) {
task.state = State.Completed;
task.completionTime = block.timestamp;
}
return true;
}
// 배치 처리 패턴 (with try/catch)
function batchProcess(uint256[] memory taskIds) public returns (uint256, uint256) {
uint256 total = 0;
uint256 successCount = 0;
for (uint256 i = 0; i < taskIds.length; i++) {
uint256 taskId = taskIds[i];
if (processedIds[taskId]) { continue; }
bool success = false;
try this.processTask(taskId) returns (bool result) {
success = result;
} catch {
tasks[taskId].state = State.Failed;
}
processedIds[taskId] = true;
total++;
if (success) { successCount++; }
}
return (total, successCount);
}
// 오류 복구 패턴
function resilientOperation(uint256 taskId) public returns (bool) {
try this.processTask(taskId) returns (bool result) {
return result;
} catch {
try this.fallbackProcessTask(taskId) returns (bool fallbackResult) {
return fallbackResult;
} catch {
tasks[taskId].state = State.Failed;
return false;
}
}
}
function fallbackProcessTask(uint256 taskId) public returns (bool) {
Task storage task = tasks[taskId];
if (task.id == taskId && task.state != State.Failed) {
task.state = State.Completed;
task.completionTime = block.timestamp;
return true;
}
return false;
}
// 일시 중지/재개 패턴
modifier whenNotPaused() { require(!paused, "시스템이 일시 중지됨"); _; }
function pauseSystem() public { paused = true; }
function resumeSystem() public { paused = false; }
function safeOperation() public whenNotPaused returns (bool) { counter++; return true; }
}
핵심 사항
- 상태 머신 패턴: 열거형(enum)으로 상태를 정의하고, 각 상태에 따른 허용된 연산과 전환 규칙을 구현합니다.
- Guard 패턴: 모든 오류 조건을 함수 시작 부분에서 검사하고 조기에 반환하여 주요 로직을 단순화합니다.
- 배치 처리 패턴: 여러 작업을 하나의 트랜잭션으로 그룹화하고, 루프를 사용하여 처리하며, 개별 작업의 실패를 처리합니다.
- 오류 복구 패턴: 기본 방법이 실패할 경우 대체 방법을 시도하고, 모든 방법이 실패할 경우 우아하게 실패를 처리합니다.
13. 가스 최적화 기법
가스 최적화 기법을 이해하고 적용하는 것은 효율적인 Solidity 계약을 작성하는 데 중요합니다.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.17;
contract GasOptimizationTechniques {
uint256 public counter;
mapping(address => uint256) public balances;
// 최적화 1: 루프 최적화 (unchecked, 길이 캐싱)
function optimizedForLoop(uint256[] memory arr) public pure returns (uint256) {
uint256 sum = 0;
uint256 length = arr.length; // 배열 길이 캐싱
for (uint256 i = 0; i < length;) {
sum += arr[i];
unchecked { i++; } // 오버플로우 검사 회피
}
return sum;
}
// 최적화 2: 단락 평가 활용
function shortCircuitEvaluation(uint256 a, uint256 b) public pure returns (bool) {
return (a == 0) || (b > 1000) || (a % b == 0); // 간단하고 단락 가능성 높은 조건을 앞에 배치
}
// 최적화 3: 상태 변수 읽기 최소화 (캐싱)
function optimizedStorageReads() public returns (uint256) {
uint256 localCounter = counter; // 상태 변수를 로컬 변수에 캐싱
localCounter += 1;
localCounter += 2;
localCounter += 3;
counter = localCounter; // 한 번만 쓰기
return counter;
}
// 최적화 4: 불필요한 저장소 쓰기 방지
function updateIfChanged(address user, uint256 newBalance) public returns (bool) {
if (balances[user] != newBalance) { // 값이 실제로 변경된 경우에만 쓰기
balances[user] = newBalance;
return true;
}
return false;
}
// 최적화 5: 비트 연산 사용
function bitwiseOperations(uint256 value) public pure returns (uint256) {
uint256 result = value << 3; // value * 8
result = result >> 1; // result / 2
result = result & 0xFF; // result % 256
return result;
}
// 최적화 6: 조건문 최적화 (삼항 연산자)
function optimizedConditional(uint256 value) public pure returns (uint256) {
return (value > 100) ? 100 : value; // if-else 보다 가스 효율적
}
// 최적화 7: 내부 함수 인라인화 (작은 함수)
function inlinedCalculation(uint256 value) public pure returns (uint256) {
return value * value + 2 * value + 1; // 직접 계산이 함수 호출보다 저렴
}
// 최적화 8: 조기 반환 (Early Return)
function earlyReturn(uint256 value) public pure returns (uint256) {
if (value == 0) return 0;
if (value > 1000) return 1000;
uint256 result = value;
for (uint256 i = 0; i < 10; i++) { result += value / (i + 1); }
return result;
}
}
핵심 사항
- 루프 최적화: 배열 길이 캐싱,
unchecked { i++; }사용, 작은 루프 풀기(unrolling) 등을 통해 가스를 절약할 수 있습니다. - 저장소 접근 최적화: 상태 변수를 로컬 변수에 캐싱하고, 실제로 값이 변경될 때만 쓰기 연산을 수행합니다.
- 단락 평가 활용: 단락될 가능성이 높은 조건을 앞에 배치하여 불필요한 연산을 피합니다.
- 비트 연산 사용: 특정 산술 연산(2의 거듭제곱 곱셈/나눗셈, 모듈로 연산)은 비트 연산으로 대체하는 것이 더 저렴할 수 있습니다.
- 함수 호출 최소화: 작은 함수는 인라인화하고, 조건문은 삼항 연산자를 사용하는 것이 더 효율적일 수 있습니다.
14. 실제 적용 사례
실제 적용 사례를 통해 다양한 표현식과 제어 구조가 어떻게 조합되어 사용되는지 보여줍니다.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.17;
// 적용 사례 1: 다중 서명 지갑 (MultiSigWallet)
contract MultiSigWallet {
address[] public owners;
mapping(address => bool) public isOwner;
uint256 public requiredSignatures;
struct Transaction {
address to;
uint256 value;
bytes data;
bool executed;
uint256 signatureCount;
}
Transaction[] public transactions;
mapping(uint256 => mapping(address => bool)) public signatures;
event Deposit(address indexed sender, uint256 value);
event TransactionSubmitted(uint256 indexed txIndex, address indexed to, uint256 value);
event TransactionSigned(uint256 indexed txIndex, address indexed owner);
event TransactionExecuted(uint256 indexed txIndex, address indexed to, uint256 value);
constructor(address[] memory _owners, uint256 _requiredSignatures) {
require(_owners.length > 0, "소유자가 필요함");
require(_requiredSignatures > 0 && _requiredSignatures <= _owners.length, "잘못된 서명 요구 수");
for (uint256 i = 0; i < _owners.length; i++) {
address owner = _owners[i];
require(owner != address(0), "잘못된 소유자 주소");
require(!isOwner[owner], "소유자는 고유해야 함");
isOwner[owner] = true;
owners.push(owner);
}
requiredSignatures = _requiredSignatures;
}
modifier onlyOwner() { require(isOwner[msg.sender], "소유자가 아님"); _; }
modifier txExists(uint256 txIndex) { require(txIndex < transactions.length, "트랜잭션이 존재하지 않음"); _; }
modifier notSigned(uint256 txIndex) { require(!signatures[txIndex][msg.sender], "이미 서명함"); _; }
modifier notExecuted(uint256 txIndex) { require(!transactions[txIndex].executed, "이미 실행됨"); _; }
receive() external payable { emit Deposit(msg.sender, msg.value); }
// 트랜잭션 제출 (자동 서명 포함)
function submitTransaction(address _to, uint256 _value, bytes memory _data) public onlyOwner returns (uint256) {
uint256 txIndex = transactions.length;
transactions.push(Transaction({ to: _to, value: _value, data: _data, executed: false, signatureCount: 0 }));
emit TransactionSubmitted(txIndex, _to, _value);
signTransaction(txIndex); // 제출자가 자동 서명
return txIndex;
}
// 트랜잭션 서명 (충분한 서명이 모이면 자동 실행)
function signTransaction(uint256 txIndex) public
onlyOwner txExists(txIndex) notSigned(txIndex) notExecuted(txIndex)
{
Transaction storage transaction = transactions[txIndex];
signatures[txIndex][msg.sender] = true;
transaction.signatureCount++;
emit TransactionSigned(txIndex, msg.sender);
if (transaction.signatureCount >= requiredSignatures) {
executeTransaction(txIndex);
}
}
// 트랜잭션 실행
function executeTransaction(uint256 txIndex) public txExists(txIndex) notExecuted(txIndex) {
Transaction storage transaction = transactions[txIndex];
require(transaction.signatureCount >= requiredSignatures, "충분한 서명이 없음");
transaction.executed = true;
(bool success, ) = transaction.to.call{value: transaction.value}(transaction.data);
require(success, "트랜잭션 실행 실패");
emit TransactionExecuted(txIndex, transaction.to, transaction.value);
}
// 서명 취소
function revokeSignature(uint256 txIndex) public onlyOwner txExists(txIndex) notExecuted(txIndex) {
require(signatures[txIndex][msg.sender], "서명되지 않은 트랜잭션");
signatures[txIndex][msg.sender] = false;
transactions[txIndex].signatureCount--;
}
// 배치 서명
function batchSign(uint256[] memory txIndices) public returns (uint256) {
uint256 signedCount = 0;
for (uint256 i = 0; i < txIndices.length; i++) {
uint256 txIndex = txIndices[i];
if (txIndex < transactions.length &&
!transactions[txIndex].executed &&
!signatures[txIndex][msg.sender] &&
isOwner[msg.sender])
{
signatures[txIndex][msg.sender] = true;
transactions[txIndex].signatureCount++;
emit TransactionSigned(txIndex, msg.sender);
signedCount++;
if (transactions[txIndex].signatureCount >= requiredSignatures) {
executeTransaction(txIndex);
}
}
}
return signedCount;
}
// 조회 함수
function getTransaction(uint256 txIndex) public view txExists(txIndex) returns (
address to, uint256 value, bytes memory data, bool executed, uint256 signatureCount
) {
Transaction storage t = transactions[txIndex];
return (t.to, t.value, t.data, t.executed, t.signatureCount);
}
function getOwnersCount() public view returns (uint256) { return owners.length; }
function getTransactionCount() public view returns (uint256) { return transactions.length; }
function getBalance() public view returns (uint256) { return address(this).balance; }
}
적용 사례 분석 (다중 서명 지갑)
- 상태 관리: 구조체(Transaction)와 매핑(signatures)을 사용하여 복잡한 상태를 관리합니다.
- 제어 구조: 다양한 modifier (onlyOwner, txExists 등)를 사용하여 접근 제어와 상태 검증을 구현합니다.
- 상태 머신: 트랜잭션은 '제출 → 서명 → 실행(또는 취소)'의 상태 흐름을 가집니다.
- 오류 처리: require를 사용하여 입력 값, 권한, 상태를 검증합니다.
- 이벤트: 모든 중요 작업(입금, 제출, 서명, 실행)에 대해 이벤트를 발생시켜 투명성을 제공합니다.