실전 스마트 컨트랙트: 간단한 도메인 레지스트리 개발과 배포

네오 블록체인·2025년 9월 4일

Neo N3

목록 보기
2/8

실제 스마트 컨트랙트: 간단한 도메인 등록기 구축 및 배포

안녕하세요! 현재 이 튜토리얼의 명령줄 버전을 보고 계시네요. 혹시 그래픽 사용자 인터페이스가 더 편하시다면 이 튜토리얼의 UI 버전을 확인해보세요.

이 튜토리얼은 Neo N3 Fungible Token Sample Contract 프로젝트에서 영감을 받아 만들어졌습니다.

오늘은 Neo Blockchain 툴킷을 사용해서 실제 Neo 스마트 컨트랙트를 만들어보는 시간을 가져볼게요. 블록체인 기반 도메인 등록 시스템을 직접 구축해보겠습니다!

사전 요구사항

시작하기 전에 몇 가지 도구들을 준비해야 해요. 다음 소프트웨어들이 필요합니다:

설치 방법이 궁금하시다면 Quick Start video 1을 참고해보세요. 6분짜리 영상으로 N3 스마트 컨트랙트 개발 환경을 설정하는 방법을 보여줍니다.

C# 확장과 컴파일러 설치 방법은 Quick Start video 4에서 확인할 수 있습니다. 11분 영상으로 설정 과정과 간단한 예제를 보여줍니다.

다음 명령어로 설치하시면 됩니다:
$ dotnet tool install Neo.Express -g

.NET SDK가 먼저 설치되어 있어야 해요!

Linux나 MacOS를 사용하신다면 추가 종속성이 필요할 수 있습니다. 자세한 내용은 Quick Start video 1이나 Neo Express 문서를 참고해주세요.

모든 소프트웨어가 무료이고 크로스 플랫폼이라서 Windows, Mac, Linux 어디서든 따라하실 수 있어요!

개인 블록체인 생성

자, 이제 본격적으로 시작해볼까요? 먼저 프로젝트를 위한 새로운 폴더를 만들어보겠습니다. registrar라는 이름으로 폴더를 만들고, 도메인 등록 서비스와 관련된 모든 파일들을 여기에 저장할 예정이에요.

$ mkdir registrar
$ cd registrar

이제 Neo Express를 사용해서 개인 블록체인을 생성해보겠습니다. 이렇게 하면 실제 GAS를 사용하지 않고도 개발 중에 컨트랙트를 배포하고 테스트할 수 있어요!

$ neoxp create
Created 1 node privatenet at
/Users/neo/registrar/default.neo-express
Note: The private keys for the accounts in this file are are *not* encrypted.
Do not use these accounts on MainNet or in any other system where security is a concern.

⚠️ 보안 주의사항: default.neo-express 파일이 생성되었는데, 이 파일에는 개인 키가 포함되어 있어요. 하지만 이 키들은 암호화되지 않았기 때문에 로컬 테스트용으로만 사용하세요!

이제 개인 블록체인을 실행해보겠습니다:

$ neoxp run

터미널에서 Neo Express의 콘솔 출력을 보실 수 있을 거예요. 약 15초마다 새로운 블록이 추가되는 것을 확인할 수 있습니다. Ctrl+C를 누르거나 터미널을 닫으면 블록체인이 중지됩니다. 지금은 Neo Express를 실행 상태로 두고, 다음 단계를 위해 새 터미널을 열어주세요!

지갑 생성

이제 개인 블록체인과 함께 사용할 지갑을 만들어보겠습니다. 이 지갑은 스마트 컨트랙트를 블록체인에 배포할 때 사용할 거예요. 처음에는 도메인 등록을 무료로 만들어서 소유자가 초기 배포 후에는 특별히 관여하지 않도록 할 예정입니다. (나중에 컨트랙트를 개선해서 도메인 등록에 NEO나 GAS로 수수료를 받고, 소유자가 그 수수료를 회수할 수 있도록 만들 수도 있겠죠!)

$ neoxp wallet create owner
owner
NigW5fvwHWMEzgoeasQTA6fdktXLe8tCsa
Note: The private keys for the accounts in this wallet are *not* encrypted.
Do not use these accounts on MainNet or in any other system where security is a concern.

