[Solidity] Error, Function Modifier, Events, Constructor, Shadowing, Super

jhcha·2023년 7월 29일
0

Solidity

목록 보기
6/17
post-thumbnail

Error

url: https://solidity-by-example.org/error/

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.17;

contract Error {
    function testRequire(uint _i) public pure {
        // Require should be used to validate conditions such as:
        // - inputs
        // - conditions before execution
        // - return values from calls to other functions
        require(_i > 10, "Input must be greater than 10");
    }

    function testRevert(uint _i) public pure {
        // Revert is useful when the condition to check is complex.
        // This code does the exact same thing as the example above
        if (_i <= 10) {
            revert("Input must be greater than 10");
        }
    }

    uint public num;

    function testAssert() public view {
        // Assert should only be used to test for internal errors,
        // and to check invariants.

        // Here we assert that num is always equal to 0
        // since it is impossible to update the value of num
        assert(num == 0);
    }

    // custom error
    error InsufficientBalance(uint balance, uint withdrawAmount);

    function testCustomError(uint _withdrawAmount) public view {
        uint bal = address(this).balance;
        if (bal < _withdrawAmount) {
            revert InsufficientBalance({balance: bal, withdrawAmount: _withdrawAmount});
        }
    }
}

에러는 트랜잭션 중 상태에 대한 모든 변경 내역을 취소시킨다.
require, revert, assert를 호출하여 에러를 발생시킬 수 있다.

  • require: 실행 전에 입력과 상태를 검증한다. if 조건문과 유사하고, 거짓인 경우 error 반환
	function testRequire(uint _i) public pure {
        // 입력 값이 10 이하인 경우, 에러 메세지를 반환하고 종료
        require(_i > 10, "Input must be greater than 10");
    }
  • revert: 에러 메세지를 출력 후 종료하는 반환문과 유사하다.
    function testRevert(uint _i) public pure {
        // 입력 값이 10 이하인 경우
        if (_i <= 10) {
        	//에러 메세지를 반환하고 종료
            revert("Input must be greater than 10");
        }
    }
  • assert: assert는 거짓이 되어서는 안 되는 코드를 확인하는 데 사용된다.
    function multiple(uint num1, uint num2) public pure returns (uint) {
        require(num1 % 2 == 0 || num2 % 2 == 0, "input error");
        uint result = num1 * num2;
        assert(result % 2 == 0);
        return result;
    }

require 함수는 외부로부터 들어오는 입력값에 대한 확인, assert는 함수 내부에 있는 값을 바탕으로 실행된 코드에 문제가 없는지 확인한다.

  • require 에러 발생 (num1, num2에 홀수를 입력한 경우)
  • assert 에러 발생 (require 주석 후, num1, num2에 홀수를 입력한 경우)

Function Modifier

url: https://solidity-by-example.org/function-modifier/

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.17;

contract FunctionModifier {
    // We will use these variables to demonstrate how to use
    // modifiers.
    address public owner;
    uint public x = 10;
    bool public locked;

    constructor() {
        // Set the transaction sender as the owner of the contract.
        owner = msg.sender;
    }

    // Modifier to check that the caller is the owner of
    // the contract.
    modifier onlyOwner() {
        require(msg.sender == owner, "Not owner");
        // Underscore is a special character only used inside
        // a function modifier and it tells Solidity to
        // execute the rest of the code.
        _;
    }

    // Modifiers can take inputs. This modifier checks that the
    // address passed in is not the zero address.
    modifier validAddress(address _addr) {
        require(_addr != address(0), "Not valid address");
        _;
    }

    function changeOwner(address _newOwner) public onlyOwner validAddress(_newOwner) {
        owner = _newOwner;
    }

    // Modifiers can be called before and / or after a function.
    // This modifier prevents a function from being called while
    // it is still executing.
    modifier noReentrancy() {
        require(!locked, "No reentrancy");

        locked = true;
        _;
        locked = false;
    }

    function decrement(uint i) public noReentrancy {
        x -= i;

        if (i > 1) {
            decrement(i - 1);
        }
    }
}

