前言
版权声明:
表示合约采用MIT许可证:(补充:MIT 许可证是一种宽松的开源许可证,允许用户自由使用、修改和再发布代码,只需要包含原始许可声明即可。)
//SPDX-License-Indentifier:MIT
版本声明:(用来指示编译器按照solidity的哪一个版本来编译智能合约)
pragma solidity ^0.8.0;
//指定固定版本: 可以使用固定的版本号来声明 Solidity 的版本pragma solidity 0.8.0;//指定兼容版本范围: 有时候我们希望指定一个兼容版本的范围,可以使用 ^ 符号,表示与指定版本兼容的范围以下例子(>=0.8.0,=, , =0.6.0 <0.9.0;
版权声明和版本声明在智能合约里面都是必不可少的,一般版权声明如上不改变,版本声明可根据自己需要进行修改
智能合约是在区块链上执行的自动化合约(大致模块)
它们由 Solidity 或其他智能合约编程语言编写,用于定义和执行特定的交易规则。智能合约通常是以太坊或其他区块链平台上的一个重要组成部分,可以实现各种去中心化应用程序(DApp)。
1.声明合约: 首先,智能合约会声明合约名字及版本。例如
pragma solidity ^0.8.0;contract MyContract {// 合约代码}
2.状态变量: 在合约内部,会声明状态变量来存储合约的状态信息。状态变量的值会被永久性地存储在区块链上,并且可以被读取和修改。
contract MyContract {uint public myVariable;}
3.构造函数: 合约可以包含一个构造函数,在合约部署时被调用,用于初始化合约的状态。构造函数只会被调用一次。
contract MyContract {constructor() {// 构造函数的初始化操作}}
4.函数: 智能合约会包含各种函数,用于定义合约的行为。这些函数可以被外部调用,执行特定的操作并返回结果。
contract MyContract {function myFunction() public returns (uint) {// 函数的执行逻辑}}
5.事件: 智能合约可以定义事件,用于记录合约中的重要状态变化,以便外部应用程序监听并做出相应的响应。
contract MyContract {event MyEvent(address indexed _from, uint _value);function myFunction() public {// 触发事件emit MyEvent(msg.sender, 100);}}
数据类型:值类型和引用类型
值类型
值类型是直接存储数据的实际值,而不是指向存储位置的引用。当使用值类型时,会将数据的副本传递给其他变量或函数
1.布尔类型(bool):表示真(true)或假(false)。
bool myBool = true;
2.整数类型(int、uint):表示有符号和无符号整数,可以指定位数(如 int8、uint256)(但是指定的位数都必须是8的倍数)。
int8 myInt = -10;uint256 myUint = 100;
3.定长字节数组(bitesN):定长字节数组是一个具有固定长度的字节数组。你可以用 bytesN
来声明定长字节数组,其中 N
是字节数。
bytes32 myBytes = 0xabcdef1234567890;
4.地址类型(address):存储20字节的以太坊地址
address myAddress = 0x7aDf8dA6CCE4D2bACd8C969e6e4EaF2D15614474;//地址类型被用来存储以太坊网络中的合约地址或者外部账户地址。地址类型是一个 20 字节长的固定大小字节数组function sendEther(uint256 _amount) public payable {require(msg.value == _amount, "Incorrect amount sent");address payable recipient = payable(myAddress);recipient.transfer(_amount);}//在 sendEther() 函数中,我们向合约发送以太币,并要求发送的以太币数量与指定的 _amount 参数相等。然后,我们使用 payable 将 myAddress 转换为可支付地址类型,并使用 transfer() 方法将指定数量的以太币发送到该地址
属性:
- balance:返回地址对应账户的余额。
address myAddress = 0x1234567890123456789012345678901234567890;uint256 balance = myAddress.balance;
方法:
- transfer(uint256 amount):向地址发送指定数量的以太币。如果发送失败,则会回滚所有状态更改。
address payable recipient = payable(0x1234567890123456789012345678901234567890);uint256 amountToSend = 1 ether;recipient.transfer(amountToSend);
- send(uint256 amount) returns (bool):向地址发送指定数量的以太币,返回一个布尔值表示发送是否成功。
address payable recipient = payable(0x1234567890123456789012345678901234567890);uint256 amountToSend = 1 ether;bool success = recipient.send(amountToSend);
5.枚举类型(enum):定义了一组明明常量
enum MyEnum { VALUE1, VALUE2, VALUE3 }MyEnum myEnum = MyEnum.VALUE2;//使用枚举型声明变量并给它赋值//枚举型是用户自定义数据类型//枚举型使用关键词enum定义//枚举型由一组预定义常量组成,枚举成员会按照定义的顺序从 0 开始依次递增。例如MyEnum.VALUE1=0;MyEnum.VALUE1=1依次递增;//在 Solidity 中,枚举类型可以隐式地转换为整数类型(uint8)。//枚举类型通常在全局作用域里面声明(有时候也在结构体但是比较少),不能在函数里面声明
引用类型(Reference Types)
引用类型存储数据的引用(即存储位置),而不是实际的数据值。在处理引用类型时,操作的是数据在存储器中的位置,而不是数据本身
1.动态数组(dynamic ary):在 Solidity 中,数组是引用类型,可以根据需要动态调整大小。
uint[] myArray;//myArray就是定义的变量名,动态数组的定义比较独特
2.映射(mapping):Mapping 类型类似于哈希表,它将键映射到值。Mapping 可以用来创建键值对的存储结构,并在 O(1) 的时间复杂度下查找、插入和删除键值对。
mapping(address => uint) balances;
3.结构体(struct):自定义数据结构,可以包含多个不同类型的数据成员
struct Person {string name;uint age;}Person myPerson;
变量
状态变量的可见性:
- public:状态变量被声明为
public
后,Solidity 会自动生成一个对应的 getter 函数,使得该变量可以被外部合约或外部调用者读取。如果合约是其他合约的基类,那么继承合约可以访问父合约中声明为public
的状态变量。
contract Example {uint256 public myPublicVar;}
- private:状态变量被声明为
private
后,只有当前合约内部可以访问该变量,外部合约无法直接访问。
contract Example {uint256 private myPrivateVar;}
- internal:状态变量被声明为
internal
后,只有当前合约及其派生合约(继承合约)可以访问该变量,外部合约无法直接访问。
contract Example {uint256 internal myInternalVar;}
变量的默认值:
contract DefaultValues {// bool类型变量默认值为falsebool defaultBool = false;// int类型变量默认值为0int defaultInt = 0;// uint类型变量默认值为0uint defaultUint = 0;// address 类型变量默认值为0X000...(40个0)address defaultAddress = address(0);// bytes类型变量默认值为0x000...(64个0)bytes32 defaultBytes = 0x0;// string类型变量默认值为""(空字符)string defaultString = "";// 枚举类型变量默认值为它列表的第一项值enum MyEnum { First, Second, Third }MyEnum defaultEnum = MyEnum.First;}
复合类型变量的默认值:
又每一项变量的默认值组成;
delete操作符:
在 Solidity 中用于将变量重置为其类型的默认值。它可以应用于各种类型的变量,包括基本类型、数组、映射和结构体等。使用 delete
操作符可以将变量恢复到其初始状态,这对于清理合约状态或重置变量非常有用
常量constant和immutable:
常量的命名规则和变量相同,但通常使用大写字母表示,单词之间用下划线”_”连接,
相同点:都能限制对状态变量的修改。
- 常量 (
constant
):常量是在编译时确定并存储在代码中的值。常量可以在合约内部或函数内部声明,并且作用域仅限于声明所在的作用域。常量在声明时必须进行初始化,并且不能被修改。pragma solidity ^0.8.0;contract ConstantExample {// 在合约内部声明常量uint constant internal MY_CONSTANT = 42;function getConstant() public pure returns (uint) {// 在函数内部使用常量uint myValue = MY_CONSTANT;return myValue;}}
- 不可变 (
immutable
):不可变变量是在合约部署时确定并存储在区块链上的值。不可变变量在声明时必须进行初始化,并且只能在构造函数中赋值一次。不可变变量的值可以是在编译时可以确定的常量,也可以是在部署时可以确定的值pragma solidity ^0.8.0;contract ImmutableExample {// 在合约内部声明不可变变量uint immutable public MY_IMMUTABLE;constructor() {// 在构造函数中给不可变变量赋值MY_IMMUTABLE = block.timestamp;}function getImmutable() public view returns (uint) {return MY_IMMUTABLE;}}
ps:常量声明时需要直接赋值,且不能在构造函数中给常量赋值;而不可变变量通常在构造函数中初始化,可以在构造函数中给不可变变量赋值
区别两个方面:
1.初始化时机:constant修饰的状态变量必须在声明时就立即显示赋值,之后就不在允许修改。Immutable修饰的状态变量既可以在声明时显示赋值,又可以在构造的函数中赋值。
2.Constant可以修饰任何数据类型,immutable只能修饰值类型(int ,uint, bool, address)
Ps:使用常量的好处
- 代码可读性
- 代码重用
- 预防错误
- 节省Gas
函数:
function 函数名称() return(){return(变量1,变量2);//多个返回值时候适用return 变量;//一个返回值适用}
1.函数的语法:
function 函数名() return (){
}
命名规则:
1.函数是由字母,数字下划线组成的。
2.以小写字母或者下划线开头。
3.采用驼峰形式。
可见性:
函数的可见性分为四种:
- private:只能在所属(本地)的智能合约内部调用
- public修饰的函数可以从任何地方调用,既可以在智能合约的内部调用也可以在智能合约的外部调用
- internal可在所属的只能合约里调用,也可以在继承的合约里调用
- external只能在智能合约的外部调用不能在智能合约的内部调用
函数的返回值:
在solidity中函数可以没有返回值,也可以有一个或者多个返回值。Ps有多个返回值的时候需要用括号包裹全部返回值变量,
函数状态可变性:
1.pure(指状态函数不会修改和读取合约的状态(数据))
function sum() public pure returns(uint){uint a = 2;//a是一个局部变量uint b = 3;//b是一个局部变量return a + b;//返回局部变量的和}//该函数并没有使用任何状态变量因此其不会读取区块链的状态,也不会改变合约的状态。即该函数不会和区块阿里区块链发生任何的关系。
//如果函数中出现以下语句则被视为读取了状态数据,就不能使用pure函数否则无法通过编译1.读取状态变量2.访问.balance3.访问任何区块,交易,msg等全局变量4.调用任何不是纯函数的函数5.使用包含特定操作码的内联汇编
2..view(函数会读取合约的状态但是并会修改,即函数会读取链上的数据但是并不会对数据进行修改)
contract View{uint a = 1;//a即为状态变量function test(uint num) public view returns(uint){ return num * a;//读取并使用了状态变量a }}
//函数中存在以下语句则被视为修改了状态数据,就不能使用可见性view了1.修改状态变量2.触发事件3.创建其他合约4.使用自毁函数:self destruct5.调用发送以太币6.调用非view或pure函数7.使用底层调用8.使用特定操作码的内联汇编
3.payable(表明这个函数可以接收以太币)
contract Payable{function test(uint ID ) public payable{//标记为payable,表示它可以接收以太币 //这些以太币市调用者在调用函数时支付的,由于payble函数接受了以太币,所以它是改变合约状态的 }}
4.未标记(意味着这个函数是要改变合约状态的,即修改合约的状态变量或者向其它合约发送交易等)
contract unmarked{uint a = 1;//a即为状态变量function test(uint num) public view returns(uint){ a = num; return num * a;//读取并修改了状态变量a }}
小结:函数的状态可变性的设计目的是为了确保合约的安全性,可靠性,和互操作性
1.安全性:编译时对函数的行为进行检验, 帮助开发者避免不经意间修改合约的状态或者方位未经授权的外部合约,从而减少潜在的安全漏洞。
2.可靠性:通过明确函数的状态可变性,合约的使用者可以更好地理解和预测函数的行为。
3.互操作性:通过标记函数的状态可变性,可以提供给其他合约和工具有关函数的重要信息。
ps:为了减少gas费用尽量使用pure和view函数。
特殊函数:构造函数constructor和receive函数
1.构造函数constructor:
1.自动执行合约部署时自动执行,不能手动调用或再次执行2.仅执行一次仅仅在合约创建时执行一次*3.通常用于状态变量初始化可以在构造函数中传递参数来初始化合约的状态变量。这些参数可以在部署合约时提供。*4.命名与可见性: 构造函数的名字必须与合约名相同,并且不能有返回类型或任何函数修饰符(如public、external等)。构造函数默认为internal可见性,不需要显式声明可见性。// SPDX-License-Identifier: MITpragma solidity ^0.8.0;contract MyContract {uint public myNumber;constructor(uint _number) {myNumber = _number;}}//构造函数的可见性默认为public可以被任何人调用//构造函数的可变性不能设置为pure或者view,但可以为payable属性。因为构造函数通常是用来初始化变量,它是会修改合约状态的。
receive函数:
补充知识:账户类型
1.外部账户 EOA,
例如小狐狸里面的账户
账户地址是以”0x”开头长度为20字节的十六进制数
外部账户都有对应的私钥,只有持有私钥的人才能对交易进行签名, 所以外部账户适用于资金管理的身份验证。
2.内部账户 CA:
在以太坊部署一个合约之后都会产生一个对应的账户称为合约账户,合约账户只要用于托管只能合约,它里面包含着智能合约的二进制代码和状态消息,合约账户也会有一个账户地址是以”0x”开头长度为20字节的十六进制数。合约账户没有私钥只能由合约代码中的逻辑控制。在一定条件下可以存入ETH(以太币)。
只有定义了receive函数或者fallback函数才能接收以太币
//1.无需使用function声明//2.参数为空//3.可见性必须为external//4.可变性必须为payablepragma solidity ^0.8.0;contract MyContract {、receive() external payable {// 处理接收的以太币}}
事件
在 Solidity 中,事件(Event)是一种特殊的合约结构,用于记录合约中的重要状态变化或触发的操作。事件可以被合约内部的函数触发,并且可以被外部应用程序监听和响应。
事件是 Solidity 中用于记录重要状态变化和操作的机制,可以方便地与外部应用程序进行交互,提供更多的可观察性和可追溯性。
事件语法
event EventName(arg1Type arg1, arg2Type arg2, ...);//EventName:事件的名称,使用驼峰命名法。//arg1Type、arg2Type:事件参数的类型。
触发事件:
emit EventName(arg1Value, arg2Value, ...);
pragma solidity ^0.8.0;contract EventExample {event NewUser(address indexed userAddress, string username);function registerUser(string memory username) public {// 用户注册逻辑emit NewUser(msg.sender, username);}}
这是一个简单示例展示了如何定义触发一个事件
在上述示例中,合约 EventExample
定义了一个名为 NewUser
的事件。NewUser
事件有两个参数:userAddress
(用户地址)和 username
(用户名)。当调用 registerUser
函数进行用户注册时,会触发 NewUser
事件,并传递相应的参数值。
require
在 Solidity 中,require
是一种异常处理机制,用于在函数执行过程中验证条件是否满足。如果条件不满足,require
会抛出异常并中止函数执行,同时回滚所有状态更改。通常用于输入参数验证、前置条件检查和合约内部状态验证等场景。
语法
require(condition, errorMessage);//condition:要验证的条件表达式,如果为 false,则触发异常。//errorMessage:可选参数,用于指定异常信息,将在异常发生时显示
使用背景:
1.输入参数验证
function deposit(uint256 amount) public {require(amount > 0, "Deposit amount must be greater than 0");// Deposit logic}
2.前置条件检查:
function transfer(address to, uint256 amount) public {require(to != address(0), "Invalid recipient address");require(amount <= balances[msg.sender], "Insufficient balance");// Transfer logic}
3.合约内部状态验证:
function withdraw(uint256 amount) public {require(amount <= address(this).balance, "Insufficient contract balance");// Withdraw logic}
4.复杂条件检查:
function buyToken(uint256 amount) public {require(amount >= minPurchase && amount <= maxPurchase, "Invalid purchase amount");// Token purchase logic}
修饰器:
在 Solidity 中,修饰器(Modifier)是一种特殊的函数,用于修改函数的行为。通过使用修饰器,可以在执行函数之前或之后添加额外的逻辑或条件检查,以确保函数按照预期方式运行。
修饰器通常用于以下情况:
- 权限控制:通过修饰器可以实现权限控制,限制只有特定账户可以调用某个函数。
- 状态检查:在函数执行之前进行状态检查,确保满足特定条件才能执行函数。
- 日志记录:可以在函数执行前后记录事件,用于日志记录或跟踪功能。
- 代码复用:可以将常用的逻辑包装在修饰器中,然后在多个函数中重复使用。
修饰器由关键字 modifier
定义,可以在函数定义前使用 modifier
进行修饰。修饰器可以接收参数,并且可以通过 _;
在修饰器内部标记函数应该被执行的位置。
下面是一个简单的示例,演示了如何在 Solidity 中定义和使用修饰器:
pragma solidity ^0.8.0;contract MyContract {address public owner;modifier onlyOwner {require(msg.sender == owner, "Only owner can call this function");_; // 执行被修饰的函数}constructor() {owner = msg.sender;}function changeOwner(address _newOwner) public onlyOwner {owner = _newOwner;}}
在上面的示例中,onlyOwner
是一个修饰器,用于限制 changeOwner
函数只能被合约的所有者调用。修饰器首先检查调用者是否是合约的所有者,如果是则继续执行函数,否则中断函数执行并抛出异常。
solidity运算符:
算术运算符:
+
:加法-
:减法*
:乘法/
:除法%
:取模(取余)
比较运算符:
==
:等于!=
:不等于<
:小于>
:大于<=
:小于等于>=
:大于等于
逻辑运算符:
&&
:逻辑与(and)||
:逻辑或(or)!
:逻辑非(not)
位运算符:
&
:按位与|
:按位或^
:按位异或~
:按位取反<<
:左移>>
:右移
赋值运算符:
=
:赋值+=
:加后赋值-=
:减后赋值*=
:乘后赋值/=
:除后赋值%=
:取模后赋值&=
:按位与后赋值|=
:按位或后赋值^=
:按位异或后赋值<<=
:左移后赋值>>=
:右移后赋值
三元条件运算符:
condition ? value1 : value2
:如果条件成立,则返回value1,否则返回value2。
流程控制语句:
条件语句(if、else if、else):
if
语句用于在满足指定条件时执行特定代码块。else if
可以在前面的if
条件不满足时再次进行条件判断。else
用于处理所有其它情况,即前面所有条件都不满足时执行的代码块。if (condition1) {// 条件1成立时执行的代码} else if (condition2) {// 条件2成立时执行的代码} else {// 所有条件都不成立时执行的代码}
循环语句(for、while、do-while):
for
循环用于按照指定条件重复执行一段代码。while
循环在每次迭代之前检查条件是否成立,如果条件成立则执行代码块。do-while
循环先执行一次代码块,然后在每次迭代之前检查条件是否成立。for (uint i = 0; i < 10; i++) {// 循环体内的代码}while (condition) {// 循环体内的代码}do {// 循环体内的代码} while (condition);
异常处理语句(try/catch):
try
语句用于包裹可能抛出异常的代码块。catch
语句用于捕获并处理try
代码块中抛出的异常。try {// 可能会抛出异常的代码块} catch (ExceptionType variableName) {// 处理异常的代码块}