좋아요! 스마트 컨트랙트 소유자를 위한 지갑이 생성되었습니다. 하지만 아직 이 지갑에는 자산이 없어요. Neo 블록체인에 스마트 컨트랙트를 배포하려면 수수료가 필요합니다. 수수료는 컨트랙트 크기에 따라 달라지지만 항상 GAS로 지불해야 해요.

Neo Express 인스턴스에는 "genesis"라는 특별한 지갑이 있습니다. 이 지갑은 처음에 NEO와 GAS(Neo 블록체인의 두 가지 네이티브 자산)의 전체 공급량을 받아요. genesis 지갑에서 owner 지갑으로 GAS를 조금 전송해보겠습니다.

$ neoxp transfer 100000 GAS genesis owner
Transfer Transaction 0x0acc8cab2dd88a3e4c73284a71edc94c77a03db62e46950ee6d42b60e22a9b9d submitted

Alice와 Bob 소개

이제 도메인 등록과 전송을 실험해볼 수 있도록 두 개의 지갑을 더 만들어보겠습니다. alicebob이라는 이름으로 지갑을 만들 거예요. (프로토콜을 설명할 때 첫 번째와 두 번째 참가자를 Alice와 Bob이라고 부르는 게 관례거든요!)

지갑 생성 과정은 위에서 owner 지갑을 만들 때와 똑같습니다—지갑 이름만 다르게 하면 돼요. 그리고 genesis 지갑에서 Alice와 Bob에게도 GAS를 전송하는 것을 잊지 마세요! (우리가 만들 등록 컨트랙트를 호출하려면 GAS가 필요하거든요)

만들어진 지갑들은 .neo-express 구성 파일 안에 저장됩니다. 파일을 열어보시면 다음과 같은 지갑 항목들을 보실 수 있을 거예요 (키와 주소는 다를 수 있어요):

새 지갑이 포함된 구성 파일

컨트랙트 생성

드디어 스마트 컨트랙트 코드를 작성할 시간이 왔네요! 🎉

Visual Studio Code를 사용해서 스마트 컨트랙트 코드를 작성해보겠습니다. 새 VS Code 창을 열고 registrar 폴더를 열어주세요. 이 폴더에는 Neo Express가 개인 블록체인 구성을 저장하기 위해 만든 default.neo-express 파일이 들어있을 거예요.

툴바에서 N3 아이콘을 클릭해서 N3 Visual DevTracker를 열어보세요:

Quick Start 패널에서 "Create a new contract" 버튼을 클릭해주세요:

(또는 Blockchains 패널의 컨텍스트 메뉴에서 "Create contract" 메뉴 옵션을 선택하셔도 됩니다.)

프로그래밍 언어를 선택하라고 하면 csharp를 선택하세요.

컨트랙트 이름을 입력하라고 하면 Registration을 입력해주세요. RegistrationContract.cs라는 새 파일이 생성되고 열릴 거예요—이게 바로 우리의 스마트 컨트랙트 코드입니다! 예제 코드로 미리 채워져 있지만, 곧 대부분을 제거하고 우리만의 코드로 바꿀 예정이에요…

VS Code의 Explorer 패널을 보시면 다양한 파일들이 생성된 것을 확인할 수 있어요:

RegistrationContract.csproj 파일은 MS Build C# 프로젝트 구성 파일이에요. .NET SDK 도구에게 프로젝트를 어떻게 빌드할지 알려주는 역할을 합니다.

tasks.json 파일은 Visual Studio Code 구성 파일로, VS Code 내에서 코드를 빌드할 수 있게 해줍니다. VS Code가 이미 샘플 코드를 빌드해놨고, 빌드로 생성된 파일들이 Registration/bin/debug/net5.0 폴더에 들어있을 거예요. 코드를 수정한 후에는 VS Code의 "Terminal" 메뉴에서 "Run build task…" 옵션을 선택해서 컨트랙트를 다시 빌드할 수 있습니다.

명령줄로 빌드하는 것을 선호하신다면, tasks.json 파일을 삭제하고 빌드할 때마다 Registration 폴더에서 dotnet build 명령을 실행하시면 됩니다:

$ cd Registration/
$ dotnet build
Microsoft (R) Build Engine version 16.8.3+39993bd9d for .NET
Copyright (C) Microsoft Corporation. All rights reserved.

Determining projects to restore...
All projects are up-to-date for restore.
RegistrationContract -> /Users/neo/registrar/RegistrationContract/bin/Debug/net5.0/RegistrationContract.dll

