无论风暴将我带到什么岸边,我都将以主人的身份上岸
目录
一、Solidity的单位
1. 货币Ether
2. 时间单位Time
二、地址的形成
三、以太坊的账户
1.内部账户(简称CA)
2.外部账户(简称EOA)
3.内部账户和外部账户的比较
4.判断是内部账户还是外部账户的方法
四、消息调用和余额查询
五、交易
六、Solidity this 和 msg.sender 、msg.value的用法
七、Solidity的转账函数
1.transfer()
2.send()
3.call()
4.transfer、send、call的区别和用法
八、Solidity 接收函数
九、Solidity 发送Eth
十、Solidity 支付Eth
十一、Solidity 回退函数
Ether的单位关键字有wei, gwei, finney, szabo, ether,换算格式如下:
- 1 ether = 1 * 10^18 wei
- 1 ether = 1 * 10^9 gwei
- 1 ether = 1 * 10^6 szabo
- 1 ether = 1* 10^3 finney
pragma solidity 0.4.20;
/**
* 对 比特币 Ether 的几个单位进行测试
*/
contract testEther {
// 定义全局变量
uint public balance;
function testEther() public{
balance = 1 ether; //1000000000000000000
}
function fFinney() public{
balance = 1 finney; //1000000000000000
}
function fSzabo() public{
balance = 1 szabo; //1000000000000
}
function fWei() public{
balance = 1 wei; //1
}
}
Time的单位关键字有seconds, minutes, hours, days, weeks, years,换算格式如下:
- 1 == 1 seconds
- 1 minutes == 60 seconds
- 1 hours == 60 minutes
- 1 days == 24 hours
- 1 weeks == 7 days
- 1 years == 365 days
//他们都会放在uint中,全部转换为秒单位
//变量now将返回当前的unix时间戳(自1970年1月1日以来到现在的秒数)
return now
return 1 days ---> unit秒数
如果你需要进行使用这些单位进行日期计算,需要特别小心,因为不是每年都是365天,且并不是每天都有24小时,因为还有闰秒。由于无法预测闰秒,必须由外部的oracle来更新从而得到一个精确的日历库(内部实现一个日期库也是消耗gas的)。
pragma solidity 0.4.20;
/**
* 对 Time 单位进行测试
*/
contract testTime {
// 定义全局变量
uint time;
function testTime() public{
time = 100000000;
}
function fSeconds() public view returns(uint){
return time + 1 seconds; //100000001
}
function fMinutes() public view returns(uint){
return time + 1 minutes; //100000060
}
function fHours() public view returns(uint){
return time + 1 hours; //100003600
}
function fWeeks() public view returns(uint){
return time + 1 weeks; //100604800
}
function fYears() public view returns(uint){
return time + 1 years; //131536000
}
}
1.地址是由公钥Keccak-256单向哈希,取最后20个字节(160位)派生出来的标识符
2.在solidity中,地址类型使用address来表示
3.地址类型在以太坊中非常重要,因为以太坊的账户需要用地址来表示
4.地址占用20个字节,共160位,即以太坊的地址长度是20个字节
5.地址类型的声明 address 地址名=0x十六进制数
地址是所有合约的基础,支持的比较运算符有 < > <= >= == !
以太坊中的两类账户:
- 内部账户:由智能合约的代码控制
- 外部账户 :由密钥控制
以太坊中账户不用申请,而实用户根据需要在钱包中生成,然后连接到以太坊
它们共用EVM中同一个地址空间 账户地址空间
无论账户是否存储代码,这两类账户对EVM来说处理方式是一样的
每个账户在EVM中都有一个键值对形式的持久化存储,其中key和value的长度都是256位
账户信息也就是地址信息
内部账户也就是合约账户,合约地址就代表该内部账户地址
有的以太币余额,有关联代码,可通过交易或者来自其他合约的调用信息来触发代码执行
执行代码时可以操作自己的存储空间,也可以调用合约,没有私钥控制,其codeHash非空
外部账户:
外部账户就是非合约账户,是由第三方钱包app所创建的账户,例如metamask
有对应的以太币余额,没有关联代码,可发送交易(转币或促发合约代码),由用户私钥控制,其codeHash为空
比较 | 外部账户 | 合约账户 |
---|---|---|
拥有私钥 | 是 | 否 |
codeHash内容 | 为空 | 非空 |
主动发起交易 | 是 | 否,只能被动发起交易 |
拥有余额 | 是 | 是 |
地址长度 | 20字节 | 20字节 |
采用extcodesize来判断,它可以获取地址关联代码长度
通过判断账户关联代码长度
合约地址大于0 外部账户地址为0
//extcodesize获取地址关联代码长度 合约地址大于0 外部账户地址为0
contract IsCadd {
function isContract(address addr) returns (bool) {
uint size;
assembly { size := extcodesize(addr) }
return size > 0;
}
}
合约可以通过消息调用的方式来调用其他合约或者发送以太币到非合约账户
balance属性用于查询账户余额
格式:
地址名.balance
1.签名的数据包,可以包含二进制数据负载和以太币,由外部账户发送到另一个账户的消息
2.既可以是外部账户的交易也可以是内部账户的交易
3.交易一经创建,每笔交易都需要消耗gas,目的是限制执行交易所需的工作量和为交易支付手续费。EVM执行交易时,gas将按特定规则逐渐耗尽
4.gas price是交易发送者设置的一个值,发送者账户需要预支付手续费=gas_price*gas。如果交易后还有剩余,gas会原路返还
Solidity 中 this 代表合约对象本身,可以通过
address(this)
获取合约地址。合约地址与合约创建者地址、合约调用者地址并不相同。Solidity 中
msg.sender
代表合约调用者地址。一个智能合约既可以被合约创建者调用,也可以被其它人调用。合约创建者,即合约拥有者,也就是指合约部署者,它的地址可以在合约的
constructor()
中,通过msg.sender
获得,因为合约在部署的时候会首先调用constructor()
。Solidity中mas.value代表调用者输入的值,通常用来让调用者自定义转账数量
1. 范例
// SPDX-License-Identifier: MIT pragma solidity ^0.8.0; contract SolidityTest { address public owner; event log(address); constructor() { owner = msg.sender; emit log(msg.sender); emit log(address(this)); } }owner 被赋值为合约部署者的地址。
log(msg.sender) 在日志中输出了合约部署者的地址。
log(address(this)) 在日志中输出了合约地址。
查看合约在部署时的日志结果:
[ { "from": "0xE3Ca443c9fd7AF40A2B5a95d43207E763e56005F", "topic": "0x2c2ecbc2212ac38c2f9ec89aa5fcef7f532a5db24dbf7cad1f48bc82843b7428", "event": "log", "args": { // 合约部署者的地址 "0": "0x5B38Da6a701c568545dCfcB03FcB875f56beddC4" } }, { "from": "0xE3Ca443c9fd7AF40A2B5a95d43207E763e56005F", "topic": "0x2c2ecbc2212ac38c2f9ec89aa5fcef7f532a5db24dbf7cad1f48bc82843b7428", "event": "log", "args": { // 合约地址 "0": "0xE3Ca443c9fd7AF40A2B5a95d43207E763e56005F" } } ]
使用 Solidity 智能合约转账可以使用 transfer 函数等其他转账函数。智能合约里面需要有一定的以太,不然合约将无法给调用者发送以太,可以在创建合约时给合约发送一定的以太来测试。
转账双方可以是:
1.外部账户向外部账户转账
2.内部账户向内部账户转账
3.外部向内部,内部向外部账户转账
具有转账功能的智能合约的 constructor 必须显式的指定为 payable。,具有接收和转账功能的函数也需要加上payable
谁给谁转就消耗谁的余额,谁调用函数就消耗谁的gas
注意事项:非payble类型地址不能
transfer()方法
接收者地址.transfer(数量)
如果当前合约的余额不够大或者 Ether转账被接收账户拒绝,转账功能将失败。接收方智能合约应定义回退函数,否则转账调用将引发错误。transfer函数在失败时恢复。另外它被硬编码以防止重入攻击(这句话不是很能理解)。
// SPDX-License-Identifier: MIT pragma solidity ^0.7.0; contract cs{ constructor() payable{ } function getETH() public{ require(address(this).balance>=1 ether,"no money"); address payable _owner = msg.sender; _owner.transfer(1 ether); } fallback() external{ } receive() payable external{ } }
格式:
接收者地址.send(数量)
Send是和Transfer具有同等功能的低级api。如果执行失败,当前合约不会因为异常而停止,但会返回false。
function send(address payable _to) public payable { bool isSend = _to.send(msg.value); require(isSend, "Send fail"); }
格式:
接收者地址.call(数量)
这是将 ETH 发送到智能合约的推荐方式。空参数触发接收地址的回退功能(fallback function)
function calls(address payable _to) public payable { (bool isSuccess, /* memory data */ ) = _to.call{value: msg.value}(""); require(isSuccess, "Failure! Ether not send."); }
使用call,还可以触发合约中定义的其他功能,并发送固定数量的gas来执行该功能。交易状态作为布尔值发送,返回值在数据变量(bytes memory data)中发送。
更具体使用的格式如下:
(bool sent, bytes memory data) = _to.call{gas :10000, value: msg.value}("func_signature(uint256 args)");
2019年,solidity官方已经弃用了send和transfer,推荐call方法进行转账操作,但还是要小心使用官方给出了的警告
1. transfer
- 如果异常会转账失败,抛出异常(等价于require(send()))(合约地址转账)
- 有gas限制,最大2300
- 函数原型:.transfer(uint256 amount)
2. send
- 如果异常会转账失败,仅会返回false,不会终止执行(合约地址转账)
- 有gas限制,最大2300
- 函数原型:.send(uint256 amount) returns (bool)
3. call
- 如果异常会转账失败,仅会返回false,不会终止执行(调用合约的方法并转账)
- 没有gas限制
- .call(bytes memory) returns (bool, bytes memory)
共同点
- addr.transfer(1 ether)、addr.send(1 ether)、addr.call.value(1 ether)的接收方都是addr。
- 如果使用addr.transfer(1 ether)、addr.send(1 ether),addr合约中必须增加fallback回退函数!
- 如果使用addr.call.value(1 ether),那么被调用的方法必须添加payable修饰符,否则转账失败!
// SPDX-License-Identifier: MIT pragma solidity ^0.7.0; contract cs{ constructor() payable{ } function getETH() public returns(bool) { address payable _owner = msg.sender; return(_owner.send(1 ether)); } # 如果使用transfer或send函数必须添加fallback回退函数 fallback() external{ } receive() payable external{ } } }
solidity 接收函数 receive 没有参数、没有返回值。
solidity 向合约转账,发送 Eth,就会执行 receive 函数。
如果没有定义接收函数 receive,就会执行 fallback 函数。
向合约转账
// SPDX-License-Identifier: MIT pragma solidity ^0.8.0; contract Fallback { event eventFallback(string); fallback() external payable { emit eventFallback("fallbak"); } receive() external payable { emit eventFallback("receive"); } // 查看合约账户余额 function getBalance() external view returns(uint) { return address(this).balance; } }我们向合约 Fallback 发送一笔 123 wei 的交易,查看日志:
[ { "from": "0xd457540c3f08f7F759206B5eA9a4cBa321dE60DC", "topic": "0x39684f4c14ee0aafaa34ed83629676cd0fbe71653659c3353ef0c33f630e7eab", "event": "eventFallback", "args": { "0": "receive" } } ]我们调用合约 Fallback 的 getBalance 方法,查看合约地址的余额为 123 wei。
receive 和 fallback 调用流程
向一个合约发送 Eth,何时调用 receive 或者 fallback 呢?下面是两者的调用流程。
发送 Eth | msg.data 是否为空 / \ 是 否 / \ 是否定义了receive fallback / \ 是 否 / \ receive fallback
Solidity 在智能合约中有三种方式发送 Eth。
transfer:使用 transfer 发送 Eth,会带有 2300 个gas,如果失败,就会 revert。
send:使用 send 发送 Eth,会带有 2300 个gas,并且返回一个 bool 值表示是否成功。
call:使用 call 发送 Eth,会发送所有剩余的 gas,并且返回表示是否成功 bool 值和 data 数据。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract SendEther {
constructor() payable{}
// 允许接收 Eth
receive() external payable {}
function transferEth(address payable _to) external payable {
_to.transfer(100);
}
function sendEth(address payable _to) external payable {
bool success = _to.send(100);
require(success, "send failed");
}
function callEth(address payable _to) external payable {
(bool success, ) = _to.call{value:100}("");
require(success, "call failed");
}
}
contract ReceiveEther {
event log(uint amount, uint gas);
// 允许接收 Eth
receive() external payable {
emit log(msg.value, gasleft());
}
}
使用 payable 标记的 Solidity 函数可以用于发送和接收 Eth。payable 意味着在调用这个函数的消息中可以附带 Eth。
使用 payable 标记的 Solidity 地址变量,允许发送和接收 Eth。
// SPDX-License-Identifier: MIT pragma solidity ^0.8.0; contract Payable { // owner 可用于收费 eth address payable public owner; constructor() { // msg.sender 默认不能收发 eth,需转换 owner = payable(msg.sender); } function deposit() external payable{ } }payable 地址变量可以通过 balance 属性,来查看余额。
// SPDX-License-Identifier: MIT pragma solidity ^0.8.0; contract Payable { function deposit() external payable{ } function getBalance() external view returns(uint) { return address(this).balance; } }
solidity 回退函数 fallback 没有参数、没有返回值。
solidity 回退函数在两种情况被调用:
- 向合约转账,发送 Eth,就会执行Fallback函数
- 如果请求的合约方法不存在,就会执行Fallback函数
向合约转账
// SPDX-License-Identifier: MIT pragma solidity ^0.8.0; contract Fallback { event eventFallback(string); fallback() external payable { emit eventFallback("fallbak"); } // 查看合约账户余额 function getBalance() external view returns(uint) { return address(this).balance; } }我们向合约 Fallback 发送一笔 123 wei 的交易,查看日志:
[ { "from": "0xd457540c3f08f7F759206B5eA9a4cBa321dE60DC", "topic": "0x39684f4c14ee0aafaa34ed83629676cd0fbe71653659c3353ef0c33f630e7eab", "event": "eventFallback", "args": { "0": "fallbak" } } ]我们调用合约 Fallback 的 getBalance 方法,查看合约地址的余额为 123 wei。
请求的合约方法不存在
// SPDX-License-Identifier: MIT pragma solidity ^0.8.0; contract Fallback { event eventFallback(string); fallback() external payable { emit eventFallback("fallbak"); } } contract SoldityTest { // 外部合约 address private fb; constructor(address addr) { fb = addr; } function callFallback() external view returns(string memory) { // 调用合约 Fallback 不存在的方法 echo() bytes4 methodId = bytes4(keccak256("echo()")); // 调用合约 (bool success,bytes memory data) = fb.staticcall(abi.encodeWithSelector(methodId)); if(success){ return abi.decode(data,(string)); } else { return "error"; } } }我们先部署合约 Fallback,再使用 Fallback 的地址来部署 SoldityTest,调用 Fallback 方法 echo 方法,就会触发 Fallback 的 fallback 方法。