1.定义事件event
2.调用事件呃,emit
3.external –只能外部读取
4.pure –纯函数(不能读不能写状态变量,只能有局部变量,完全不对链上有任何操作)
5.returns() –返回(括号是参数返回类型)
6.address 地址类型
7.view –只读方法(读链上)–不消耗gas
8.pure –不会读链上
9.constant –把状态变量变成常量(常量后变大写命名名称)有助于省gas费–不需要修改的值定为常量
10.判断
/require(i10”)
|revert (在if里面写判断)
\assert –断言 进行测试,不包含错误信息
11. modifier–函数修改器(可做公共功能,里面的下划线就是其他代码在这里运行)
调用就是在函数后面加入修改器名称就行
eg:
modifier when(uint x){
require(x10”);
_;
}
function in() external when{
}
12. constructor –构造器(初始化,一个合约只能创建一个)
13. 创建数组–
动态数组 uint[] public nums=[1,2,3]
定长数组 uint[3] public num=[1,2,3]
动态数组可增加数据 nums.push(4) eg:[1,2,3,4]
删除数字 但是不会修改数组长度,只会是成默认数据 delete nums[1] eg:[1,0,3,4]
完全删除数组中数据–remove(1)
弹出数组中数据 nums.pop(); eg:[1,2,3] 特点:费gas
获取数组长度 nums.length;
14.memory –在内存中 eg:uint[] memory a = new uint
15.映射 mapping(address =>uint)public balances
嵌套式映射 mapping(address =>mapping(address => bool)) public isFriend
eg:调用者有123余额
function eg() external{
//给映射赋值
balances[msg.sender] = 123;
获取映射返回的值
uint bal =balances[msg.sender];
删除映射数据
delete balances[msg.sender];
嵌套式映射赋值
isFriend[msg.sender][address(this)] = true;
}
16.结构体 Structs
17.枚举 enum
18 .payable –链上操作转账,发送以太坊主币
19.内联汇编(代理合约调用)
eg:
assembly{
//create(v,p,n)
//v –合约主币数量
//p –机器码开始位置
//n –机器码大小
//msg.value =callvalue()–内联汇编中用 mload–获取大小
create(callvalue(),add(_code,0x20,mload(_code)))
}
20.calldata 省gas
21.memory
22.可视范围
private 私有 继承也看不到
public 公共外部
internal 内部函数
external外部函数
23.immutable –不可变量 省gas
24 .发送主币
2300gas tranfer eg:_to.tranfer(123);–代表123个wei被发送
2300gas send 会返回bool类型值 确定是都发送失败 eg:bool seed = _to.send(123); require(seed ,“seng failed”);
很多gas call eg:(bool success, ) = _to.call{value:123}(“”);
25.类似于类的概念
注意: constant 是定义恒常数,不能改变成员变量。但是,这个版本中,只是警示用,实际程序可以修改的。returns(uint) 定义返回类型。
最新版本中,constant变为view,但是仍然可以改参数;pure是加强版的view,意思是函数中不能修改,也不能读取成员变量,例如:用于加密库之类。
合约的执行

个人简单的理解:
当部署在区块链中时,会申明出这个合约中的方法,然后大家就可以通过执行这些方法来进行相应的交易。
每一个函数执行都是有一定的费用的,简单理解为小费,单位gas,在以太坊的ether是最小的虚拟货币单位,会有价格波动,所以用gas表示函数执行所拥有的小费。比如说,一个函数固定执行一次会有2146gas,但是gas的price是随ether的价格变动而变动的。这样,每一个函数执行的费用就不会随着货币价值的变化而需要修改相应的数量了。
类型

uint/int ,目前不存在浮点数类型address,类似于一个objecct类型,默认值为0x0(包含钱包地址,智能合约的地址等)。.balance地址上的金额,.transfer转钱