Build succeeded.
0 Warning(s)
0 Error(s)

Time Elapsed 00:00:01.21

이제 샘플 코드를 제거하고 컨트랙트 메타데이터를 채워넣은 다음, 우리만의 스마트 컨트랙트 코드를 작성해보겠습니다!

RegistrationContract.cs 파일에는 하나의 클래스가 들어있어요. 이 클래스는 RegistrationContract라고 불리며 SmartContract 클래스(Neo.SmartContract.Framework 패키지에서)를 확장해서 스마트 컨트랙트임을 나타냅니다. 이 클래스에는 컨트랙트와 함께 N3 블록체인에 배포될 메타데이터를 제공하는 다양한 속성들이 있어요:

[DisplayName("YourName.RegistrationContract")]
[ManifestExtra("Author", "Your name")]
[ManifestExtra("Email", "[[email protected]](/cdn-cgi/l/email-protection)")]
[ManifestExtra("Description", "Describe your contract...")]

이것들을 실제 값으로 바꿔보겠습니다…

DisplayName은 지갑 소프트웨어나 다른 도구에서 컨트랙트를 참조할 때 사용됩니다. 보통 자신의 식별자(예: GitHub ID나 회사 약어)와 점, 그리고 컨트랙트 이름으로 구성된 문자열을 제공하는 게 관례예요.

ManifestExtra 속성들은 예제 값을 실제 정보로 바꿔주세요. 예제 컨트랙트에서 OnNumberChanged 이벤트, MAP_NAME 상수, ChangeNumberGetNumber 메서드도 제거할 수 있어요:

using System;
using System.ComponentModel;
using System.Numerics;

using Neo;
using Neo.SmartContract.Framework;
using Neo.SmartContract.Framework.Native;
using Neo.SmartContract.Framework.Services;

namespace Registration
{
    [DisplayName("djnicholson.RegistrationContract")]
    [ManifestExtra("Author", "David Nicholson")]
    [ManifestExtra("Email", "[[email protected]](/cdn-cgi/l/email-protection)")]
    [ManifestExtra("Description", "A domain registration service for Neo blockchains")]
    public class RegistrationContract : SmartContract
    {
    }
}

이제 컨트랙트를 다시 빌드해서 여전히 잘 빌드되는지 확인해보세요. 하지만 우리 컨트랙트는 아직 유용한 일을 하지 않아요. 다음으로 완전히 기능하는 도메인 등록 서비스로 만들기 위해 다양한 메서드들을 코딩해보겠습니다!

도메인 등록 인터페이스

이 예제에서는 유효한 도메인 이름을 'a'부터 'z'까지의 문자로만 구성된 비어있지 않은 문자열로 정의하겠습니다. 간단하게 시작해보죠!

우리가 구현할 기능들은 다음과 같아요:

  • 조회 기능: 누구나 Lookup 메서드를 사용해서 도메인 이름이 이미 등록되었는지, 그리고 등록되었다면 누가 소유하고 있는지 확인할 수 있어요.
  • 등록 기능: 사용 가능한 이름은 Register 메서드를 호출해서 등록할 수 있습니다 (도메인의 의도된 소유자로 트랜잭션에 서명해야 해요).
  • 전송 기능: 도메인의 기존 소유자는 Transfer 메서드를 호출해서 다른 사람에게 도메인을 전송할 수 있어요 (새 소유자를 인수로 제공하고 현재 소유자로 트랜잭션에 서명).
  • 삭제 기능: 도메인의 기존 소유자는 Delete 메서드를 호출해서 등록을 삭제할 수 있습니다 (현재 소유자로 트랜잭션에 서명).

그리고 도메인 이름의 소유권이 변경될 때마다 이벤트도 발생시킬 예정이에요!

헬퍼 메서드

코딩을 시작하기 전에 몇 가지 유용한 헬퍼 메서드들을 먼저 구현해보겠습니다.

첫 번째로 검증 로직을 구현하는 헬퍼 메서드를 만들어볼게요. 임의의 문자열이 우리가 정한 규칙에 따라 유효한 도메인 이름인지 확인하고, 그렇지 않으면 예외를 발생시키는 역할을 합니다:

