이더리움 스마트 컨트랙트 개발 주의사항

Eun Woo Nam
RayonProtocol
Published in
7 min readAug 21, 2018

스마트 컨트랙트를 블록체인에 배포하기 전에는, 많은 검증과 테스트를 거쳐야 한다. 왜냐하면 우선 블록체인 위에 배포가 된다면 수정이 어려우며, 한 번 취약점이 발견되어 해킹이 발생할 경우 경제적인 손실이 매우 크기 때문이다.

때문에, 토큰 마켓(https://tokenmarket.net/)이나 오픈 제플린(https://openzeppelin.org/)과 같은 프로젝트에는 검증이나 사용자의 실수를 방지하는 코드를 삽입한다. 아래의 예제를 통해 토큰 스마트 컨트랙트를 배포할 때 주의해야 할 사항들에 대해 짚어보도록 하자.

다중 상속 시 순서 주의

solidity는 다중 상속이 가능하다. 이 때, 상속하는 부모를 선언해준 순서에 따라 생성자의 순서가 정해진다. 따라서 아래의 코드를 보았을 때, A 컨트랙트는 순서대로 B, C 컨트랙트를 상속받기 때문에, A 컨트랙트의 생성자에서 setFee()를 실행할 경우, C 컨트랙트 코드가 B보다 나중에 실행되어서 fee 값은 5가 된다.

테스트 코드

스마트 컨트랙트가 작동하는지 확인 하기 위해서는, 미리 테스트 코드를 삽입하는 것이 좋다. 다음 setOwnerTestValue 함수는 ownerTestValue라는 변수에 값을 세팅하게 되어 있는데, ownerTestValue를 읽었을 때 변경되지 않았다면 스마트 컨트랙트가 작동하지 않는다는 뜻이다.

버전 선언 규칙

solidity는 상단에 pragma 키워드로 버전을 명시한다. 이 때, 보통 ‘^’ 문자로 명시된 버전 이상을 허용하는데, 이 경우 업그레이드 된 버전으로 적용이 되었을 때 문제가 발생할 수 있다. 따라서 상위 버전을 허용하기 보다는 특정 버전에서만 동작하도록 선언하는 것이 좋다.

// bad
pragma solidity ^0.4.4;


// good
pragma solidity 0.4.4;

접근 제어자 설정

스마트 컨트랙트 함수는 public, private, internal, external의 4가지의 접근 제어자(Access Modifier) 중 1개에 해당하게 되어 있다.

  • public: 어디에서나 호출할 수 있다.
  • external : 해당 컨트랙트 내부에서 호출이 불가능하며, 외부에서만 호출해야 한다.
  • private: 해당 컨트랙트 내에서만 함수가 호출될 수 있다.
  • internal: 해당 컨트랙트와 derived 컨트랙트만 호출 할 수 있다.

실수를 방지하기 위해 최소한의 접근 권한만 갖도록 접근제어자를 정하고 꼭 명시하는 것이 좋다.

Storage 변수 사용 자제

로컬 변수인 memory와 달리 storage는 블록체인에 직접 데이터를 기록하고, 많은 가스 비용이 발생한다. 그러므로 무분별하게 storage 변수를 사용하거나, for 문 안에서 storage 변수를 불필요하게 조작하는 등의 실수를 하지 않아야 한다.

memory 명시

function 내부에 변수를 선언할 경우, 기본적으로 storage로 저장된다. 따라서 memory 키워드를 반드시 명시해야한다.

이 때, 함수안에서 memory로 선언된 변수는 함수가 완료되었을 때가 아닌, 트랜잭션이 완료 되었을 때 사라지게 된다.

오버/언더플로우

수학 연산의 결과로 각 데이터 타입이 가질 수 있는 범위를 벗어나는 오버/언더플로우가 발생하지 않도록 유의해야 한다. 그렇지 않으면 이를 활용한 공격이 발생할 수 있다.

오픈 제플린에서 제공하는 SafeMath를 사용하도록 하자. 만약 SafeMath에서 계산 과정 중 오더/언더플로우가 발생하는 상황이 되면 예외가 발생되고, 실행은 revert 되어서 안전하다.

함수의 Reenter 방지

외부 스마트 컨트랙트 함수를 호출하는 경우 그 함수에서 호출한 함수를 재귀 호출하게 되어 있다면 반복적으로 토큰을 전송하는 등 예상지 못한 결과가 발생할 수 있다.

이를 방지하기 위해서 위와 같이 nonReentrant라는 modifier를 구현할 수 있다. nonReentrant가 포함되면 reentrancy_lock이라는 플래그 변수로 lock(잠금) 상태로 전환된다. modifier가 포함된 함수가 실행되는 도중에 해당 함수를 재귀 호출할 수 없게 막아준다.

거래소 계정에서 이더 전송 막기

거래소 계정에서 직접 이더를 전송해서, 그 계정으로 토큰을 받았다고 가정해보다.

거래소가 제공한 계정은 비밀키를 거래소가 관리하고 외부에 제공해주지 않기 때문에 사실상 제어권이 거래소에 있고 보통 특정 코인용으로만 사용하도록 되어 있다.

따라서 입금된 토큰 인출을 거래소에 요청을 하더라도, 절차가 복잡하고 보안상의 문제가 발생할 수 있기 때문에, 돌려받지 못할 가능성이 크다.

사용자 입장에서 이더 전송 과정이 복잡하지만, 스마트 컨트랙트 상에서 이 문제를 막기 위해서는 fallback 함수에 payable을 제거하거나, 예외를 발생시켜서 fallback으로 이더를 받지 않고, 이더 수신 전용 함수를 통해 이더를 수신하면된다. 이렇게 하면 거래소 계정에서 호출할 수 없어서 이 문제는 해결된다.

스마트 컨트랙트로 잘못 보내진 토큰 전송

사용자가 실수로 스마트 컨트랙트에 토큰을 전송하는 일이 발생할 수 있다. 이에 대처하기 위해 CanReclaimToken 코드를 상속받는다. 이렇게 하면 reclaimToken이라는 함수를 호출함으로써 스마트 컨트랙트가 보유하고 있는 토큰을 외부로 전송할 수 있게 된다.

여기서 이더리움 전송과 달리 token을 파라미터로 받는 이유는, 이더는 컨트랙트 내부에 있지만 토큰은 다른 컨트랙트에 있기 때문이다.

컨트랙트의 오너를 안전하게 변경

컨트랙트의 오너를 변경할 때 새로운 오너 주소를 잘못 입력하여 오너쉽을 영원히 잃어버리는 경우가 발생할 수 있다. 따라서 오너쉽 변경의 절차를 2단계로 나누는 것이 좋다.

아래 코드는 OpenZeppelin의 claimable.sol이다. transferOwnership을 호출하면 pendingOwner 변수에 새로 변경되는 오너가 저장되며, 새로운 오너의 계정으로 claimOwnership을 호출해야 비로소 소유권이 최종 변경된다. 새로운 오너가 이를 호출하지 못하면 transferOwnership를 다시 호출하여 다른 오너로 변경을 다시 시도할 수 있다.

스마트 컨트랙트 제거 시 토큰 인출

스마트 컨트랙트를 제거해야 하는경우에는, 포함된 이더는 주어진 계정으로 전송된다. 하지만 토큰의 경우에는 해당 스마트 컨트랙트 외부에 기록되므로 일일이 전송하지 않으면 영영 사용하지 못하게 된다.

따라서, 제거 시 토큰 스마트 컨트랙트 목록을 받아서 일일이 보유한 토큰을 꺼내야 한다.

사용자 정의의 값 읽어오기

스마트 컨트랙트 내부에 사용자가 정의한 구조체나 map, 또는 이들의 배열값은, 일반적인 public view 함수를 호출하여 한꺼번에 읽어 올 수 없다. 따라서 다음과 같은 방법을 통해 클라이언트에 반환이 가능하다.

구조체 값 반환하기

  • 단일 구조체 반환 : 클라이언트에서 구조체의 값을 읽기 위해서는 스마트 컨트랙트에서 구조체의 property들을 개별적으로 반환해야한다.
  • 구조체 배열 반환: 전체 구조체 배열을 반환할 수는 없다. 특정 인덱스의 구조체에 대해 단일 구조체를 반환하는 방식을 제공하고 클라이언트는 구조체 배열의 길이만큼 반복해서 호출해야 한다.

Map의 값 반환하기

  • map에서 특정 key 값의 value 반환: 해당 map의 key값으로 value를 찾아 반환한다. 이 때 value가 구조체라면, property들을 개별적으로 반환해야 한다.
  • map의 전체 value 반환: map 내의 key를 배열로 변환할 방법이 없어서, 일괄적으로 반환받을 방법은 없다. 따라서 key 목록을 아래의 userAddressList와 같은 별도의 배열에 저장하고 있어야 한다. 클라이언트에서는 key 마다 반복적으로 호출해서 값을 읽어가야 한다.

이상으로 스마트 컨트랙트 개발 시 주의해야 할사항에 대해서 알아보았다.

해킹 사례가 점차 늘면서, 이를 방어하는 코드도 점점 견고해지고 있다. 성공적인 블록체인 서비스를 위해, 신중하게 보안 점검 및 테스트를 진행하며 개발하도록 하자.

--

--