address方法
一些单位跟常用变量
uint salary = 1 ether; //金额的最小单位wei,本质上就是等于整型的1
uint constant dur = 30 days ;// 时间单位,这里的constant是有效的,申明常变量,但是修饰函数,是没用的
uint lastPday = now;//
统一单位、金额、时间、块的概念,相当于静态变量(全局变量)。在区块链中,就是挖到的交易块消息
当执行某个函数时,包含调用用户信息等。
.sender 谁调用了这个函数
.gas 这个函数附带的gas消费
调用函数,给合约塞钱
充值函数
关键字 payable 只有添加这个关键字,才能在执行这个函数时,给这个合约充钱。
this.balance 这里的this就是指代合约的地址,return this.balance 就是返回执行之后合约上的钱。
通过分析2,可以推测函数的执行,虽然是用户点击执行,但是真正执行环境是contract。
给一个地址转钱
申明金额
钱包地址
合约转钱
注意: 当执行函数时,是执行多少语句就要花费多少gas,如果说遍历很大的数据时,就很贵了。所以如果这个函数出现了异常或者没有按照理想的情况执行,这些gas是拿不回来的。执行revert()函数,能够终止当前函数,并不消耗gas。而throw异常的话,执行到throw前的gas就拿不回来了。
固定执行的用户
只能frank调用
变量作用域与js很像
会有变量提升
计算顺序,注意点

在solidity中,除法是做整除,没有小数点。so
当计算a(b-c)/d时,如果计算顺序是a((b-c)/d),会导致误差变大。
构造函数

在合约发布的时候,就执行。
这里的意思是,保存发布这个合约的人。

构造函数
合约名

注意: 构造函数与合约名要一致。
assert && require

assert函数用于确定运行中的代码满足某要求。
require函数用于要求输入的起始条件。
require
assert
数组

可固定,也可以动态数组。
uint[2] a;//固定长度

uint[] a;//动态长度,这时长度为0,所以无法用a[0]=1进行赋值
a.push(1);//用于增加元素,之后,就可以用a[0]进行获取

delete a[i];
a[i] = a[a.length-1];
a.length -=1;//删除数组中的值,并且把最后一个数放到删除的位置上,长度减一
struct结构

这个就类似于C语言里面的,构造类型。
struct
Employee[] employees;//相当于一个新的类型
Employee(employee,salary,now);//这就新建了一个Employee类型对象了
可视度,函数默认为public

但是一个函数的输入或者输出是自己建的struct,那么函数可视度需要为private。
可视度
数据存储
数据存储

storage 是在区块链上,永久存在
memory 临时空间,当函数运行之后,就会释放
calldata 也是临时空间,类似memory

这里的存储类似于JS,对于一个对象来说,存储的是内存地址。

规则

注意点: 由于函数返回的是memory上的数据。
实例

这里的_findEmployee函数返回相应的状态变量的拷贝。因为函数返回是memory,而状态变量是storage。

这时当修改employee上的值时,存在storage上的状态变量是不会改变的。
为了减少gas的消耗,mapping

就是一个hash数据结构。
只有四种类型做key

mapping
实现规则

由于不能进行遍历,所以下面的语句报错。

错误

解决方法:将uint totalSalary变为状态变量,然后在每次增加或者移除一个employee时,便操作这个totalSalary的值。
命名参数
命名返回参数
得到的结果

对于输出也可以直接进行赋值

直接赋值
可视度
四种可视度

internal像protect。
external就是只能是外部用户或者其他合约来调用,而当前合约其他内部函数,是无法调用的。为了能够使用,可以用this.fn1();这样就相当于外部调用,代价会贵一点。

状态变量可视度

因为在区块链上所有的数据都是公开的。合约的成员变量都是肉眼可见的。
函数的可视度
继承
继承的小例子

子类有构造函数

父类具有构造函数

不同的父类构造函数形式

Parent is owned 继承
抽象合约
抽象合约

抽象合约不能部署在区块链上,继承抽象合约的合约需要重新定义其中的函数,类似于重写。
INTERFACE
interface

必须实现继承的interface上的函数,否则因为还有抽象函数在里面,合约不能发布。
多继承
简单的多继承
继承顺序
动态绑定1
动态绑定2
super.func1();//用于绑定上级继承的函数func1
实例
执行顺序的线性确定

如果多继承的执行函数出现环,就无法进行线性化。
modifier,使得代码工程化
address owner = 0xdfsdfdff;
modifier onlyOwner{
require(msg.sender == owner)
-;//相当于替代修饰的函数剩下的语句,也可以加参数
}

function removeEm(address employ) onlyowner{

}//这样修饰之后就相当于加了一个输入限制

加参数的modifier

modifier参数来自于当前函数的传入参数或者全局的状态变量。
不同的-;

这里的modifier中的a=1;执行在修饰的函数return之前。
SAFE math

在solidity中,运算是比较危险的。
因为这个数字大部分都跟钱有关系,所以要避免数据的溢出等异常。