static void Validate(string domain)
{
    var domainBytes = domain.ToByteArray();
    for (int i = 0; i < domain.Length; i++)
    {
        if (domainBytes[i] < 'a' || domainBytes[i] > 'z')
        {
            throw new Exception("Domains must only use lowercase a-z characters");
        }
    }

    if (domain.Length == 0)
    {
        throw new Exception("Domains must be non-empty");
    }
}

유효한 도메인 이름의 현재 소유자를 알아야 하는 경우가 많으니까, 이를 위한 헬퍼 메서드도 추가해보겠습니다. 컨트랙트 스토리지를 사용해서 어떤 도메인이 어떤 주소에 의해 소유되고 있는지 추적하고, 도메인이 등록되지 않은 경우에는 0을 반환하도록 할 거예요:

static UInt160 GetOwner(string domain)
{
    var value = Storage.Get(Storage.CurrentContext, domain);
    if (value == null)
    {
        return UInt160.Zero;
    }
    else
    {
        return (UInt160) value;
    }
}

마지막으로 도메인 이름 소유권이 변경될 때마다 발생시킬 이벤트도 선언해야 해요:

[DisplayName("ChangeOwner")]
public static event Action<string, UInt160> OnChangeOwner;

도메인 이름 조회

이제 첫 번째 기능부터 구현해보겠습니다! 사람들이 도메인 이름의 현재 소유자를 조회할 수 있게 해주는 기능이에요. (0을 반환하면 도메인이 현재 등록되지 않았다는 뜻입니다)

public static UInt160 Lookup(string domain)
{
    Validate (domain);
    return GetOwner(domain);
}

도메인 이름이 유효한지 먼저 확인한 후에 추가 처리를 진행하는 패턴을 사용했어요. 앞으로 모든 컨트랙트 작업에서 이와 같은 패턴을 따를 예정입니다!

도메인 이름 등록

이제 두 번째 기능인 도메인 등록 기능을 구현해보겠습니다! 누군가가 사용 가능한 도메인 이름을 등록할 수 있게 해주는 기능이에요:

public static void Register(string domain)
{
    Validate(domain);

    if (!GetOwner(domain).IsZero)
    {
        throw new Exception("Already registered");
    }

    var tx = (Transaction) Runtime.ScriptContainer;
    Storage.Put(Storage.CurrentContext, domain, tx.Sender);
    OnChangeOwner(domain, tx.Sender);
}

먼저 도메인이 유효하고 사용 가능한지 확인하는 것을 볼 수 있어요. 그런 다음 트랜잭션에 서명한 주소를 추출해서 컨트랙트 스토리지를 업데이트합니다. 이렇게 하면 도메인 이름과 주소 간의 매핑이 블록체인에 영구적으로 저장돼요!

도메인 이름 전송

세 번째 기능인 도메인 전송 기능을 구현해보겠습니다! 도메인 소유자가 다른 사람에게 도메인을 전송할 수 있게 해주는 기능이에요:

public static void Transfer(string domain, UInt160 to)
{
    Validate(domain);

    var owner = GetOwner(domain);
    if (GetOwner(domain).IsZero)
    {
        throw new Exception("Not registered");
    }

    if (!to.IsValid || to.IsZero)
    {
        throw new Exception("Invalid transferee");
    }

    if (!Runtime.CheckWitness(owner))
    {
        throw new Exception("Not authorized");
    }

    Storage.Put(Storage.CurrentContext, domain, to);
    OnChangeOwner(domain, to);
}

여기서는 여러 가지 검사를 수행해요: 도메인이 이미 등록되었는지, 대상 주소가 유효한지, 그리고 트랜잭션을 서명한 사람이 실제로 도메인의 현재 소유자인지 확인합니다. 모든 검사가 통과하면 컨트랙트 스토리를 업데이트하고 소유권 변경 이벤트를 발생시켜요!

도메인 이름 삭제

마지막 기능인 도메인 삭제 기능을 구현해보겠습니다! 도메인 소유자가 자신의 등록을 삭제할 수 있게 해주는 기능이에요:

public static void Delete(string domain)
{
    Validate(domain);

    var owner = GetOwner(domain);
    if (owner.IsZero)
    {
        throw new Exception("Not registered");
    }

    if (!Runtime.CheckWitness(owner))
    {
        throw new Exception("Not authorized");
    }

    Storage.Delete(Storage.CurrentContext, domain);
    OnChangeOwner(domain, UInt160.Zero);
}

