본문 바로가기
Blockchain

Smart contract의 code size를 줄이는 방법

2023. 4. 10.

EVM에서 스마트 컨트랙트로 작성 가능한 크기는 약 24.000 KiB로 한계가 있습니다. solidity optimizer의 runs 옵션을 이용해 배포할 때의 코드 크기를 어느 정도 줄일 수 있기는 하지만, 이 역시 무한정으로 줄일 수 있는 것이 아니기에 이 외에도 어떻게 스마트 컨트랙트의 코드 크기를 줄일 수 있을지 고민하는 것이 중요합니다. 

 

해당 포스팅은 이전 포스팅 [Solidity Optimizer의 runs 옵션 이해하기]와 이어집니다.

 

Solidity Optimizer의 runs 옵션 이해하기

스마트 컨트랙트를 작성한 후에 컴파일/배포하다 보면, 컨트랙트가 너무 크다는 경고를 마주할 때가 있습니다. Warning: 1 contracts exceed the size limit for mainnet deployment (24.000 KiB). 1 contracts exceed the size

him-down.tistory.com

 


컨트랙트 로직 분리하기

하나의 큰 스마트 컨트랙트를 여러 개의 작은 단위의 스마트 컨트랙트로 분리하는 방법입니다. 즉, 비슷한 기능 단위로 나누거나, 접근 권한 단위로 나누는 등 구현하고자 하는 컨트랙트를 위해 아키텍처를 설계하고 분리합니다. 이후 필요하다면 한 컨트랙트에서 다른 컨트랙트의 함수를 호출하여 사용할 수 있습니다.
(ex. A contract → B contract의 함수 호출)

 

혹은 여러 컨트랙트에서 공통적으로 사용할 수 있는 로직은 library로 기능을 분리할 수 있습니다. Library로 코드 로직을 옮기면 컨트랙트의 코드 크기는 줄어들고, 컨트랙트에서 외부 library 함수를 호출하여 로직은 그대로 사용할 수 있습니다.

 

Proxies

Proxy 패턴을 이용해서 코드 로직을 분리하여 코드 사이즈를 줄이는 방법입니다. 간단하게 설명해 보자면, Proxy 패턴에서는 코드 로직을 구현한 컨트랙트와 상태 변수를 저장할 컨트랙트를 분리합니다. 

 

 

Proxy Contract의 함수에서는 에서는 delegate call을 통해서 Logic Contract의 함수를 호출하여 실제 함수에서 실행할 로직은 logic contract의 것을 따르고, 해당 로직에 따라 변화되는 storage는 proxy contract의 storage를 사용하게 됩니다. 이렇게 실제 실행할 로직은 다른 컨트랙트로 옮겨두기 때문에 코드 크기를 줄일 수 있습니다. 또한, 

 

함수 줄이기

External 함수는 편의성을 위해 view function의 용도로 많이 추가합니다만, 불필요한 함수들까지 과도하게 선언되어 있다면 사용하지 않을 함수들은 줄이는 것이 좋습니다. 또한 internal 함수 역시 적은 횟수로 사용되는 경우에는 그냥 inline 처리하여 코드 크기를 줄일 수 있습니다.

 

변수 선언 자제하기

함수 안에서 지역 변수를 불필요하게 많이 선언하는 것도 코드 사이즈에 영향을 줄 수 있습니다. 예를 들어, 아래와 같은 코드를

 

function get(uint id) returns (address,address) {
    MyStruct memory myStruct = myStructs[id];
    return (myStruct.addr1, myStruct.addr2);
}

 

다음과 같이 변경한다면 0.28kb가 줄어듭니다.

 

function get(uint id) returns (address,address) {
    return (myStructs[id].addr1, myStructs[id].addr2);
}

 

다만, 개인적으로 너무 변수 사용을 꺼리다 보면 코드 가독성이 떨어지거나, 다른 컨트랙트의 값을 가져오는 경우에는 너무 잦게 다른 컨트랙트와의 상호작용이 일어나므로 적당히 조절해서 쓰는 것이 좋다고 생각합니다. 

 

Error 메시지 줄이기 & Cusom error 사용하기

Require문을 통해 트랜잭션을 revert 시킬 때, 어떤 오류 때문에 트랜잭션이 revert되었는지 error 메시지(string)를 전달합니다. 이때 너무 긴 문자열은 코드 크기를 많이 증가시키기 때문에 짧은 약어를 사용하면 코드 크기를 줄일 수 있습니다. 

 

require(msg.sender == owner, "Only the owner of this contract can call this function");

require(msg.sender == owner, "OW1");

 

혹은 require문을 사용하여 error 메시지를 전달하는 것 대신에, custom error를 통해 어떤 오류가 발생한 것인지를 전달할 수도 있습니다. Custom error는 function처럼 selector로 ABI-encode 되기 때문에 코드 사이즈를 줄일 수 있습니다. (해당 내용은 Solidity 0.8.4 버전부터 업데이트 되었습니다)

 

error Unauthorized();

if (msg.sender != owner) {
    revert Unauthorized();
}

 

ECT 

이 외에도 코드 크기에 영향은 작지만 시도해 볼 만한 여러 방법들이 더 있습니다. 함수 매개 변수로 구조체를 넘겨주는 것을 피하거나,

 

function _get(MyStruct memory myStruct) private view returns(address,address) {
    return (myStruct.addr1, myStruct.addr2);
}

function _get(address addr1, address addr2) private view returns(address,address) {
    return (addr1, addr2);
}

 

modifier 대신에 함수를 대신 사용하거나,

 

modifier checkStuff() {}
function doSomething() checkStuff {}

function checkStuff() private {}
function doSomething() { checkStuff(); }

 

함수마다 적절한 visibility를 사용하는 것 등이 도움이 될 수 있습니다.

 


프로젝트를 진행하면서 코드 크기를 줄이기 위해 많은 시도들을 해봤었는데요. 제일 효과가 좋았던 것은 당연히 function에서 중복되는 내용을 모두 library로 분리하고 코드를 단순화 한 부분이었습니다. 이 외에도 의외로 효과가 있었던 것은 custom error를 사용하는 것이었네요. Error message를 줄이는 것보다는 비슷한 오류 사항들끼리 모아 하나의 error를 만들고 컨트랙트의 require문을 모두 custom error로 바꾸어 주니 경험상 코드 사이즈가 꽤 많이 줄었었습니다. 요즘에는 컨트랙트 개발을 진행하면서 점차 내용이 방대해지니 처음부터 좋은 컨트랙트 아키텍처를 짜는 것이 중요하다는 것을 많이 느끼네요. 다음에는 위에서 간략히 설명한 proxy pattern을 더 자세히 다뤄보겠습니다.  

 

 

댓글