提前检验一下

但是这样做的话,很麻烦。所以我们加入一些第三方库,来帮我们确保safe math。
例如:

zeppelin-solidity

import ‘./SafeMath.sol’;//进行导入第三方的library

如果要自己编写一个第三方库:

library申明

当引入第三方库后,就可以直接使用里面定义的方法了。但是这样也会很麻烦,相当于每次进行操作时,都会调用这个函数。
利用一个语法,using SafeMath for uint8; 这样每当调用SafeMath中的sub函数时,直接用a.sub(100)替代SafeMath.sub(a,100),因为a是uint8类型,可以直接穿透到参数里面。


新的 fallback 函数写法
fallback() external {
}
receive() payable external {
currentBalance = currentBalance + msg.value;
}

对于这种新的写法,有几点是要注意的:
1.fallback 和 receive 不是普通函数,而是新的函数类型,有特别的含义,所以在它们前面加 function 这个关键字。加上 function 之后,它们就变成了一般的函数,只能按一般函数来去调用。
2.每个合约最多有一个不带任何参数不带 function 关键字的 fallback 和 receive 函数。
3.receive 函数类型必须是 payable 的,并且里面的语句只有在通过外部地址往合约里转账的时候执行。
4.fallback 函数类型可以是 payable 也可以不是 payable 的,如果不是 payable 的,可以往合约发送非转账交易,如果交易里带有转账信息,交易会被 revert;如果是 payable 的,自然也就可以接受转账了。
5.尽管 fallback 可以是 payable 的,但并不建议这么做,声明为 payable 之后,其所消耗的 gas 最大量就会被限定在 2300。

从 0.6 开始,solidity 引入了 abstract, virtual, override 几个关键字,继承关系需要用下面的写法。
abstract contract Employee {
function getSalary() public virtual;
}

contract Manager is Employee {
function increaseSalary() public {
}
function getSalary() public override {
}
}
对 try…catch 机制的支持
这是我们需要调用的外部合约接口
interface DataFeed { function getData(address token) external returns (uint value); }
contract FeedConsumer {
DataFeed feed;
uint errorCount;
function rate(address token) public returns (uint value, bool success) {
// 如果外部合约调用错误次数超过 10 次,就不再进行更多调用了
require(errorCount < 10);
try feed.getData(token) returns (uint v) {
return (v, true);
} catch Error(string memory /* 出错原因 */) {
// 这块儿代码只有在对 DataFeed 调用失败时才会执行,通常是不满足外部合约 require 语句条件或触发 revert 语句时所引起的调用失败
errorCount++;
return (0, false);
} catch (bytes memory) {
// 当外部调用触发 assert 语句或除 0 等比较严重错误时会执行这个 catch 块
errorCount++;
return (0, false);
}
}
}

abi-to-sol在线转换
https://gnidan.github.io/abi-to-sol/

发布合约
https://ropsten.etherscan.io/verifyContract-solc?a=0x9bbc5c7eeb4ac6facce81aba65fd9a1b8bb28cd0&c=v0.8.1%2bcommit.df193b15&lictype=1

一、数据类型,学一门语言,先看数据类型:
1.基础类型 value type
bit:位
一个二进制数据0或1代表1bit
byte:字节
存储空间的基本计量单位,如sql server中的varchar(60)即是指60个字节
1 byte = 8 bit
一个英文字母占一个字节
1 英文字母 = 1 byte = 8 bit
一个汉字占2个字节
1 汉字 = 2 byte = 16 bit
在这里插入图片描述
5.error ,0.8以上版本

2.引用类型 reference type
2.1数组 (字符串与bytes是特殊的数组,所以也是引用类型)
uint[5] array; 固定长度的数字,不能改变长度,不能用push等。

2.2struct (结构体),类似js object,用.访问成员
struct成员是任意type
value type、reference type、mapping都可以
也可以是别的struct、或者dynamic array
struct stu{
uint id;
string name;
mapping(uint=>string) maptest; //mapping即使在结构体内,初始化时也是可以忽略的
}
stu memory student2=stu({name:‘stu2’,id:5678}); // 设置

2.3map (映射)
mappingName[name] = “something”; //赋值
mappingName[name]; //获取值
第三方库:
mapping: github.com/ethereum/dapp-bin/blob/master/library/iterable_mapping.sol
mapping没有长度,无序,所以获取不到长度。不能用迭代访问。