여기서는 도메인이 현재 등록되어 있는지, 그리고 등록된 사람이 실제로 트랜잭션에 서명했는지 확인해요. 그런 다음 스토리지에서 관련 항목을 제거하고 소유권 변경 이벤트를 발생시킵니다. (도메인이 다시 사용 가능해졌음을 나타내기 위해 0 주소를 사용해요)

와! 이제 개인 Neo 블록체인에 컨트랙트를 배포할 준비가 완전히 끝났습니다! 🚀

컨트랙트 배포

이제 정말 중요한 순간이에요! 스마트 컨트랙트를 블록체인에 배포해보겠습니다.

스마트 컨트랙트 코드를 빌드할 때 생성되는 파일 중 하나가 RegistrationContract.nef인데, 이 파일에는 컨트랙트의 N3 가상 머신 바이트코드가 들어있어요. 배포할 때 이 파일이 필요합니다. 다음과 같이 개인 블록체인에 컨트랙트를 배포해보세요:

$ neoxp contract deploy Registration/bin/Debug/net5.0/RegistrationContract.nef owner
Deployment Transaction 0xc5f6b16350427e4bf0dfa01fc939ac192df45e39e703e60cd08db5c1a37e17a5 submitted

🎉 축하합니다! 컨트랙트가 개인 Neo 블록체인에 성공적으로 배포되었어요! 이제 실제로 도메인을 등록해보는 시간입니다…

도메인 등록

이제 정말 재미있는 부분이에요! Neo Express를 사용해서 개인 블록체인에 배포된 컨트랙트를 호출해보겠습니다.

이를 위해 "invoke file"이라는 JSON 파일을 만들어야 해요. 이 파일은 호출해야 할 컨트랙트 메서드들을 지정합니다.

새 텍스트 파일을 만들고 다음 JSON을 붙여넣어보세요:

[
  {
    "contract": "djnicholson.RegistrationContract",
    "operation": "register",
    "args": ["widgets"]
  }
]

파일을 alice-registration.neo-invoke.json으로 저장한 다음 다음 명령을 실행해보세요:

$ neoxp contract invoke alice-registration.json owner
Invocation Transaction 0xdbf3fa71a74fa0676d7513ded2e2f18ea6cf5b9f022a9b008b9b05bc36865217 submitted

이 명령은 Alice의 지갑을 사용해서 invoke 파일의 모든 단계(우리 경우에는 하나의 단계만)를 호출하는 트랜잭션을 개인 블록체인에 제출합니다.

🎊 축하합니다! 첫 번째 도메인을 성공적으로 등록했어요! widgets는 이제 Alice가 소유하고 있습니다!

도메인 전송

이제 Alice가 widgets 도메인을 Bob에게 전송해보겠습니다!

alice-to-bob-transfer.neo-invoke.json이라는 새 invoke 파일을 만들고 다음과 같이 채워보세요:

[
  {
    "contract": "djnicholson.RegistrationContract",
    "operation": "transfer",
    "args": ["widgets", "@bob"]
  }
]

💡 : invoke 파일에서 지갑 이름 앞에 '@' 문자를 붙이면 지갑 주소를 참조할 수 있어요!

이제 이전과 같은 방식으로 이 invoke 파일을 실행해보세요 (Alice의 계정을 사용해서 트랜잭션을 제출). 이제 Bob이 widgets 도메인을 소유하게 됩니다!

흥미로운 점은 동일한 invoke 파일을 다시 실행해보면 트랜잭션이 오류로 결과된다는 거예요. Alice가 더 이상 소유자가 아니기 때문에 스마트 컨트랙트가 예외를 발생시키거든요!

도메인 삭제

마지막으로 widgets 도메인을 삭제해보겠습니다!

delete-widgets.neo-invoke.json이라는 새 invoke 파일을 만들고 다음과 같이 채워보세요:

[
  {
    "contract": "djnicholson.RegistrationContract",
    "operation": "delete",
    "args": ["widgets"]
  }
]

이제 이전과 같은 방식으로 이 invoke 파일을 실행하되, 이번에는 Bob의 계정을 사용해서 트랜잭션을 제출해보세요. 이제 아무도 widgets 도메인을 소유하지 않게 되고, 다시 등록할 수 있게 됩니다!

독자를 위한 연습