함수 변경자 (function modifier)는 한글로 함수 제어자 라고 표현하기도 한다. 아마도, visibility를 자바에서 접근 제어자라고 하듯이, 이해가 쉽고 유사한 기능이라 그런 것 같다.
함수 변경자는 사용자 정의 함수 변경자를 선언하여 사용하거나, pure, view와 같이 내장된 기능을 사용할 수도 있다.

Events

url: https://solidity-by-example.org/events/

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.17;

contract Event {
    // Event declaration
    // Up to 3 parameters can be indexed.
    // Indexed parameters helps you filter the logs by the indexed parameter
    event Log(address indexed sender, string message);
    event AnotherLog();

    function test() public {
        emit Log(msg.sender, "Hello World!");
        emit Log(msg.sender, "Hello EVM!");
        emit AnotherLog();
    }
}

event는 이더리움 블록체인에 로깅할 수 있다. 이벤트의 사용 사례는 다음과 같다.

  • 이벤트 수신 및 사용자 인터페이스 업데이트
  • 저렴한 형태의 스토리지
    블록체인의 트랜잭션이 완료되면, 트랜잭션은 그에 대한 일종의 영수증을 발행한다. 이러한 트랜잭션 영수증은 트랜잭션의 실행 동안 발생했던 행위 관련 정보들을 제공하는 로그 엔트리 (log entry)들을 갖는다.
    이벤트는 특정 이벤트가 일어나는지 감시해서 사용자 인터페이스에 반영하거나 해당 컨트랙트 상의 이벤트에 대응되는 변화를 애플리케이션의 상태에도 반영되게 할 수 있다.
    로그는 블록체인에 저장되며, 계약이 블록체인에 존재할 때 까지 컨트랙트 주소를 사용하여 액세스할 수 있다.
    생성된 이벤트는 컨트랙트 내에서 액세스할 수 없으며, 컨트랙트를 생성하고 내보낸 이벤트도 액세스할 수 없다.
{
    // 이벤트 선언
    event Log(address indexed sender, string message);
    // 이벤트 발생
    emit Log(msg.sender, "Hello World!");
}

indexed는 블록안에 출력된 이벤트들을 필터링하여 원하는 이벤트만을 가져오는데 사용할 수 있다.

참고자료: https://velog.io/@octo__/Solidity-event-%EC%99%80-indexed-%ED%82%A4%EC%9B%8C%EB%93%9C

Constructor

url: https://solidity-by-example.org/constructor/
참고자료: https://caileb.tistory.com/137

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.17;

// Base contract X
contract X {
    string public name;

    constructor(string memory _name) {
        name = _name;
    }
}

// Base contract Y
contract Y {
    string public text;

    constructor(string memory _text) {
        text = _text;
    }
}

// There are 2 ways to initialize parent contract with parameters.

// Pass the parameters here in the inheritance list.
contract B is X("Input to X"), Y("Input to Y") {

}

contract C is X, Y {
    // Pass the parameters here in the constructor,
    // similar to function modifiers.
    constructor(string memory _name, string memory _text) X(_name) Y(_text) {}
}

// Parent constructors are always called in the order of inheritance
// regardless of the order of parent contracts listed in the
// constructor of the child contract.

// Order of constructors called:
// 1. X
// 2. Y
// 3. D
contract D is X, Y {
    constructor() X("X was called") Y("Y was called") {}
}

// Order of constructors called:
// 1. X
// 2. Y
// 3. E
contract E is X, Y {
    constructor() Y("Y was called") X("X was called") {}
}

Constructor (생성자)는 컨트랙트 작성 시 선택적으로 정의할 수 있다. 생성자 함수는 컨트랙트가 실행된 후 최초 1회만 동작한다.

// Base contract X
contract X {
    string public name;

    constructor(string memory _name) {
        name = _name;
    }
}

contract X는 Contract 배포 시 생성자 함수 매개변수 _name을 입력해야 한다.

