c0mpos3r

[Solidity] Basic Syntax 본문

Web3/Solidity

[Solidity] Basic Syntax

음대생 2025. 6. 6. 14:05

1. Solidity License

// SPDX-License-Identifier: GPL-3.0
// SPDX-License-Identifier: MIT

 

MIT License: 간단하고 자유로운 Open Source License

  • 재배포 및 수정: 소프트웨어를 자유롭게 사용, 복사, 수정, 병합, 게시, 배포, 서브, 라이센스, 판매할 수 있다.
  • 책임 면제: 소프트웨어는 "있는 그대로" 제공되며, 사용에 따른 책임은 사용자에게 있다.
  • 저작권 표시: 소프트웨어를 사용할 때 원래 저작권 표시와 허가 표시를 포함해야 합니다.

GPL-3.0: MIT License보다 더 엄격한 조건을 License

  • 자유로운 사용: 소프트웨어를 자유롭게 사용, 복사, 수정, 병합, 게시, 배포할 수 있습니다.
  • 소스 코드 공개: 소프트웨어를 배포할 때, 수정한 소스 코드를 공개해야 합니다.
  • 동일한 라이선스 적용: 수정된 소프트웨어를 배포할 때는 동일한 GPL-3.0 라이선스를 적용해야 합니다.
  • 특허 보증: 소프트웨어를 배포하는 사람은 해당 소프트웨어에 관련된 특허 권리를 포기해야 합니다.

2. Solidity Compile Version 지정

// pragma solidity ^0.8.24; // 0.8.24 단일 지정
pragma solidity >= 0.7.0 < 0.9.0; // 범위 지정 0.7.0 이상 0.9.0 미만

3. Solidity Contract 정의

‘contract’ 키워드를 사용하여 스마트 계약을 명시할 수 있으며, 이름을 지정할 수 있다. 기존의 class와 비슷하게 명시를 진행할 수 있으며, 컨트랙트 이름의 경우 CamelCase로 표기한다.

// SPDX-License-Identifer: MIT
pragma solidity >= 0.7.0 < 0.9.0;

contract MyContract {
		// 상태 변수, 함수 등을 여기에 정의한다.
}

4. Variable 정의

uint256 public num = 10; 
// 자료형 접근제한자 변수명

 


5. DataType(자료형)

  • integer: (u)int 앞의 Unsigned 여부에 따라 부호의 유무가 달라지는 정수 자료형 int 뒤의 수는 2의 배수로 범위를 지정할 수 있음
// int(음수 포함) Vs uint (음수 미포함)  
int8 public it = -4; //int8 : -2^7 ~ (2^7)-1
uint8 public it2 = 123; //uint8 : 0 ~ 2^8-1
    
uint256 public it3 = 123; //uint256 : 0 ~ 2^256-1

 

  • boolean: true, false
bool public t = true;
bool public f = false;

 

  • byte: bytes(1~32) 길이를 지정한 byte 자료, bytes 길이를 지정하지 않는 byte 자료
bytes1 public b1 = 0x12;
bytes32 public b32 = 0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef;
		
bytes public s = "Hello World";

 

  • address: 20 bytes의 길이로 스마트 컨트랙트의 주소를 저장할 수 있다.
address public addr = 0x123456789012345678901234567890
  • 이와 마찬가지로, 디지털 지갑의 계정마다 각자의 주소를 할당 받는다.
  • 이 주소를 통해 디지털 코인을 보내기도 하고, 스마트 컨트랙트를 불러오기도 함.(쉽게, 주소 = 이더 같은 디지털 코인을 주고 받는 은행 계좌번호)

6. Gas, Ether

  • Ether: 블록체인 상에서 가격의 단위 나타낸다. 1 ether == 10^9 Gwei == 10^18 wei
uint256 public price = 1 ether;

 

  • Gas: 블록체인을 이용할 때 발생하는 수수료 gwei == gas
uint256 public value2 = 1 wei;
uint256 public value3 = 1 gwei;