// SPDX-License-Identifier: MIT
pragma solidity ^0.7.0;
contract MappingExample{
mapping(address => uint) public balances;

function update(uint amount) public returns (address addr){balances[msg.sender] = amount;return msg.sender;}

}
数据位置data location类型,分为memory(内存-临时)和storage(区块链-永久),通过在变量名前声明memory还是storage来定义该变量的数据位置。一般来讲,函数参数默认为memory,局部复杂类型(作用域为局部)以及状态变量(作用域为全局)属于storage类型。还有一个calldata与memory差不多,专门用于存储函数参数的,也不是永久存储。额外提一点,EVM的memory是基于stack的,stack可以临时存储一些小的局部变量。这些变量存储消耗的gas是不同的,storage最大>memory(calldata与memory差不多)较小>stack几乎免费

数组类型Arrays,长度可定可变,可以存储于storage和memory,元素类型可以是任何类型,但memory时不能是映射类型(就是键值对类型)。uint[5] array; 固定长度的数字,不能改变长度,不能用push等。
结构体struct(如下例子),与Go语言相同的设定,自定义类型,使用方式也与Go极为相似。类似js的对象,使用前需要声明。
mapping 类型就是键值对,现在最新语言都会给自身增加键值对数据结构的封装支持。mapping的声明方式为:
mapping(_KeyType => _ValueType)
键值对中间通过一个“=>”连接。元素内容,Solidity类型均可,与其他键值对使用差不多,遇到问题再深入研究。

3.地址类型 address
地址类型表示以太坊地址,长度为20字节,默认0x40个0。unit160(address)
地址可以使用 .balance方法获得余额,也可以使用 .transfer方法将余额转到另一个地址。

address x = 0x212;
address myAddress = this;
if (x.balance = 10)
x.transfer(10);
address的四个方法
send,call,callcode,delegatecall
例子:
发送以太币的send方法
//下面是send方法,涉及到以太币的情况可能用到payable,senddemo方法是可以发送以太币过去的,add.transfer(u)

pragma solidity ^0.4.1;
contract addressDemo{
function addressDemo() payable{
}

function sendDemo(address add){
uint u=1 ether;//以太币的最小单位是wei,最大单位是ether
add.transfer(u)
}
}
call方法,注意地址.call,和地址.delegatecall方法的区别,call是自己本身不发生改变,被调用的值发生改变,delegatecall是自己本省的值发生改变,被调用的不发生改变

pragma solidity ^0.4.1;

//首先定义了两个合约
contract A{
uint public p;
event e(address add,uint p)//为了观察声明一个事件
//定义了一个方法
function fun(uint u1,uint u2) {
p=u1+u2;//改变了A合约中的p,改变被调用者合约方法中的变量
e(msg.sender,p)//下面的2,3传给了fun方法

}

}

contract B{

uint public q;
bool public b;

//当我们用B中的call方法的时候,调用A中的某个方法的执行,只会改变A中某个方法的值,B中的某个方法的值不会发生改变,被调用的合约本身发生改变

function call1(address add) returns(bool){
b=add.call(bytes4(keccak256(“fun(uint256,uint256)”)),2,3)
return b;//下面的add调用call方法
}

//下面的delegatecall是会改变B中的某个方法,而A中的某个方法是不会有任何的改变,下面是自己的合约发生变化
function call2(address add) returns(bool){
b=add.delegatecall(bytes4(keccak256(“fun(uint256,uint256)”)),1,3)
return b;
}

}

例子:
pragma solidity ^0.4.0;

//定义一个合约
contract CA{
uint public p;
bytes public failmsg;
string public str;
event e(address add,uint p)
event e1(address add, bytes b);

//定义一个构造函数,构造方法
function CA(string _str) {
str=_str;//str重新被赋值
}
function fun(uint u1,uint u2) {
p=u1+u2;
e(msg.sender,p)
}
//构造一个匿名函数
function () {//下面就会执行这个匿名函数
failmsg=msg.data;
e1(msg.sender,failmsg)//
}
}