생성자 함수 실행 후 name 프로퍼티에 값이 저장된다.

contract C is X, Y {
    constructor(string memory _name, string memory _text) X(_name) Y(_text) {}
}

contract C는 contract X, Y를 상속하고 부모 contract의 생성자 함수를 호출함으로써 부모 contract를 초기화할 수 있다.

contract C is X, Y {
    constructor(string memory _name, string memory _text) X(_name) Y(_text) {}
    function getName () view public returns (string memory){
        return name;
    } 
    function getText () view public returns (string memory){
        return text;
    } 
}

상속받는 베이스 컨트랙트의 프로퍼티를 internal로 수정해서 외부에서 값을 접근하지 못하게 하고, getter 함수를 작성해서 테스트했다.

// Order of constructors called:
// 1. X
// 2. Y
// 3. D
contract D is X, Y {
    constructor() X("X was called") Y("Y was called") {}
}

// Order of constructors called:
// 1. X
// 2. Y
// 3. E
contract E is X, Y {
    constructor() Y("Y was called") X("X was called") {}
}

상속 순서에 따라 부모 생성자 호출 순서가 정해진다.

Inheritance (상속)

url: https://solidity-by-example.org/inheritance/

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.17;

/* Graph of inheritance
    A
   / \
  B   C
 / \ /
F  D,E

*/

contract A {
    function foo() public pure virtual returns (string memory) {
        return "A";
    }
}

// Contracts inherit other contracts by using the keyword 'is'.
contract B is A {
    // Override A.foo()
    function foo() public pure virtual override returns (string memory) {
        return "B";
    }
}

contract C is A {
    // Override A.foo()
    function foo() public pure virtual override returns (string memory) {
        return "C";
    }
}

// Contracts can inherit from multiple parent contracts.
// When a function is called that is defined multiple times in
// different contracts, parent contracts are searched from
// right to left, and in depth-first manner.

contract D is B, C {
    // D.foo() returns "C"
    // since C is the right most parent contract with function foo()
    function foo() public pure override(B, C) returns (string memory) {
        return super.foo();
    }
}

contract E is C, B {
    // E.foo() returns "B"
    // since B is the right most parent contract with function foo()
    function foo() public pure override(C, B) returns (string memory) {
        return super.foo();
    }
}

// Inheritance must be ordered from “most base-like” to “most derived”.
// Swapping the order of A and B will throw a compilation error.
contract F is A, B {
    function foo() public pure override(A, B) returns (string memory) {
        return super.foo();
    }
}

Solidity는 다중 상속을 지원한다.
상속은 is 키워드를 사용해서 다른 컨트랙트를 상속할 수 있다.

contract A {
    function foo() public pure virtual returns (string memory) {
        return "A";
    }
}
contract B is A {
    // Override A.foo()
    function foo() public pure virtual override returns (string memory) {
        return "B";
    }
}

컨트랙트 B는 is 키워드를 사용하여 A 컨트랙트를 상속한다.
상속하는 컨트랙트에서 재정의하여 사용되는 함수는 virtual 키워드로 선언한다.
virtual 함수를 재정의 할 경우, virtual override 키워드를 사용한다.

contract D is B, C {
    // D.foo() returns "C"
    // since C is the right most parent contract with function foo()
    function foo() public pure override(B, C) returns (string memory) {
        return super.foo();
    }
}

is B C 상속 순서에 따라 super.foo()의 결과는 C가 나온다. super는 상속한 부모 컨트랙트를 참조하는 키워드

contract F is A, B {
    function foo() public pure override(A, B) returns (string memory) {
        return super.foo();
    }
}

상속은 기본 형태부터 (most base-like) 가장 파생된 순서 (most derived)형태여야 한다.
A와 B의 상속 순서를 변경하면 컴파일 에러가 발생한다.

Shadowing Inherited State Variables

url: https://solidity-by-example.org/shadowing-inherited-state-variables/

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.17;

contract A {
    string public name = "Contract A";

    function getName() public view returns (string memory) {
        return name;
    }
}