우리가 만든 컨트랙트는 누구나 무료로 도메인을 등록할 수 있게 해줍니다 (호출 트랜잭션을 제출하기에 충분한 GAS가 있다면요). 하지만 실제 환경에서는 사용자가 도메인을 등록할 때 수수료를 부과하고 싶을 수도 있겠죠? 사람들은 NEO, GAS 또는 다른 NEP-17 자산으로 이러한 수수료를 지불할 수 있어요.

도전 과제: RegistrationContract를 수정해서 이 기능을 지원해보세요!

누군가가 컨트랙트에 자산을 지불할 때마다 호출될 OnPayment 메서드를 컨트랙트에 추가할 수 있습니다. OnPayment 메서드는 발신자와 자금 금액을 인수로 제공하고, 런타임에서 제공하는 Runtime.CallingScriptHash 속성을 검사해서 어떤 자산이 지불되었는지 확인할 수 있어요. 그리고 사용자가 등록하고 싶은 이름을 지정할 수 있도록 선택적 data 인수도 활용할 수 있습니다.

OnPayment 메서드 내에서 트랜잭션을 거부할 수도 있어요—예를 들어, 도메인이 사용 불가능하거나 충분한 자금이 지불되지 않은 경우 예외를 발생시켜서요.

이 기능을 구현해보시면 더욱 실용적인 도메인 등록 시스템을 만들 수 있을 거예요! 🚀

소스 코드 목록

마지막으로 완전한 스마트 컨트랙트 소스 코드를 보여드릴게요! 참고하시면 도움이 될 거예요:

using System;
using System.ComponentModel;

using Neo;
using Neo.SmartContract.Framework;
using Neo.SmartContract.Framework.Native;
using Neo.SmartContract.Framework.Services;

namespace Registration
{
    [DisplayName("djnicholson.RegistrationContract")]
    [ManifestExtra("Author", "David Nicholson")]
    [ManifestExtra("Email", "[[email protected]](/cdn-cgi/l/email-protection)")]
    [ManifestExtra("Description", "A domain registration service for N3 blockchains")]
    public class RegistrationContract : SmartContract
    {
        [DisplayName("ChangeOwner")]
        public static event Action<string, UInt160> OnChangeOwner;

        static void Validate(string domain)
        {
            var domainBytes = domain.ToByteArray();
            for (int i = 0; i < domain.Length; i++)
            {
                if (domainBytes[i] < 'a' || domainBytes[i] > 'z')
                {
                    throw new Exception("Domains must only use lowercase a-z characters");
                }
            }

            if (domain.Length == 0)
            {
                throw new Exception("Domains must be non-empty");
            }
        }

        static UInt160 GetOwner(string domain)
        {
            var value = Storage.Get(Storage.CurrentContext, domain);
            if (value == null)
            {
                return UInt160.Zero;
            }
            else
            {
                return (UInt160) value;
            }
        }

        public static UInt160 Lookup(string domain)
        {
            Validate (domain);
            return GetOwner(domain);
        }

        public static void Register(string domain)
        {
            Validate (domain);

            if (!GetOwner(domain).IsZero)
            {
                throw new Exception("Already registered");
            }

            var tx = (Transaction) Runtime.ScriptContainer;
            Storage.Put(Storage.CurrentContext, domain, tx.Sender);
            OnChangeOwner(domain, tx.Sender);
        }

        public static void Transfer(string domain, UInt160 to)
        {
            Validate (domain);

            var owner = GetOwner(domain);
            if (GetOwner(domain).IsZero)
            {
                throw new Exception("Not registered");
            }

            if (!to.IsValid || to.IsZero)
            {
                throw new Exception("Invalid transferee");
            }

            if (!Runtime.CheckWitness(owner))
            {
                throw new Exception("Not authorized");
            }

            Storage.Put(Storage.CurrentContext, domain, to);
            OnChangeOwner (domain, to);
        }

        public static void Delete(string domain)
        {
            Validate (domain);

            var owner = GetOwner(domain);
            if (owner.IsZero)
            {
                throw new Exception("Not registered");
            }

            if (!Runtime.CheckWitness(owner))
            {
                throw new Exception("Not authorized");
            }

            Storage.Delete(Storage.CurrentContext, domain);
            OnChangeOwner(domain, UInt160.Zero);
        }
    }
}
profile
스마트 이코노미를 위한 퍼블릭 블록체인, 네오에 대한 모든것

0개의 댓글