스마트 컨트랙트 배포는 왜 한 번의 전송으로 끝나지 않을까
토큰을 보관하는 볼트를 배포하려는데, 생성자에서 토큰 계약의 주소를 요구한다. 토큰 계약도 아직 없다면 무엇부터 보내야 할까? 먼저 토큰을 만들고, 그 결과 주소를 읽어 볼트의 입력으로 넣어야 한다. 두 번째 배포가 실패했다고 첫 번째 계약까지 없었던 일이 되는 것도 아니다.
계약 하나를 체인에 만드는 일과, 여러 계약을 연결해 서비스가 사용할 구성을 만드는 일은 범위가 다르다. 후자에는 어떤 코드를 어떤 순서로 배포하고, 앞 단계의 어떤 결과를 다음 입력으로 쓸 것인가라는 계획이 필요하다.
BXB에는 계약 템플릿을 단계로 구성하고, 실행별 입력과 결과를 기록하는 구조가 있다.
포함된 BasicERC20과 VaultToken을 골라 두 단계 계획을 구성해 보자.
아래는 소스에서 확인한 생성자와 단계별 처리 경로를 연결한 설명용 예제이며, 실제 네트워크에 배포해 얻은 실행 기록은 아니다.
1. 토큰 A를 만든 뒤, A를 보관하는 볼트 B를 만든다
이번 예제에서 A와 B는 사람의 지갑이 아니라 새로 만들 두 계약의 주소다. 배포에 사용할 지갑은 D, 대상은 하나의 EVM 네트워크 N이라고 부르겠다. 두 단계 모두 같은 네트워크에서 실행하며, D에는 각 배포의 가스비를 낼 네이티브 자산이 있다고 가정한다.
첫 계약은 기본 토큰인 BasicERC20이다. 생성자는 토큰 이름, 기호, 최초 소유자 주소를 받는다.
이름을 Example Asset, 기호를 AST로 정하고 최초 소유자에는 D의 주소를 넣는다.
배포가 정상적으로 끝나면 새 토큰 계약의 주소 A를 얻는다. 아직 토큰을 발행하는 mint는 호출하지 않는다.
두 번째 계약인 VaultToken은 어떤 ERC-20 토큰을 예치 대상으로 삼을지 생성자에서 받는다.
그 입력의 이름이 underlyingAsset이다. 여기에 A를 넣고 볼트 지분 토큰의 이름과 기호를 정하면,
볼트는 이후 예치·인출에서 A의 토큰을 다루도록 만들어진다.
ERC-4626의 asset 정의도 볼트가 관리하는 기초 토큰의 주소를 이 역할로 구분한다.
이 글은 그 주소 연결을 다루며 수익 발생이나 예치 비율을 검증하는 예제는 아니다.
VaultToken의 생성자는 전달받은 토큰 주소를 보관하고, 토큰의 소수 자릿수도 조회한다.
즉 A는 나중에 화면에 붙일 설명 문구가 아니라 B를 구성하는 실제 입력이다.
계약 이름이 같더라도 다른 네트워크나 다른 실행에서 얻은 주소를 넣으면 다른 구성을 만들 수 있다.
예제의 목표는 N에 A와 B가 생기고, B의 asset()을 조회하면 A가 나오는 것이다.
배포 중 토큰 발행이나 예치는 하지 않으므로 토큰과 볼트 지분의 총발행량은 0에서 출발한다.
배포가 끝났다는 이유로 볼트에 자산이 들어가거나 고객에게 지분이 생기는 것은 아니다. 배포 비용은 D의 네이티브 자산에서 별도로 지불한다.
2. 템플릿과 이번 실행은 서로 다른 기록이다
같은 두 계약을 개발 환경과 별도 운영 환경에 배포한다고 생각해 보자. 배포할 코드와 순서는 같아도 네트워크, 배포 지갑, 실제 주소와 실패 여부는 달라진다. 모든 정보를 하나의 템플릿에 덮어쓰면 어느 실행에서 만들어진 주소인지 구분하기 어렵다.
BXB는 이 정보를 템플릿, 계획, 실행, 단계로 나눈다. 다음 표의 결과는 별도 종류의 계약이 아니라 실행 단계에 남기는 출력이다.
| 기록 | 이 예제에서 맡는 역할 |
|---|---|
| 템플릿 | BasicERC20·VaultToken의 계약 자원을 가리킨다 |
| 배포 계획 | 토큰을 1단계, 볼트를 2단계에 놓는다 |
| 배포 실행 | 그 계획을 네트워크 N에서 수행할 이번 작업이다 |
| 실행 단계 | 단계별 생성자 입력·배포 지갑·진행 상태를 갖는다 |
| 단계 결과 | 배포 주소 또는 오류를 다음 판단에 남긴다 |
계약 자원에는 ABI와 배포용 바이트코드가 연결된다. ABI는 생성자에 어떤 형식의 값을 전달할지 설명하고, 바이트코드는 체인이 실행할 배포 프로그램이다. 템플릿을 등록했다는 것은 이 재료를 선택할 수 있다는 뜻이지, 이미 체인에 주소가 생겼다는 뜻은 아니다.
계획에는 단계 순서와 사용할 템플릿, 생성자 입력을 둔다. 실행을 만들면 그 계획을 바탕으로 단계별 기록을 만들고, 실행에는 대상 네트워크와 이름을 연결한다. 그 뒤 실제로 사용할 지갑과 생성자 값을 확인해 단계별 배포를 요청한다. 같은 계획을 다시 사용하는 것과 이미 성공한 실행의 결과를 다시 쓰는 것은 이렇게 구별된다.
또 하나 구분할 값은 배포 지갑 D와 생성자의 initialOwner다.
이 예제에서는 같은 주소로 정했지만, 전자는 거래를 서명하고 비용을 내는 주체이고 후자는 토큰 계약의 권한을 받을 주소다.
BXB의 이 EVM 배포 경로는 ADMIN 역할 지갑인지 검사한다. 그것만으로 생성자에 넣은 소유자 주소의 업무상 적절성까지 결정되는 것은 아니다.
3. 아직 모르는 주소는 앞 단계의 결과로 적는다
계획을 작성할 때 A의 실제 주소는 아직 없다. 그렇다고 임의의 주소를 넣어 두고 배포 직전에 기억에 의존해 바꾸는 것은 연결 실수를 만들기 쉽다. 이 자리에 “이번 실행의 1단계에서 나온 주소”라는 참조를 남길 수 있다.
BXB의 생성자 입력 해석 경로는 ${step:N} 표기를 지원한다.
N은 같은 배포 실행 안의 단계 순서다. 따라서 볼트의 입력은 다음과 같이 작성할 수 있다.
{
"underlyingAsset": "${step:1}",
"name": "Example Vault Share",
"symbol": "vAST"
}
이 JSON은 볼트 생성자 입력을 설명하기 위한 것으로, 배포 요청 전체는 아니다.
두 번째 단계를 실행할 때 관리 서비스는 같은 실행에 속한 1단계 기록을 찾고,
그곳의 배포 주소로 ${step:1}을 바꾼다. 체인에 보내는 생성자에는 이 표기 대신 실제 A의 주소가 들어간다.
단계를 찾을 수 없거나 결과 주소가 비어 있으면 주소 참조를 해결할 수 없다.
이 표기는 자동으로 계약의 의존 관계를 추론하는 기능과는 다르다. 사용자가 계획에서 올바른 순서를 정하고, B가 참조해야 할 단계 번호도 지정해야 한다. 같은 실행 안에서 주소를 찾는다는 규칙이 A의 업무상 의미까지 증명하지는 않는다. 첫 단계에 어떤 토큰 템플릿과 설정을 넣었는지도 함께 확인해야 한다.
생성자는 계약을 만들 때 한 번 실행된다. Solidity의 계약 생성 설명에 따르면 생성자 인자는 배포 코드 뒤에 ABI 형식으로 인코딩되어 전달된다. 따라서 입력을 바꾸는 시점은 두 번째 배포를 서명하기 전이다. 계획 화면의 값을 나중에 고쳤다고 이미 만들어진 B의 설정이 바뀌는 것은 아니다.
4. 선행 단계 확인에서 주소 기록까지
두 번째 단계의 배포 버튼을 누르면 주소 치환부터 무조건 시작하는 것은 아니다.
관리 서비스는 현재 단계가 이미 완료됐는지 확인하고, 순서가 앞선 단계들이 모두 COMPLETED인지 검사한다.
이 예제에서는 1단계가 먼저 완료돼야 2단계의 요청이 진행된다.
배포 엔진에도 다음 순서의 단계인지 확인하는 조건이 있다.
선행 조건을 통과하면 생성자 참조를 실제 값으로 바꾸고, 선택한 지갑과 함께 개별 단계 배포를 요청한다.
엔진은 사용할 생성자 입력과 지갑을 단계에 기록한 뒤 계약 배포 경로를 호출한다.
EVM 배포 서비스는 등록된 ABI로 입력을 인코딩하고, 배포 바이트코드와 이어 붙여 거래를 만든다.
일반 함수 호출과 달리 새 계약을 만드는 거래에는 기존 수신 계약의 to 주소가 없다.
이 구분은 Ethereum의 거래 유형 설명에서도 확인할 수 있다.
정상 경로에서 엔진은 서명된 거래를 보내고 거래 영수증인 receipt를 기다린다. 배포 서비스는 그 receipt의 계약 주소로 계약 등록 기록을 만들고, 단계 결과에도 주소를 남긴다. 이때 다음 단계가 사용하는 값은 거래 해시가 아니라 배포된 계약 주소다. 해시는 “어느 거래를 조회할 것인가”에 답하고, 주소는 “어느 계약을 연결할 것인가”에 답한다.
단계 기록과 거래 기록도 쓰임이 다르다. 단계에는 해석된 생성자 입력, 사용한 지갑, 주소와 오류를 남기고, 거래 기록은 제출과 receipt 조회를 따라가는 근거가 된다. 운영자가 나중에 B의 설정을 확인할 때는 결과 주소 하나뿐 아니라 그 B를 만든 입력과 네트워크까지 함께 봐야 한다.
5. 두 번의 배포가 끝났을 때 무엇을 얻었는가
정상적으로 각 요청을 마치고 상세 정보를 다시 읽었다면, 두 단계는 다음과 같은 결과를 갖는다. 아래는 설명용 예제의 단계별 결과이며 전체 실행의 표시 하나를 온체인 성공 증명으로 삼은 표는 아니다.
| 확인 시점 | 토큰 단계 | 볼트 단계 |
|---|---|---|
| 실행 준비 | PENDING, 주소 없음 | PENDING, 주소 없음 |
| 토큰 배포 후 | COMPLETED, A | PENDING, 주소 없음 |
| 볼트 배포 후 | COMPLETED, A | COMPLETED, B |
업무에서 필요한 최종 결과는 “완료 두 건”보다 구체적이다.
N에 토큰 A와 볼트 B가 있고, B가 사용하는 기초 토큰 주소가 A여야 한다.
단계에 남은 생성자 입력을 확인하고, 배포된 B의 asset()을 읽어 연결을 확인할 수 있다.
이 조회는 예제의 결과를 확인하는 방법이지, 현재 단계 실행기가 모든 배포 뒤 자동으로 수행한다고 주장하는 절차는 아니다.
receipt 역시 의미를 구분해서 읽어야 한다. Ethereum JSON-RPC의 receipt 설명은 계약 생성 주소와 실행 상태를 별도 항목으로 제공한다. 거래 해시를 받았다는 사실만으로 생성 성공을 정할 수 없고, receipt 확인과 조직이 요구하는 블록 확정 수준도 구분해야 한다. 단계 상태는 이 확인을 이어 가는 운영 기록이다.
이 예제에서는 두 번의 배포 외에 발행·예치·인출 거래를 보내지 않는다. 정상적으로 끝나도 고객의 토큰 잔액이나 볼트 지분이 증가하지 않는다. 두 계약의 총발행량은 여전히 0이고, D는 두 배포의 실제 거래 비용을 부담한다.
이제 A와 B의 ABI와 주소를 어떤 업무 API에 연결할지 정할 수 있다. 컨트랙트 하나를 REST API로 운영하기까지에서 다룬 ABI와 실행 대상 주소의 연결은 이 지점부터 이어진다. 계약을 배포하는 일, API를 등록하는 일, 그 API로 고객의 예치를 실행하는 일은 각각 별도의 결과를 가진다.
6. 두 번째 단계가 멈췄다고 첫 번째가 사라지지 않는다
A를 성공적으로 만든 뒤, B의 입력에서 실수로 ${step:9}를 참조했다고 하자.
이번 실행에는 9단계가 없으므로 주소 해석에서 멈춘다.
이 경우 B의 배포 거래는 아직 제출되지 않았고, 앞서 만든 A는 그대로 있다.
확인할 것은 2단계의 참조 번호와 저장된 입력이다. B가 생겼다고 표시하거나 A를 다시 만들 이유는 없다.
이처럼 제출 전에 끝난 오류까지 언제나 같은 단계 상태로 기록된다고 가정해서는 안 된다.
반면 B의 거래를 보낸 뒤 응답을 기다리다가 연결이 끊기는 경우는 다르다. 화면에서 실패를 봤거나 서버가 receipt를 받지 못했어도, 체인에서는 B가 만들어졌을 수 있다. 먼저 기존 거래 해시로 결과를 조회하고, 계약 주소와 생성자 입력을 이번 실행 기록에 대조해야 한다. 아직 결과를 모른다면 “배포되지 않았다”가 아니라 “결과를 확인해야 한다”는 상태로 판단해야 한다.
결과를 확인하지 않은 채 새 배포 거래를 만들면, 같은 이름과 코드를 가진 별도 계약을 추가로 만들 수 있다. 앞선 B와 새 B 중 어느 것을 서비스가 사용할지도 다시 결정해야 한다. 송금 요청이 타임아웃되면 다시 보내도 될까에서 구분한 결과 조회와 새 거래 생성의 차이가 배포에도 적용된다. 단계를 다시 누르는 행동이 이전 거래의 조회나 동일 실행의 안전한 재개와 같지는 않다.
BXB의 단계 처리에는 진행·완료·실패와 오류 내용을 기록하는 경로가 있다. 그러나 이 사실을 모든 실패를 자동 복구하거나 중단 위치에서 안전하게 재시작한다는 보장으로 넓혀 읽어서는 안 된다. 운영자는 제출 전 오류인지, 제출 후 결과가 불명확한지, 체인 실행이 실패한 것인지 구분하고, 현재 주소와 단계 기록이 맞는지 확인한 뒤 입력 수정이나 별도 복구 절차를 결정해야 한다.
비용도 경우별로 다르다. 앞의 잘못된 참조처럼 B의 거래를 보내기 전에 멈췄다면 B 배포의 온체인 수수료는 없다. 이미 처리된 거래가 있다면 receipt에서 그 거래가 실제로 사용한 가스를 확인해야 한다. 앞서 성공한 A의 비용은 어느 경우에도 계획의 실패 표시만으로 반환되지 않는다. 별도 거래로 만든 A와 B를 하나의 DB 트랜잭션처럼 함께 되돌리는 자동 rollback을 전제할 수 없기 때문이다.
7. 배포 계획은 다음에 무엇을 할지 판단할 근거다
이미 같은 네트워크에 쓸 수 있는 토큰이 있다면 그것을 다시 배포하지 않는 구성도 생각할 수 있다. 다만 기존 주소를 재사용하려면 주소의 네트워크, 계약 코드와 설정, 이번 계획에서 요구하는 역할이 맞는지 확인해야 한다. 재사용은 “배포를 건너뛰었다”는 표시만으로 완성되지 않고, 그 주소를 다음 단계에 전달하는 규칙까지 연결돼야 한다.
BXB 배포 엔진에는 공유 구성요소에 기존 주소를 전달받으면 배포를 생략하고 그 주소를 기록하는 분기가 있다. 이번 글은 그 분기를 콘솔 전체에서 검증된 재사용 절차로 확장하지 않고, 두 계약을 새로 만드는 단계별 경로에 한정했다. 모든 단계를 한 번에 자동 배포하는 기능도 현재 확인한 단계별 실행과 구분한다. 독자가 이 예제에서 가져갈 동작은 한 번 누르면 전체가 알아서 완성된다는 약속이 아니라, 앞 단계의 결과를 확인하고 다음 입력으로 연결하는 방법이다.
여러 계약이 언제나 여러 거래를 요구하는 것은 아니다. 별도 팩토리 계약 안에서 여러 계약을 생성하는 설계도 가능하다. 여기서 살펴본 것은 계약별 배포 거래를 순서대로 보내는 경로이므로, 한 거래의 원자성을 계획 전체에 적용할 수 없다는 뜻이다. 구성에 맞는 배포 방식과 실패 처리 단위를 먼저 정해야 한다.
이번 계획의 출력은 같은 네트워크의 A와 B, 그리고 B가 A를 사용하도록 만들어졌다는 연결이다. 템플릿은 무엇을 만들지 제공하고, 계획은 순서를 정하며, 실행 단계는 그 순서에서 실제로 일어난 일을 남긴다. 이 구분이 있어야 두 번째 배포가 멈췄을 때도 이미 만들어진 것을 확인하고 다음 행동을 정할 수 있다.