// Shadowing is disallowed in Solidity 0.6
// This will not compile
// contract B is A {
//     string public name = "Contract B";
// }

contract C is A {
    // This is the correct way to override inherited state variables.
    constructor() {
        name = "Contract C";
    }

    // C.getName returns "Contract C"
}

위에서 살펴본 것 처럼, 상속한 컨트랙트에서 부모의 virtual function을 오버라이딩해서 재정의할 수 있지만 변수는 불가능하다.
따라서,상속된 상태 변수를 올바르게 재정의하기 위해서 생성자 함수에서 값을 변경할 수 있다.

Calling Parent Contracts

url: https://solidity-by-example.org/super/

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.17;

/* Inheritance tree
   A
 /  \
B   C
 \ /
  D
*/

contract A {
    // This is called an event. You can emit events from your function
    // and they are logged into the transaction log.
    // In our case, this will be useful for tracing function calls.
    event Log(string message);

    function foo() public virtual {
        emit Log("A.foo called");
    }

    function bar() public virtual {
        emit Log("A.bar called");
    }
}

contract B is A {
    function foo() public virtual override {
        emit Log("B.foo called");
        A.foo();
    }

    function bar() public virtual override {
        emit Log("B.bar called");
        super.bar();
    }
}

contract C is A {
    function foo() public virtual override {
        emit Log("C.foo called");
        A.foo();
    }

    function bar() public virtual override {
        emit Log("C.bar called");
        super.bar();
    }
}

contract D is B, C {
    // Try:
    // - Call D.foo and check the transaction logs.
    //   Although D inherits A, B and C, it only called C and then A.
    // - Call D.bar and check the transaction logs
    //   D called C, then B, and finally A.
    //   Although super was called twice (by B and C) it only called A once.

    function foo() public override(B, C) {
        super.foo();
    }

    function bar() public override(B, C) {
        super.bar();
    }
}

solidity는 super 키워드를 사용해서 부모 컨트랙트를 호출할 수 있다.

    event Log(string message);

    function foo() public virtual {
        emit Log("A.foo called");
    }

    function bar() public virtual {
        emit Log("A.bar called");
    }

이벤트는 event 키워드를 사용해서 선언하고, emit 키워드를 통해 이벤트를 방출할 수 있다. 이벤트는 트랜잭션 로그에 기록되어 함수 호출을 추적하는데 유용하게 사용할 수 있다.

contract B is A {
    function foo() public virtual override {
        emit Log("B.foo called");
        A.foo();
    }

    function bar() public virtual override {
        emit Log("B.bar called");
        super.bar();
    }
}

컨트랙트 B를 배포하여 bar() 함수를 호출하면 아래와 같이 B, A 순으로 호출된다.

emit Log("B.bar called");
emit Log("A.bar called");

컨트랙트 D는 컨트랙트 B, C를 상속한다.

contract D is B, C {
    function foo() public override(B, C) {
        super.foo();
    }

    function bar() public override(B, C) {
        super.bar();
    }
}

이 때, D.foo()를 호출한 경우 아래와 같이 상속 순서에 따라 오버라이딩과 함수가 호출된다.

function foo() public override(B, C) {
        super.foo();
}

super 키워드는 가장 마지막에 상속받은 컨트랙트를 가르킨다. 따라서, B -> C 상속 순서에 따라 C.foo()가 호출된다.

Contract C is A {
    function foo() public virtual override {
        emit Log("C.foo called");
        A.foo();
    }
}

따라서, C.foo()가 실행되어 아래와 같은 결과가 로그로 출력된다.

contract D is B, C {
    function bar() public override(B, C) {
        super.bar();
    }
}

D.bar()는 super 키워드를 통해 가장 마지막에 상속받은 C.bar()를 호출한다.

contract C is A {
    function bar() public virtual override {
        emit Log("C.bar called");
        super.bar();
    }
}

C.bar()는 마찬가지로 B.bar()를 호출하고, B.bar()는 A.bar()를 호출하게 된다.

0개의 댓글