7. Function 정의

  • function 함수이름(매개 변수) 접근제한자 view/pure returns(리턴 타입)
function Test(uint256 _value) public view returns(uint256) {  
		return _value + 10; 
}

 

  • return 타입 명시
function Test(uint256 _value) public returns(bool) {
		return _value < 10 ? true : false;
}

 

  • view: function 밖에 변수 읽을 수 있으나 변경이 불가능한 함수 정의
uint256 public a = 10;

function ReadA() public view returns(uint256) {
		return a + 5;
}

 

  • pure: function 밖에 변수들을 읽지 못하며, 변경도 불가능한 함수 정의
function ReadB() public pure returns(uint256) {
	uint256 public b = 2;
	return b + 8;
}

어떠한 외부 변수를 읽는 것도 변경하는 것도 불가능하며, 오로지 내부에서 정의된 변수만 변경하고 읽을 수 있다.

  • viewpure 둘 다 명시하지 않을 경우, 외부의 변수를 읽고 변경할 수 있다.

8. 접근 제한자

  • public: 모든 컨트랙트 및 계정에서 접근 가능
contract Public_example {
    uint256 public a = 3;
    
    function changeA(uint256 _value) public {
        a =_value;
    }
    function get_a() view public returns (uint256)  {
        return a;
    }
}

contract Public_example_2 {
    
    Public_example instance = new Public_example();

    function changeA_2(uint256 _value) public{
      instance.changeA(_value);
    }
    function use_public_example_a() view public returns (uint256)  {
        return instance.get_a();
    }
}

 

  • private: 해당 컨트랙트 내에서만 접근 가능
contract Private_example {
    uint256 private a = 3;
    
    function get_a() view public returns (uint256)  {
        return a;
    }
}

 

  • external: 컨트랙트 외부에서만 접근 가능
contract external_example {
    uint256 private a = 3;
    
    function get_a() view external returns (uint256)  {
        return a;
    }
}

contract external_example_2 {
    external_example instance = new external_example();

    function external_example_get_a() view public returns (uint256)  {
        return instance.get_a();
    }
}
  • internal: 컨트랙트과 상속 받은 컨트랙트에서만 접근 가능

9. Data Stored

  • storage
    • 컨트랙트 내부의 전역 변수, 함수들이 저장된다. 즉, 대부분의 변수, 함수들이 storage 영역에 저장된다.
    • storage의 저장된 데이터는 영속적으로 저장이 되기 때문에 상대적으로 가스 비용이 비싸다. (모든 블록체인에 영속적인 데이터를 계속 복사해야하기 때문에 비용이 많이 든다.)
  • memory
    • 함수 내부의 파라미터, 리턴값, 래퍼런스 타입, 지역 변수 등이 주로 저장이 된다.
    • 영속적이지 않으며, 함수가 실행 중일 때만 유효하고 함수가 종료되면 사라지기 때문에, storage 보다 상대적으로 가스 비용이 싸다.
  • calldata
    • 주로 external function의 파라미터에서 사용된다.
  • stack
    • EVM(Ethereum Virtual Machine)에서 stack data를 관리할 때 쓰는 영역이다. 1024MB로 제한적인 영역을 갖게 된다.

10. Contract Instance

  • Instance: 주로 하나의 컨트랙트에서 다른 컨트랙트로 접근할 때 사용한다.
  • ContractName InstanceName = new ContractName();
contract InstanceA {
	string public name;
	uint256 public age;
	
	constructor(string memory _name, uint256 _age) {
		name = _name;
		age = _age;
	}
	
	function change(string memory _name, uint256 _age) public {
		name = _name;
		age = _age;
	}
}

contract InstanceB {
	A instance = new A("Alice", 52);
	
	function change(string memory _name, uint256 _age) public {
		instance.change(_name, _age);	
	}
	
	function get() public view returns (string memory, uint256) {
		return (instance.name(), instance.age());
	}
}

 

  • constructor(생성자): 스마트 컨트랙트가 생성 또는 배포, 그리고 인스턴스화 될 때 초기값을 설정해주는 용도로 사용됨.
