经过前面关于Solidity编程语言的学习,我们已经可以编写出我们自己基本的代码功能了,也掌握了怎么去发行我们的代币了。那么,在这一节中,我将会带着大家学习下,在我们编写合约的过程中,我们需要注意一些什么问题,以及感受一下它区别于我们常规的编程语言写接口又有什么区别呢。让我们继续往下学习吧。
首先,智能合约,作为合约那么一定在排版上应该是非常简单清晰明了的。我们按照这种以下代码的排版方式,我们的合约看起来就会非常顺眼。
我们在刚开始写合约的时候,一定是先声明版权。之后,我们一定要选择Solidity的最新版本,因为最新版本一定是经过优化过的,对代码安全提示或者代码底层优化方面肯定也是优化过了的。最后,我们才开始导入我们需要的外部文件,如果是想要开发遵循EIP协议的合约,那么我们一定是要引用官方Openzeppelin开源的安全代码。
其次,以上这些前提工作做好之后,我们开始编写我们的主要的合约逻辑。在编写合约代码的时候,如果我们有用到library库合约,我们就先使用using for 将我们需要的库引入进来。然后我们在其后都声明一个写入数据到链上的事件,这样,我们想要用其它支持的编程语言与链交互的时候,就可以调用这个方法获取到这个事件返回给我们的更多的具体数据。在声明变量的时候,如果我们能用结构体我们就用结构体将变量在结构体来声明。需要注意的是,在声明变量的时候,我们要将同一种类型放在一起,因为这里面涉及到EVM底层的堆栈分配设计。但是声明的变量不能超过堆栈的限制,否则合约将不会被编译。补充一点,在Solidity语言中,uint和uint256是一样的。当我们声明了uint,其实就是uint256。一般来说,我们建议用uint来书写就可以了,因为即使我们写了uint8或者uint64等,EVM底层同样也会帮我们转换成uint256来处理。
在结构体之后,我们再紧跟着声明数组和映射。就这样,保持我们的合约代码层次结构分明,同时更重要的是考虑到底层可以不用帮我们做数据类型的再次转换,这样我们可以使代码保持在比较好的堆栈分配,使我们部署合约的成本尽量减少,也使我们的合约更加安全。
最后,我们编写我们的初始化构造函数。然后我们将写入方法和读取方法,以及一些不重要的方法依次写在合约。整个代码现在看起来,就很清晰。希望大家以后在编写合约的时候,尽量养成良好的习惯。遵循安全,简单,清晰的原则。
代码示例如下:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.17; // 最新版本 0.8及以上
import '@openzeppelin/contracts/token/ERC721/extensions/ERC721Enumerable.sol'; // 官方Openzeppelin库合约
contract Master is ERC721Enumerable,Ownable {
// 首先将需要使用的代码库写在合约首位
using Strings for uint256;
// 事件
event Write(string indexed data);
// 变量
address private _owner;
// 声明私有变量,私有的通常用下划线
address public manager;
uint private _count;
uint public score;
string public color;
string public weight;
bool public isTrue;
// 将变量放在结构体
struct Users {
address account;
uint64 beforeAmount;
uint126 afterAmount;
string info;
}
// 数组
uint[] arr1;
address[] arr2;
// 映射
mapping(uint => string) maps; mapping(uint => Users) userMapping;
// 构造函数
constructor() public {
...
...
}
// 写入方法1
function WriteLogic1() external {
_writeLogic();
}
// 写入方法1
function WriteLogic2() external {
_writeLogic();
}
// 读取方法1
function ReadLogic1() external {
...
}
// 读取方法2
function ReadLogic2() external {
...
}
// 写入方法具体逻辑
function _writeLogic() private {
...
}
// 其它不重要的方法
function otherNotImportantFunction() internal {
...
}
}
前面,我们强调了合约的版本,以及着重强调了合约的排版问题。接着,我们要考虑的是合约的可见性的问题。合约的可见性,在之前的文章中,我们也有讲过,分为private,internal,external和public。当我们在编写写入数据的方法时,我们建议使用external,因为这个相对于public来说安全一些,再一个也是节约部署合约成本的一个细节。在涉及到逻辑比较严谨的代码中,我们建议使用private函数,让我们的写入数据的方法直接去调用即可,例如上面代码中的_writeLogic方法。这四个的区别是:private只能在本合约调用,internal可以在本合约和继承的合约调用,external只能在外部合约调用而本合约调用不了,public则没有限制,public的使用也是要小心谨慎,否则方法容易被黑客利用,造成不必要的损失。
我们在编写合约的时候,一定要使代码量越少越好,这里的代码量包含了引入的代码库。代码量少,代码不至于太过复杂,安全性也会高。当我们编写合约逻辑的时候,发现我们的代码量特别多,这时候就要小心谨慎的审阅我们的代码,包括不限于检查代码中的算术溢出或者转账等逻辑代码。
以太坊部署合约,执行合约写入数据的方法都需要花费gas费,也就是需要花费ETH来作为交易的手续费。这就要求我们在确保合约代码安全性的前提下,尽量的使我们的代码简洁,将需要上链的数据上链,将不必要的数据进行链下处理。正确合理的引用官方的库合约代码,比如之前我们的NFT代码每次铸造都需要触发一次mint方法,每一次都需要消耗gas费,那么我们就用官方的ERC721Enumerable.sol这份合约,我们可以使用官方的ERC721A合约,这样,我们就可以实现批量铸造,一次性铸造多个NFT啦。
编写合约,就是要考虑代码的安全性,简洁性,节约部署成本。但是需要提醒一下的是,我们不能为了节省成本而节省成本,什么意思呢?当我们的代码逻辑中,首先考虑的是它的安全问题,而成本问题只能优化对代码主要逻辑和安全影响不大的部分,对于影响比较大的,我们还是要保留,即使它多花了我们些部署合约的费用。
合约的安全性一直是智能合约开发者的首要问题。由于一切的合约产品都是要开源源代码的,这就意味着任何人都可以查看到源代码的逻辑,此时合约的安全问题就显得非常重要。因为合约一旦部署,是不可控的。合约的安全是一个重要的话题,需要大家好好在学习合约的路途上,多去摸索,也多去看看别人的合约是如何被攻击的,从中学习经验。
经过上面的讲解,我们可以看到,合约的编写与我们传统的写开发接口是不一样的。区块链没有并发的概念,而我们在开发传统的接口的时候,我们经常要考虑到使用同步还是异步,使用什么样的数据库等相关考虑的问题,而且也还考虑到服务器的承载量,代码的负载均衡等诸多问题。而在区块链世界中,这些基本上都不用考虑到。
好了,这次关于以太坊Solidity语言的学习到此就结束了。在接下来的文章中,我们来讲讲什么是web3,以及怎么使用web3来与我们的合约进行交互。咱们下期再见!
领取专属 10元无门槛券
私享最新 技术干货