contract CB{
uint public q;
bool public b;
function call1(address add) returns(bool){
b=add.call(bytes4(keccak256(“fun(uint256,uint256)”)),2,3);
return b
}
function call2(address add) returns(bool){
b=add.delegatecall(bytes4(keccak256(“fun(uint256,uint256)”)),2,3);
return b
}

//使用的是call方法,上面发生的值发生改变,下面不发生改变
function call3(address add) returns(bool){
b=false
b=add.call(“aaaa”,2,4,5,54,3);
return b
}

//下面是本身自己的值发生改变,b的值发生改变
function call4(address add) returns(bool){
b=false;
b=add.delegatecall(“bbbb”,5,“10×2323”,43);//执行匿名函数
return b
}
}

二、变量类型:
1.状态变量
2.局部变量
3.全局变量:blockhash(uint blockNumber) returns (bytes32)、block.gaslimit (uint)、tx.gasprice (uint)

pragma solidity ^0.5.0;
contract SolidityTest {
uint storedData; // 状态变量
constructor() public {
storedData = 10; // 使用状态变量
}
}

三、作用域:
Public – 公共状态变量可以在内部访问,也可以通过消息访问。对于公共状态变量,将生成一个自动getter函数。
external-外包合约才能访问,内部访问要加this
Internal – 内部状态变量只能从当前合约或其派生合约内访问。
Private – 私有状态变量只能从当前合约内部访问,派生合约内不能访问。

四、data location, 4个位置:(https://www.lidihuo.com/solidity/solidity-variabledatalocation.html)

Storage
该存储位置存储永久数据,这意味着该数据可以被合约中的所有函数访问。可以把它视为计算机的硬盘数据,所有数据都永久存储。
保存在存储区(Storage)中的变量,以智能合约的状态存储,并且在函数调用之间保持持久性。与其他数据位置相比,存储区数据位置的成本较高。
Memory
内存位置是临时数据,比存储位置便宜。它只能在函数中访问。
通常,内存数据用于保存临时变量,以便在函数执行期间进行计算。一旦函数执行完毕,它的内容就会被丢弃。你可以把它想象成每个单独函数的内存(RAM)。
Calldata
Calldata是不可修改的非持久性数据位置,所有传递给函数的值,都存储在这里。此外,Calldata是外部函数的参数(而不是返回参数)的默认位置。
Stack
堆栈是由EVM (Ethereum虚拟机)维护的非持久性数据。EVM使用堆栈数据位置在执行期间加载变量。堆栈位置最多有1024个级别的限制。

花费gas:storage > memory(calldata) > stack

五、变量的数据位置
规则1 – 状态变量
状态变量总是存储在存储区storage中。(隐式地标记状态变量的位置)

pragma solidity ^0.5.0;
contract DataLocation {
// storage
uint stateVariable;
uint[] stateArray;
uint storage stateVariable; // 错误
uint[] memory stateArray; // 错误
}

规则2 – 函数参数与返回值
函数参数(值类型)包括返回参数(值类型)都存储在内存memory中。
return

pragma solidity ^0.5.0;
contract DataLocation {
// storage
uint stateVariable;
uint[] stateArray;
function calculate(uint num1, uint num2) public pure returns (uint result) {
return num1 + num2
}
}

规则3 – 局部变量
值类型的局部变量存储在内存中。但是,对于引用类型,需要显式地指定数据位置。

pragma solidity ^0.5.0;
contract Locations {
/* 此处都是状态变量 /
// 存储在storage中
bool flag;
uint number;
address account;
function doSomething() public {
/
此处都是局部变量 */
// 值类型
// 所以它们被存储在内存中
bool flag2;
uint number2;
address account2;
// 引用类型,需要显示指定数据位置,此处指定为内存
uint[] memory localArray;
}
}

不能显式覆盖具有值类型的局部变量。

function doSomething() public {
/* 此处都是局部变量 */
// 值类型
bool memory flag2; // 错误
uint Storage number2; // 错误
address account2;
}
规则4 – 局部storage的赋值,只能赋值给引用类型指针reference

// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.5.0 <0.8.0;
contract C{
uint[] x;
function f(uint[] memory memoryArray)public{
x = memoryArray; // 会把整个memory array复制进去storage中
uint[] storage y = x; // 把x的reference复制给y
y[7]; //回传x的第八个元素
y.pop(); // 会将x的最后一个元素删除
delete x; // 把x清空
// 下面2行会失败,要赋值指针,x就是指针
// y = memoryArray;
// delete y;

g(x); // 只会传达referenceh(x); // 会产生一个独立,暂时的复制,在memory中}function g(uint[] storage) internal pure{} // 因为只是修改指针,所以消耗gas少,便宜function h(uint[] memory) public pure{}// 全部复制一个新的对象,所以消耗gas多,贵。

}

规则5 – 外部函数的参数
Calldata是不可修改的非持久性数据位置,所有传递给函数的值,都存储在这里。此外,Calldata是外部函数的参数(而不是返回参数)的默认位置。
calldata
web3.sendTransaction(from,to,calldata)
calldata就是外部函数的参数

六、赋值数据的位置

七函数
状态可变性(mutability)

Private(私有):限制性最强,函数只能在所定义的智能合约内部调用。
Internal(内部):可以在所定义智能合约内部调用该函数,也可以从继承合约中调用该函数。
External(外部):只能从智能合约外部调用。 (如果要从智能合约中调用它,则必须使用 this。)
Public(公开):可以从任何地方调用。 (最宽松)

状态可变性(mutability)
view:用view声明的函数只能读取状态,而不能修改状态。不消化gas
pure:用pure声明的函数既不能读取也不能修改状态。不消化gas
payable:用payable声明的函数可以接受发送给合约的以太币,如果未指定,该函数将自动拒绝所有发送给它的以太币。

pragma solidity 0.8.10;
contract C {
uint balance = 0;
// Payable function that allows other contracts
// to send ether to this contract.
function deposit () payable public{
balance += msg.value;
}
}
no-payable 如果未指定,该函数将自动拒绝所有发送给它的以太币。

pragma solidity 0.8.10;
contract D { uint count = 0;
// non-payable function that reads and modifies state.
function increment() public returns(uint){
count += 1;
return count;
}
}

增删改查例子:

// SPDX-License-Identifier: MIT
// 可见性:public 状态可变性:空
// 此函数将用户的ID删除,如果找到,则将其从数组中删除;如果用户不存在,则回退交易。
// 提示:由于最后三个函数都需要查找用户,因此你将需要创建一个私有函数,该函数将获取用户的ID并在数组中返回其索引(如果找到),以避免重复相同的代码。
pragma solidity ^0.7.0;

contract Crud {

struct User {uint256 id;string name;}User[] public users;uint256 public nextId = 1;function add(string memory name) public {User memory user = User({id : nextId, name : name});users.push(user);nextId++;}function read(uint256 id) public view returns(string memory){uint256 i = find(id);return users[i].name;}function update(uint256 id, string memory newName) public {uint256 i = find(id);users[i].name = newName;}function destroy(uint256 id) public {uint256 i = find(id);delete users[i];}function find(uint256 id) private view returns(uint256){for(uint256 i = 0; i< users.length; i++) {if(users[i].id == id)return i;}revert("User not found");}

}
二、Solidity 0.4.10 版本发布了新的 assert() , require() 和 revert() 函数,解决了以前代码中有困惑的地方。
参考:https://blog.csdn.net/qq_33829547/article/details/80377689
https://remix.ethereum.org/#optimize=false&runs=200&evmVersion=null&version=soljson-v0.7.1+commit.f4a555be.js
Solidity 0.4.10之前(以及其后一段时间),这种强制授权处理方式很普遍:
if (msg.sender != owner) { throw; }

完全等价于如下三种形式(新方法):

if(msg.sender != owner) { revert(‘Something bad happened’); }

assert(msg.sender == owner);

require(msg.sender == owner, ‘Something bad happened’);// 参数1不成立,执行参数2. 如果参数1成立,执行require

注意在 assert() 和 require() 例子中的条件声明,是 if 例子中条件块取反,也就是用 ==代替了 != 。
revert require assert
代替throw – –
允许返回一个数值 允许返回一个数值
将剩余gas返还调用者 将剩余gas返还调用者 不返还gas
if后面使用 在函数最开始的地方使用 在函数结尾处使用
一般地,尽量使用 require 函数,require 应该在函数最开始的地方使用

一般地,尽量少使用 assert 调用,assert 应该在函数结尾处使用
除非认为之前的检查(用 if 或 require )会导致无法验证 overflow,否则不应该盲目使用 assert 来检查 overflow”

一、程序的授权码// SPDX-License-Identifier: GPL-3.0
可以从这里挑:https://spdx.org/licenses/
不开发直接写:// SPDX-License-Identifier: UNLICENSED
二、版本:pragma solidity =0.8.7; //合约版本
三、合约
contract Vote {

}