constructor(string memory _name, uint256 _age){
		name = _name;
		age = _age;
}

 

11. 상속 및 Override

  • contract 컨트랙트이름 is 상속받을컨트랙트()
contract Father {
	string public familyName = "Kim";
	string public givenName = "Jung";
	
	constructor (string memory _givenName) public {
		givenName = _givenName;

}

// Father 컨트랙 상속 받음
contract Son is Father("James") {}

 

Override

  • 부모 contract에서 상속 받은 기능을 자식 contract에서 재정의 하는 것
  • 자식 contract에 의해 override될 함수는 virtual 키워드를 사용하여 선언한다.
  • 부모 contract의 함수를 override하려는 자식 contract의 함수는 override 키워드를 사용하여 함수를 정의한다.
contract Father {
	string public familyName = "Kim";
	string public givenName = "Jung";
	uint256 public money = 100;
	
	constructor (string memory _givenName) public {
		givenName = _givenName;
	}
	
	function getMoney() view public virtual returns (uint256) {
		return money;
	}
}

// Father 컨트랙 상속 받음
contract Son is Father {
	uint256 public earning = 0;

	// 이런 방법으로도 부모 컨트랙을 초기화 할 수 있다.
	constructor () Father("James") {}
	
	function work() public {
		earning += 100;
	}
	
	function getMoney() view public override returns (uint256) {
		return money + earning;
	}
}

2개 이상의 컨트랙트 상속

  • 상속을 받으려는 2개의 contract에서 모두 같은 이름의 메서드를 사용하고 있는 경우, 오버라이딩을 자식 contract에서 필수로 진행해야 한다.
contract Father {
	uint256 public fatherMoney = 1000;
	
	function getMoney() public view virtual returns (uint256) {
		return fatherMoney;
	}
}

contract Mother {
	uint256 public motherMoney = 500;
	
	function getMoney() public view virtual returns (uint256) {
		return motherMoney;
	}
}

contract Son is Father, Mother {
	function getMoney() public view override(Father, Mother) returns (uint256) {
		return fatherMoney + motherMoney; // 1500
	}
}

Super

  • 오버라이드할 메서드의 크기가 크다면 일일히 코드를 작성하기도 불편하고 중복되는 코드로 가독성이 떨어질 수 있다. 이때 super를 사용한다.
contract Father {
	event fatherName(string name);
	
	function who() public virtual {
		emit fatherName("father");
	}
}

contract Son is Father {
	event sonName(string name);
	
	function who() public override {
		super.who(); // Father 컨트랙의 who 메서드의 기존 코드를 모두 가져온다.
		
		emit sonName("son"); // 기존 메서드에서 추가하고 싶은 코드를 그대로 추가한다.
	}
}

12. Event

solidity에서는 기본적으로 print와 같은 출력 함수가 존재하지 않는데, 그 대신 이벤트라는 것을 사용한다.

  • 이벤트 수신 및 사용자 인터페이스 업데이트
  • 가스를 적게 소모하는 스토리지 형태
contract Event {

    event Log(address indexed sender, string message);
    event AnotherLog();

    function test() public {
		    emit info("test", 1000); // 블록에 "test", 1000 이라는 데이터가 저장된다.
        emit Log(msg.sender, "Hello World!");
        emit Log(msg.sender, "Hello EVM!");
        emit AnotherLog();
    }
}

Event Indexed

  • 여러개의 데이터를 블록안에 저장하고, 출력할때 이터러블하게 사용할 수 있도록 인덱싱을 제공한다.
  • indexed를 쓰지 않으면 이벤트를 이터러블하게 접근할 수 없기 때문에, 순회도 불가능하고 원하는 값만 필터로 가져오기 힘들다.
  • 하지만 indexed를 사용하게 된다면, 이터러블하게 동작하여 원하는 이벤트만을 가져올 수도 있고 필터링도 가능하다.
contract Event_Indexed {
	event tracker(uint256 num, string str);
	event trackerIndexed(uint256 indexed num, string str);
	
	uint256 num = 0;
	
	function pushEvent(string memory _str) public {
		emit tracker(num, _str);
		emit trackerIndexed(num, _str); // num에 대한 인덱싱을 적용하여 이터러블하게 사용할 수 있다.
		
		num++;
	}
}

13. Mapping

  • mapping은 다음 구문을 사용하여 생성됩니다 mapping(keyType => valueType).
  • mapping은 기본적으로 key:value로 값을 저장하고 관리할 수 있다. 하지만 이터러블하지 않음으로 순회가 불가능하며, 전체 length도 구할 수 없다.
  • python의 dict 자료형과 유사하다고 보면 편할 것 같다.
contract Mapping {
	mapping(uint256=>uint256) private ageList;
	
	function setAgeList(uint256 _index, uint256 _age) public {
		ageList[_index] = _age; // uint256의 key, value 저장
	}
	
	function getAge(uint256 _index) public view returns (uint256) {
		return ageList[_index];
	}
}

14. Array

  • 배열은 컴파일 시 고정 크기 또는 동적 크기를 가질 수 있습니다.
  • DoS 공격의 경우 반복적으로 배열을 순회하는 공격을 진행할 경우 가스비를 많이 소모 시키도록 공격을 진행할 수 있기 때문에 사용한다고 하더라도 최대 배열 사이즈를 50정도로 제한해두어서 사용하는 것이 좋다.
contract Array {
	uint256[] public ageArray; // 동적 길이 배열
	uint256[10] public ageArray; // 고정 길이 배열
	
	string[] public nameArray = ["Kin", "test", "test2"]; // 배열 초기화
	
	ageArray.length; // 배열 길이
	ageArray.push(_value); // 배열 값 추가
	ageArray.pop(); // 가장 최신 원소 제거
	
	ageArray[_index]; // 배열 원소 가져오기
	ageArray[_index] = _age; // 특정 배열 원소 변경

	delete ageArray[_index]; // 특정 배열 원소 제거
}

15. Struct

  • struct를 생성하여 자신만의 유형을 정의할 수 있다.
  • 구조체는 관련 데이터를 함께 그룹화할 때 유용하다.
  • 구조체는 컨트랙트 외부에서 선언하고 다른 컨트랙트에서 가져올 수 있다.
contract Struct {
	struct Person {
		uint256 age;
		string name;
		string job;
	}
	
	mapping(uint256=>Person) public PersonMapping; // 구조체 딕셔너리
	Person[] public PersonArray; // 구조체 배열
	
	function createPerson(uint256 _age, string memory _name, string memory _job) pure public returns (Person memory) {
		return Person(_age, _name, _job);
	}
}

16. 조건문

contract IfElse {
    function foo(uint256 x) public pure returns (uint256) {
        if (x < 10) {
            return 0;
        } else if (x < 20) {
            return 1;
        } else {
            return 2;
        }
    }

    function ternary(uint256 _x) public pure returns (uint256) {
        return _x < 10 ? 1 : 2;
    }
}

17. 반복문

  • 바인딩되지 않은 루프를 작성하면 가스 한도에 도달하여 Transaction이 실패할 수 있으므로 작성하지 않는 것이 좋다.
  • 위와 같은 이유로 while과 do while 루프는 거의 사용되지 않는다.
contract Loop {
    function loop() public {
        // for loop
        for (uint256 i = 0; i < 10; i++) {
            if (i == 3) {
                continue;
            }
            if (i == 5) {
                break;
            }
        }

        uint256 j;
        while (j < 10) {
            j++;
        }
    }
}

18. 예외처리

  • assert: gas를 다 소비한 후, 조건식이 false일 때 에러를 발생시킨다.
contract Assert {
	function assertNow() public pure {
		assert(false); // 조건식이 false임으로 에러를 발생시킨다. 가스비가 소비되지만, 환불해주지 않는다.
	}
}

 

  • revert: 조건 없이 에러를 발생시키고, gas를 환불 해준다.
contract Revert {
	function revertNow() public pure {
		revert("error!!"); // 조건식 없이 에러를 발생시킨다. 소비한 가스비를 환불해준다.
	}
}

 

  • require: 조건식이 false일 때 에러를 발생시키고, gas를 환불 해준다.
contract Require {
	function requireNow() public pure {
		require(false, "error!!"); // assert+revert라고 생각할 수 있다. 조건식이 false일떄, 2번째 인수의 에러 메시지로 에러를 발생시킨다. 소비한 가스비를 환불해준다.
	}
}

19. Modifier

반복되는 작업을 하나의 modifier로 정의하여 미들웨어처럼 적용하고 사용할 수 있다. 가독성 측면에서도 좋아지고 유지보수 측면에서도 유용한 역할을 한다.

만약 아래와 같은 예시 상황에서의 경우 modifier를 적용하면 편하다.

contract Modifier {
	function checkAdult(uint256 _age) public pure return (bool) {
		require(age>19,"you're not adult");
		return true;
	}
	
	function checkAdult2(uint256 _age) public pure return (bool) {
		require(age>19,"you're not adult");
		return true;
	}
}

 

위와 같은 컨트랙이 존재할 경우, require 문은 조건식도 같고 반환되는 메시지도 모두 같다. 한쪽 조건식이 바뀌어야 하는 경우 나머지 부분도 모두 바꿔주어야 하기 때문에 유지보수 측면에서 불편한다. 이럴경우 modifier를 이용해서 동작을 축약할 수 있다.

 

contract Modifier {
	modifier check(uint256 _age) {
		require(age>19,"you're not adult");
		_;
	}

	function checkAdult(uint256 _age) public check(_age) pure return (bool) {
		return true;
	}
	
	function checkAdult(uint256 _age) public check(_age) pure return (bool) {
		return true;
	}
}

위와 같이 modifier를 정의할 경우 간단하게 동작을 축약할 수 있다. 여기서 _ 의 의미는 modifier 안에 로직이 실행된 후에 적용될 코드의 위치이다. 위의 코드의 경우는 아래와 같이 동작한다.

 

contract Modifier {
	modifier check(uint256 _age) {
		require(age>19,"you're not adult");
		_;
	}

	function checkAdult(uint256 _age) public check(_age) pure return (bool) {
		require(age>19,"you're not adult");
		return true; // _ 위치에 기존 함수의 로직이 위치한다.
	}
	
	function checkAdult(uint256 _age) public check(_age) pure return (bool) {
		require(age>19,"you're not adult");
		return true; // _ 위치에 기존 함수의 로직이 위치한다.
	}
}

반대로 다음 상황일 경우에는 아래와 같이 동작한다.

 

contract Modifier {
	modifier check(uint256 _age) {
		_;
		require(age>19,"you're not adult");
	}

	function checkAdult(uint256 _age) public check(_age) pure return (bool) {
		return true; // _ 위치에 기존 함수의 로직이 위치한다.
		require(age>19,"you're not adult");
	}
	
	function checkAdult(uint256 _age) public check(_age) pure return (bool) {
		return true; // _ 위치에 기존 함수의 로직이 위치한다.
		require(age>19,"you're not adult");
	}
}

즉, _의 의미는 modifier가 실행된 이후에 기존의 함수 로직 코드가 위치할 위치를 정한다고 생각하면 편하게 이해가 가능하다.


20. Interface

contract 내에서 정의되어야할 필요한 것들을 정의할 수 있다. 여기에는 아래의 규칙을 필수로 지켜야한다.

  1. interface 내에 함수는 무조건 external로 표시한다.
  2. enum, structs 모두 사용이 가능하다.
  3. 단 변수, 생성자는 정의할 수 없다.

interface는 추상적으로 contract을 정의하는 것이기 때문에 별도로 호출해서는 안되고 내부적으로 함수들끼리 상호 작용할 수 없다.

interface ItemInfo {
	struct item {
		string name;
		uint256 price;
	}
	
	function addItemInfo(string memory, uint256 _price) external;
	function getItemInfo(uint256 index) external view returns (item memory);
}

contract Interface is ItemInfo {
	item[] public itemList;
	uint256[] public a;
	
	function addItemInfo(string memory _name, uint256 _price) override public {
		itemList.push(item(_name, _price));
	}
	
	function getItemInfo(uint256 _index) override public view returns (item memory) {
		return itemList[_index];
	}
}

21. Payable

Solidity에서는 송금이라는 특이한 함수가 사용된다. 이더를 특정 계좌로 송금할 수 있는 기능을 제공한다.

Payable

  • payable 키워드는 이더/토큰과 상호작용시 필요한 키워드이다. 즉 이더를 보내거 나 토큰을 보내는 함수의 경우는 꼭 payable 키워드를 부착 해주어야 한다.
  • 이더를 보낼 떄 send, transfer, call 이라는 메서드로 이더를 보낼 수 있는데 이때 payable 키워드의 사용을 필요로 한다. 주로 함수, 주소, 생성자에 붙여서 사용된다.
  • msg.value: 특정 주소에게 송금을 보낼 이더의 값을 표기한다.
contract MsgValue {
	event SendInfo(address _msgSender, uint256 _currentValue);

	function sendEther(address payable _to) public payable {
		require(msg.sender >= msg.value, "Balance is not enough");
		_to.transfer(msg.value); // msg.value 값 만큼 상대방에게 이더를 송금한다.
		emit SendInfo(msg.sender, msg.sender.balance);
	}
}

 

  • msg.sender: contract를 사용하는 주체의 주소를 반환한다. 즉, contract를 실행하고 있는 주체의 주소이다
contract MsgSender {
	event Sender(address _msgSender);

	function checkSender() public {
		emit Sender(msg.sender); // checkSender 함수를 호출한 사람의 주로를 반환한다.
	}
}

 

  • address.balance: 특정 주소의 계정이 현재 가지고 있는 이더의 잔액을 나타낸다.
contract Balance {
	event MyCurrentValue(address _msgSender, uint256 _currentValue);

	function checkValueNow() public {
		emit MyCurrentValue(msg.sender, msg.sender.balance); // 조회를 요청한 사람의 잔액을 보여준다.
	}
}

 

  • 생성자에 payable을 적용하여 권한 제어
  • 생성자에 payable 키워드를 적용할 경우 이더와 상호작용하여, 특정 함수에 대한 권한을 제어할 수 있다.
contract AccessControl {
	address owner;
	
	constructor() payable {
		owner = msg.sender;
	}
	
	event SendInfo(address _msgSender, uint256 _currentValue);
	
	function sendEther(address payable _to) public payable {
		require(msg.sender == owner, "Only Owner!");
		_to.transfer(msg.value);
		emit howMuch(msg.value);
	}
}

22. Sending Ether (transfer, send, call)

다른 contract로 Ehter를 송금

  • transfer (2300 가스, 오류 발생)
  • send (2300 가스, 반환 부울)
  • call(모든 가스를 전달하거나 가스를 설정하고 bool을 반환합니다)

다른 contract에서 Ether를 받으려면 아래 기능 중 하나 이상이 있어야 합니다.

  • receive() external payable
  • fallback() external payable
contract ReceiveEther {
    receive() external payable {} // Ether을 수신하는 함수입니다. msg.data는 비어 있어야 합니다.
    fallback() external payable {} // msg.data가 비어 있지 않을 때 폴백 함수가 호출됩니다.

    function getBalance() public view returns (uint256) {
        return address(this).balance;
    }
}

contract SendEther {
    function sendViaTransfer(address payable _to) public payable {
        // 이 기능은 더 이상 Ether 전송에 권장되지 않습니다.
        _to.transfer(msg.value);
    }

    function sendViaSend(address payable _to) public payable {
        // Send는 성공 또는 실패를 나타내는 부울 값을 반환합니다. 
        // 이 함수는 Ether 전송에는 권장되지 않습니다.
        bool sent = _to.send(msg.value);
        require(sent, "Failed to send Ether");
    }

    function sendViaCall(address payable _to) public payable {
        // 호출하면 성공 또는 실패를 나타내는 부울 값을 반환합니다. 
        // 현재 권장되는 방법입니다.
        (bool sent, bytes memory data) = _to.call{value: msg.value}("");
        require(sent, "Failed to send Ether");
    }
}


23. Receive & Fallback

receive

순수하게 다른 contract 또는 계정으로 이더를 받을때 동작한다

contract Bank {
	event JustFallback(address _from, string message);
	event ReceiveFallback(address _from, uint256 _value, string message);
	event JustFallbackWithFunds(address _from, uint256 _value, string message);
	
	receive() external payable {
		emit ReceiveFallback(msg.sender, msg.value, "ReceiveFallback is called");
	}
}
  • 이더 수신 전용: receive 함수는 오직 이더를 수신할 때만 호출됩니다.
  • 입력 인수 없음: 이 함수는 입력 인수를 받을 수 없습니다.
  • 반환 값 없음: 반환 값을 가질 수 없습니다.
  • 단순한 로직: 일반적으로 간단한 로직만 포함할 수 있습니다. 복잡한 로직은 fallback 함수에 포함하는 것이 좋습니다.

 

Fallback

함수를 실행하면서 이더를 보낼때, 불려진 함수가 없을 때 동작한다.

contract Bank {
	event JustFallback(address _from, string message);
	event ReceiveFallback(address _from, uint256 _value, string message);
	event JustFallbackWithFunds(address _from, uint256 _value, string message);
	
	fallback() external {
		emit JustFallback(msg.sender, "JustFallback is called");
	}
}

 

 

fallback의 경우에도 직접적으로 내부에서 호출할 수 없으며, 외부에서 함수를 실행하면서 이더를 보내는 경우, 또는 Bank에 없는 함수를 call 메서드로 호출 할 경우 동작하게 된다.

만약에 Bank에 없는 함수를 호출하면서 이더를 보낼 경우, 다음과 같이 fallback을 설정할 수 있다.

contract Bank {
	event JustFallback(address _from, string message);
	event ReceiveFallback(address _from, uint256 _value, string message);
	event JustFallbackWithFunds(address _from, uint256 _value, string message);
	
	fallback() external payable {
		emit JustFallbackWithFunds(msg.sender, msg.value, "JustFallbackWithFunds is called");
	}
}

24. Call & Delegate Call 

call

기본적으로 이더를 보낼때 사용하는 메서드이지만 외부 스마트 컨트랙의 함수를 호출할때 사용할 수 있다.

contract Add {
	function addNumber(uint256 _num1, uint256 _num2) public pure returns (uint256) {
		return _num1 + _num2;
	}
}

contract Caller {
	event CalledFunction(bool _success, bytes _output);
	
	function callMethod(address _contractAddr, uint256 _num1, uint256 _num2) public {
		(bool success, bytes memory outputFromCalledFunction) = _contractAddr.call(
			abi.encodeWithSignature("addNumber(uint256, uin256)", _num1, _num2)
		);
		
		require(success, "failed to transfer ether");
		emit CalledFunction(success, outputFromCalledFunction);
	}
}

위와 같이 call 메서드를 이용하면 외부 스마트 컨트랙 함수를 호출할 수 있다. 호출을 할때는 조금 특이하게 호출을 진행하는데, abi라는 내장 라이브러리를 이용해 함수 호출을 진행한다. (abi는 이더리움 환경 내에서 스마트 컨트랙을 상호작용시키는 표준 방법이다.) abi의 encodeWithSignature 메서드를 통해서 외부 스마트 컨트랙의 함수를 호출할 수 있다.

 

반환값은 함수 호출의 성공 여부를 나타내는 bool과 함수가 호출되고 난 이후 반환값이 bytes 형식으로 반환된다.

 

delegate call

delegate call을 사용할 경우 외부 contract의 함수를 단순히 호출하는것이 아닌 외부 contract의 함수를 완전히 복제하여 사용하는 것과 같다. 즉, msg.sender로 외부 contract의 호출자를 확인해보면 contract을 실행한 사용자를 가리킨다는 특징이 있다.

 

delegate call의 경우 외부 contract의 함수들을 자신의 것처럼 사용한다. (실질적인 값도 caller에 저장된다.) 여기서 하나의 조건이 존재하는데, 외부 contract 와 caller contract은 같은 변수를 가지고 있어야한다.

contract Add {
	uint256 public num = 0;
	event Info(address _addr, uint256 _num);
	
	function plusOne() public {
		num = num + 1;
		emit Info(msg.sender, num);
	}
}

contract Caller {
	uint256 public num = 0;
	
	function callNow(address _contractAddr) public payable {
		(bool success,) = _contractAddr.call(abi.encodeWithSignature("plusOne()"));
		require(success, "failed");
	}
	
	function delegateCallNow(address _contractAddr) public payable {
		(bool success,) = _contractAddr.delegatecall(abi.encodeWithSignature("plusOne()"));
		require(success, "failed");
	}
}

위와 같은 예시 코드의 경우 callNow 함수를 호출하면, Add 스마트 컨트랙의 num 값이 변경된다. 하지만 delegate call 함수를 호출할 경우 Add 스마트 컨트랙의 값이 바뀌는 것이 아닌 Caller 스마트 컨트랙의 num 값이 변경된다.

 

delegate call의 경우에는 외부 스마트 컨트랙의 함수를 완전히 복제하여 사용한다.


25. Enum

상수 집합을 따로 정의하고 싶은 경우 사용한다. 이를 이용하면 코드의 가독성을 높이고, 의도를 분명하게 표현할 수 있다.

contract Order {
    enum Status { 
		    Pending,    // 0
		    Shipped,    // 1 
		    Completed,  // 2
		    Cancelled   // 3
		}

    Status public orderStatus;

    function setStatus(Status _status) public {
        orderStatus = _status;
    }

    function getStatus() public view returns (Status) {
        return orderStatus;
    }
}

26. Library 

  • using 키워드를 사용
  • 공통적으로 사용되는 코드를 재사용 가능하게 만드는 방법이다.
  • 라이브러리는 상태를 가지지 않으며, 상태 변경을 하지 않는 함수들로 구성됩니다. 라이브러리를 사용하면 코드 중복을 줄이고, 코드의 모듈화를 통해 유지보수를 쉽게 할 수 있다.
library SafeMath {
	function add(uint8 a, uint8 b) internal pure returns (uint8) {
		require(a + b >= a, "overflow");
		return a + b;
	}
}

contract  {
	using SafeMath for uint8; // 라이브러리 사용
	uint8 public a;
	
	function test(uint8 _num1, uint8 num2) public {
		a = _num.add(_num2); // 이와 같이 메서드처럼 호출이 가능하다.
		a = SafeMath.add(_num1, _num2); // 단순하게 라이브러리에서 add 메서드를 호출할 수도 있다.
	}
}

27. Import

Solidity에서는 로컬 및 외부 solidity 파일을 가져올 수 있다.

  • local: 상대 경로나 절대 경로를 이용해 로컬에 위치한 solidity 파일을 import 시킬 수 있다.
import "./test.sol";
  • 외부: url을 이용하면 외부에 있는 solidity 파일도 import시켜 사용할 수 있다.
import "<https://github.com/OpenZeppelin/openzeppelin-contracts/blob/docs-v3.x/contracts/math/SafeMath.sol>";

 


참고 인용 문헌

 

Solidity by Example

 

solidity-by-example.org

 

 

 

 

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

[Secureum] Solidity 201  (2) 2025.07.11
[Secureum] Solidity 101  (8) 2025.07.11