Sự bùng nổ NFT đã xảy ra, và vẫn đang diễn ra (tính đến thời điểm viết bài này vào tháng 7 năm 2022). Etherscan có một tiện ích tìm kiếm tiện dụng, cùng với các tính năng xác minh và dịch ngược tiện dụng, cho phép bạn xem qua mã của nhiều ERC721 để so sánh. Cùng với nhiều hợp đồng được thiết kế tốt, chúng ta cũng có thể thấy nhiều hợp đồng lặp đi lặp lại những sai lầm tương tự. Trong bài viết này, tôi sẽ đưa ra ý kiến của mình về 4 trong số những "lỗi thiết kế" phổ biến nhất đối với NFT, mà tôi thường nhận thấy khi xem các hợp đồng NFT trên etherscan.
Lưu ý rằng bài viết này được viết chủ yếu với các blockchain tương thích với EVM, nhưng nhiều điểm có thể áp dụng hoặc có một số điểm tương tự hoặc tương tự trên các mạng khác.
Xin đừng xúc phạm nếu bạn đã phạm phải một số điều mà tôi gọi là "thiết kế không thành công". Đây là những ý kiến của tôi, và hơn nữa với tư cách là một nhà phát triển, tôi hoàn toàn hiểu sự cần thiết phải tiết kiệm chi phí gas trong những thời điểm tắc nghẽn mạng lưới đắt đỏ như thế này. Hãy xem xét quan điểm của tôi với tư cách là một nhà tư vấn tự do; một khách hàng có thể chi hàng nghìn đô la cho sự trợ giúp phát triển chắc chắn có thể đặt ra thêm hàng trăm đô la để triển khai, nhằm theo đuổi sự xuất sắc.
Tất nhiên đây là nói về chuỗi Ethereum, chuỗi này đắt nhất tính đến thời điểm viết bài này; Đa giác thì ít hơn, và các chuỗi khác như Solana (không phải EVM) thậm chí còn ít hơn. Quan điểm của tôi là nếu có sẵn quỹ, lợi ích của việc triển khai chất lượng cao hơn có thể xứng đáng với chi phí bổ sung.
Điều này cực kỳ phổ biến, nhưng khi tôi nhìn thấy điều này, nó gắn cờ hợp đồng, đối với mắt tôi, như được thực hiện một cách nghiệp dư. Công bằng mà nói, có những động cơ hợp lệ và dễ hiểu. Thứ nhất, việc triển khai và quản lý các hợp đồng trên nhiều mạng trở nên rất tốn kém, và người ta đã phải nỗ lực để tiết kiệm những chi phí đó. Và, vì đơn giản, người ta có thể nghĩ, tại sao không đưa logic bán và đúc tiền vào chính hợp đồng?
Nhưng đây không hẳn là một ý kiến hay. Bản thân hợp đồng phải là trung tâm bất biến của một mạng lưới logic, nhưng không bao giờ được làm bẩn bàn tay của chính nó bằng cách xử lý tiền trực tiếp. Điều này bao gồm việc bán, thời gian bán, danh sách trắng, v.v., trực tiếp trong cùng một mã hợp đồng khi triển khai ERC721. Logic bán hàng và logic cốt lõi được kết hợp chặt chẽ với nhau.
Mặc dù tiết kiệm chi phí khí đốt có thể là lý do tốt nhất và dễ hiểu nhất để nhồi nhét tất cả logic vào một hợp đồng, tôi nghĩ rằng, tất cả những điều được xem xét, có nhiều lý do tốt hơn để không thực hiện lối tắt thiết kế này. Logic hợp đồng cốt lõi của bạn phải là thứ duy nhất được thiết lập sẵn và trong hầu hết các trường hợp sẽ thực hiện tiêu chuẩn một cách rất, tốt… tiêu chuẩn. Nhiều bản sao là (hoặc có thể là) gần như là bản sao của nhau. Chiến lược đúc tiền của bạn, giá cả (nếu bạn đang bán bạc hà) - những thứ này nên được tách biệt. Điều này cho phép hợp đồng của bạn linh hoạt theo cách không làm tổn hại đến lòng tin của người dùng. Thiết kế tách rời và nguyên tắc trách nhiệm duy nhất. Lưu ý phụ: Tôi nghĩ rằng việc hạn chế nguồn cung cấp (tức là maxSupply) trong chính hợp đồng ERC721 là hợp lý, miễn là nó có thể được sửa đổi bởi người có vai trò quản trị viên.
Hợp đồng mã thông báo cần một số loại kiểm soát truy cập, bởi vì có các chức năng (như đúc tiền hoặc làm bất cứ điều gì với các tham số cung cấp) chỉ khả dụng cho các địa chỉ được cấp phép. Cách đơn giản nhất để thực hiện điều này là sử dụng mô hình Có thể sở hữu (thường sử dụng hợp đồng Có thể sở hữu của OpenZeppelin vì tại sao lại phát minh lại bánh xe cho một nhu cầu cơ bản như vậy). Nhưng tôi thực sự khuyên bạn nên sử dụng điều khiển truy cập dựa trên vai trò thay thế, vì những lý do sau. Động lực đằng sau việc sử dụng Ownable (hoặc một cái gì đó tương tự) có lẽ là sự đơn giản (và tiết kiệm chi phí xăng), bề ngoài là tốt. Bạn cũng có thể “biết” rằng bạn (hoặc khách hàng của bạn) sẽ “luôn luôn” là người duy nhất quản lý hợp đồng. Tốt hơn là kiểm chứng tương lai, khi chi phí thấp; và độ phức tạp của bảo mật dựa trên vai trò (ví dụ như IAccessControl của OpenZeppelin) thực sự chỉ phức tạp hơn một chút (và đắt tiền) khi so sánh với mô hình Có thể sở hữu. Nếu chi phí xăng vẫn là một vấn đề, bạn luôn có thể cắt bớt mã bảo mật dựa trên vai trò (có thể là OpenZeppelin hoặc mã của riêng bạn) để chỉ những gì bạn cần. Nhưng lý do quan trọng hơn để sử dụng dựa trên vai trò là nó cho phép bạn tách chức năng (như trong thông tin điểm, bán và giá trước đó) khỏi chính hợp đồng ERC721. Nó cho phép bạn chỉ định một hợp đồng riêng biệt với tư cách là người khai thác bằng cách gán cho nó vai trò “người thuê”, mà không cho phép nó có đầy đủ quyền quản trị. Trong khi đó, quản trị viên (hoặc quản trị viên, có thể là con người chứ không phải hợp đồng) vẫn có quyền cấp cao hơn (chẳng hạn như xóa và thêm quyền). Khi người thợ đúc (ví dụ) không còn đáp ứng nhu cầu của bạn nữa, người ta chỉ cần loại bỏ nó bằng cách thu hồi quyền khai thác và chuyển nhượng quyền khai thác cho một hợp đồng mới thực hiện chiến lược khai thác mới; nó theo mô-đun, tiện lợi và an toàn. Các hoạt động khác ngoài việc đúc tiền có thể được xử lý theo cách tương tự, dựa trên các trường hợp sử dụng cụ thể của dự án.
Nhiều mã thông báo (hoặc hợp đồng nói chung) hoặc không triển khai ERC-165 hoặc không triển khai nó một cách tối ưu. ERC-165, theo quan điểm của tôi, là về khả năng tương tác. Nó làm cho hợp đồng của bạn tương thích trong tương lai và các sàn giao dịch có thể gọi nó để tìm hiểu (ví dụ) về cấu trúc tiền bản quyền của NFT của bạn. Tôi thấy điều này thường không được triển khai hoặc thực hiện không tối ưu.
Đây là một quy tắc chung để triển khai nó một cách chính xác:
|| type(ISomeInterface).interfaceId == _interfaceId
thí dụ:
function supportsInterface(bytes4 _interfaceId) public view override(ERC721, ERC721Enumerable) returns (bool) { return super.supportsInterface(_interfaceId) || _interfaceId == type(IERC2981).interfaceId; }
Nếu mã của bạn không có lớp cha nào triển khai ERC-165, thì chỉ loại thứ hai nên được đại diện, chẳng hạn như
function supportsInterface(bytes4 _interfaceId) public view override returns (bool) { return _interfaceId == type(IERC721).interfaceId || _interfaceId == type(IERC2981).interfaceId || _interfaceId == type(IAccessControl).interfaceId; }
Nếu mã của bạn triển khai không có giao diện nào khác ngoài những giao diện được xử lý bởi việc triển khai ERC-165 của các lớp cha, thì kiểu thứ hai là không cần thiết. Như là:
function supportsInterface(bytes4 _interfaceId) public view override(ERC721, ERC721Enumerable) //just make sure this list is complete returns (bool) { return super.supportsInterface(_interfaceId); }
Thực hiện đúng ERC-165 là tùy chọn, nhưng quan trọng. Bạn muốn mã thông báo của mình tương thích với càng nhiều hệ thống khác (chẳng hạn như sàn giao dịch) càng tốt, bao gồm cả những hệ thống trong tương lai chưa được triển khai. Tiêu chuẩn ERC-165 có thể sẽ được sử dụng nhiều hơn và quan trọng hơn khi thời gian trôi qua và không gian trưởng thành.
Mã thông báo ERC721 của bạn có thể rất chuẩn và có thể sử dụng tất cả các lớp và thư viện mẹ của bên thứ ba với rất ít tùy chỉnh và bạn có thể biết rằng mã của bên thứ ba đó đã được kiểm tra và bảo mật rất tốt. Nhưng bạn vẫn cần phải kiểm tra kỹ lưỡng mã của mình, bởi vì bạn chỉ có một cơ hội để có được nó ngay trước khi nó được triển khai vào mạng chính, mang lại cho bạn vinh quang hoặc xấu hổ mãi mãi.
Đầu tiên, tất nhiên, kiểm thử đơn vị . Theo tôi, bạn sử dụng khuôn khổ thử nghiệm nào không quan trọng; Tôi sử dụng hardhat với ete và mocha. Đối với tôi, phần quan trọng duy nhất là phạm vi kiểm tra và phạm vi bao phủ của các trường hợp đường hạnh phúc, trường hợp ngoại lệ và trường hợp cạnh là rộng và sâu. Mặc dù bạn có thể đang kiểm tra mã (ví dụ: OpenZeppelin) đã nổi tiếng được kiểm tra tốt, (a) mã tùy chỉnh của bạn có thể đã bị hỏng một số trường hợp đó, vì vậy chúng nên được kiểm tra lại và (b) OpenZeppelin đã có lỗi trước đó, và họ có thể một lần nữa trong tương lai. Để tiết kiệm thời gian, bạn có thể có một bộ thử nghiệm tiêu chuẩn cho tất cả các mã thông báo ERC721, tất cả các mã thông báo ERC20, tất cả các mã thông báo ERC1155, v.v. mà bạn có thể sử dụng lại từ dự án này sang dự án khác. Điều này là tốt. Sau đó, bạn có thể thêm các trường hợp cho mỗi dự án để bao gồm bất kỳ tùy chỉnh nào theo tiêu chuẩn; điều này sẽ tiết kiệm thời gian. Kiểm tra đơn vị phải bao gồm kiểm soát truy cập, chức năng cơ bản (như đúc và chuyển giao), khả năng tạm dừng (nếu hợp đồng của bạn có thể tạm dừng), triển khai tiêu chuẩn ERC165 và hơn thế nữa. Bạn có thể kiểm tra phạm vi phủ sóng của mình bằng cách sử dụng phạm vi bảo hiểm vững chắc (một gói nodejs).
Cuối cùng, các công cụ tự động có thể giúp bạn rất nhiều trong việc kiểm tra. Slither , Manticore và Mythril là các tiêu chuẩn ngành, thường được sử dụng bởi những tên tuổi lớn trong lĩnh vực kiểm toán bảo mật như Consensys và Certik. Phạm vi bao phủ vững chắc (một gói nodejs) sẽ cho bạn biết tỷ lệ phần trăm ước tính của phạm vi bao phủ mà các thử nghiệm đơn vị của bạn đang cung cấp (rất tiện dụng như một quy tắc chung). Solgraph là một công cụ có thể giúp bạn xem các mối quan hệ và kết nối trong mã hợp đồng; hữu ích trong việc lập kế hoạch kiểm tra. Echidna cũng hữu ích; nó là một công cụ kiểm tra fuzzing. Cá nhân tôi sử dụng phương pháp thử nghiệm đầu tiên nếu có thể. Điều này đảm bảo phạm vi kiểm tra tốt và bộ kiểm tra trở nên tương tự như một thông số kỹ thuật của dự án. Tôi yêu tôi một số bảo hiểm thử nghiệm tốt.
> pip3 install slither-analyzer > pip3 install mythril > npm install solidity-coverage
Vì vậy, để tóm tắt:
Xác minh mã hợp đồng trên etherscan là một tính năng tuyệt vời. Nhìn thấy dấu kiểm màu xanh lá cây trên tab Hợp đồng và có thể xem chính mã, với thẻ "khớp chính xác", tạo ra sự tin tưởng và trách nhiệm. Khi ai đó xem hợp đồng của bạn lần đầu tiên, cố gắng đánh giá rủi ro tương đối của việc sử dụng hoặc đầu tư, điều này chỉ có thể hữu ích. Và điều này nói chung, cho tất cả các loại hợp đồng, không chỉ NFT.