function
函数修饰符
- view 函数表示不会修改状态,
- pure 函数表示不会读取或修改状态。
- external 外部调用(是否外部调用)
- internal 内部调用
function addSessionKey(address sessionKey, bytes4 allowedSelector, uint48 validAfter, uint48 validUntil)
external
{
_addSessionKey(msg.sender, sessionKey, allowedSelector, validAfter, validUntil);
emit SessionKeyAdded(msg.sender, sessionKey, allowedSelector, validAfter, validUntil);
}
// 这个函数使用 external 没有view,是如何调用的?
addSessionKey 函数是一个外部函数(通过 external 关键字指定),它可以从合约外部调用,通常是通过以下两种方式:
- 通过发送交易:用户或外部账户可以发送一个交易到智能合约的地址,调用这个函数。
- 从其他合约调用:另一个智能合约可以通过外部调用的方式调用这个函数。
由于这个函数没有使用 view 或 pure 修饰符,它可以修改智能合约的状态,例如存储数据或者发出事件。在这个函数的例子中,它会发出一个 SessionKeyAdded 事件,这是区块链上的一个日志记录,可以被外部的监听器监测到。
view pure
https://solidity-cn.readthedocs.io/zh/develop/contracts.html?highlight=view#view
以下不可以使用 view
- 【变量】 修改状态变量。
- 【变量】 产生事件。
- 【合约 创建销毁】 创建其它合约,使用 selfdestruct。
- 【调用】通过调用发送以太币。
- 【调用】调用任何没有标记为 view 或者 pure 的函数。
- 【调用】使用低级调用。
- 【调用】使用包含特定操作码的内联汇编。
以下不可以使用 pure
- 【状态变量】读取状态变量。
- 【实例变量】访问 this.balance 或者
<address>.balance。 - 【全局变量】访问 block,tx, msg 中任意成员 (除 msg.sig 和 msg.data 之外)。
- 【调用】调用任何未标记为 pure 的函数。
- 【汇编】使用包含某些操作码的内联汇编
fallback
类似于 switch default 用来兜底
你可以把 Solidity 中的 fallback 函数想象成类似于其他编程语言中的switch语句的default分支。它是一个“兜底”的机制,用于处理所有未明确匹配的情况。如果一个智能合约接收到一个调用,但是调用的数据不匹配任何合约中定义的函数签名,fallback 函数就会被执行。
这里有一些类比:
- 在
switch语句中,如果没有任何case与表达式的值匹配,default分支会被执行。 - 在智能合约中,如果调用合约的数据不匹配任何现有函数(或者根本没有调用数据),fallback 函数(或
receive函数,如果是纯 ETH 发送并且合约中定义了receive函数)就会被执行。
Fallback 函数是智能合约设计中的一个安全特性,确保合约可以优雅地处理意外的交互,包括直接接收 ETH。然而,由于其在合约交互中的特殊角色,编写 fallback 函数时需要格外小心,确保它不会意外地触发不期望的行为,同时要注意它在执行时的 gas 消耗。
在 Solidity 中,fallback 函数是一个合约中的特殊函数,它没有名字,不接受任何参数,也不返回任何值。它可以是 external 的,并且有两种主要的用途:
-
接收以太:当一个合约接收到以太(ETH),但没有匹配到任何其他函数的签名时,或者根本就没有数据被发送,fallback 函数会被调 用。这允许智能合约接收以太币,就像一个普通的以太坊地址一样。
-
合约交互的最后手段:如果一个调用目标合约的函数不存在(也就是说,没有匹配的函数签名),那么 fallback 函数会被执行。这是一个错误处理机制,或者当合约需要处理直接发送到它的任意调用时使用。
从 Solidity 0.6.0 版本开始,fallback 函数被分为两类:
- 接收函数 (
receive()):是一个专门用于处理纯 ETH 发送(没有额外数据)的新函数。如果合约接收到 ETH 而没有任何数据,且存在receive函数,那么receive函数会被调用。如果没有receive函数,或者发送时包含了数据,那么 fallback 函数会被调用。 - Fallback 函数:如果调用合约时没有任何函数与提供的函数签名匹配,或者根本就没有提供数据,并且没有
receive函数,那么 fallback 函数会被执行。
Fallback 函数可以是有状态(能够修改合约状态)的,但是由于 gas 的限制,执行复杂的操作之前需要谨慎考虑。在 Solidity 0.6.0 之前,只有一个 fallback 函数,而在之后,开发者可以选择实现receive函数、fallback 函数或两者都实现,具体取决于他们想要合约如何响应 ETH 的发送和/或无法识别的函数调用。
自定义修饰符
使用 修饰器 modifier 可以轻松改变函数的行为。 例如,它们可以在执行函数之前自动检查某个条件。
修饰器 modifier 是合约的可继承属性, 并可能被派生合约覆盖
pragma solidity ^0.4.11;
contract owned {
function owned() public { owner = msg.sender; }
address owner;
// 这个合约只定义一个修饰器,但并未使用: 它将会在派生合约中用到。
// 修饰器所修饰的函数体会被插入到特殊符号 _; 的位置。
// 这意味着如果是 owner 调用这个函数,则函数会被执行,否则会抛出异常。
modifier onlyOwner {
require(msg.sender == owner);
_;
}
}
contract mortal is owned {
// 这个合约从 `owned` 继承了 `onlyOwner` 修饰符,并将其应用于 `close` 函数,
// 只有在合约里保存的 owner 调用 `close` 函数,才会生效。
function close() public onlyOwner {
selfdestruct(owner);
}
}
contract priced {
// 修改器可以接收参数:
modifier costs(uint price) {
if (msg.value >= price) {
_;
}
}
}
contract Register is priced, owned {
mapping (address => bool) registeredAddresses;
uint price;
function Register(uint initialPrice) public { price = initialPrice; }
// 在这里也使用关键字 `payable` 非常重要,否则函数会自动拒绝所有发送给它的以太币。
function register() public payable costs(price) {
registeredAddresses[msg.sender] = true;
}
function changePrice(uint _price) public onlyOwner {
price = _price;
}
}
contract Mutex {
bool locked;
modifier noReentrancy() {
require(!locked);
locked = true;
_;
locked = false;
}
// 这个函数受互斥量保护,这意味着 `msg.sender.call` 中的重入调用不能再次调用 `f`。
// `return 7` 语句指定返回值为 7,但修改器中的语句 `locked = false` 仍会执行。
function f() public noReentrancy returns (uint) {
require(msg.sender.call());
return 7;
}
}
修饰器|函数中 return
如果函数体中或者某个修饰器中有一个显式的return语句被执行,那么:
- 函数体中的
return会结束函数的执行并返回结果,但是还会继续执行当前修饰器中_之后的代码,以及之前应用的修饰器中_之后的代码。 - 如果
return出现在某个修饰器中,它会阻止后续修饰器和函数体中的代码执行,但是仍然会执行当前修饰器以及之前应用的修饰器中_之后的代码。
是的,正是由于每个修饰器结束后都会执行_占位符对应的代码导致的。在 Solidity 中,修饰器的_符号实际上代表了修饰器装饰的函数体的位置,或者如果有多个修饰器,它代表下一个修饰器的位置。
当你调用一个使用了一个或多个修饰器的函数时,这些修饰器会按照它们被应用的顺序执行。每个修饰器可以包含在_之前和之后的代码:
_之前的代码在进入函数体或下一个修饰器之前执行。_之后的代码在函数体执行完毕或从下一个修饰器返回之后执行。
如果函数体中或者某个修饰器中有一个显式的return语句被执行,那么:
- 函数体中的
return会结束函数的执行并返回结果,但是还会继续执行当前修饰器中_之后的代码,以及之前应用的修饰器中_之后的代码。 - 如果
return出现在某个修饰器中,它会阻止后续修饰器和函数体中的代码执行,但是仍然会执行当前修饰器以及之前应用的修饰器中_之后的代码。
这种行为确保了即使函数体提前返回,修饰器中的逻辑依然可以得到执行,这对于清理工作、日志记录和状态恢复等操作来说非常重要。因此,在设计修饰器时,你需要考虑到这种逻辑,确保不管函数执行路径如何,修饰器的行为都是预期的。
变量|函数 可见性
external:只能从合约外部调用的函数。public:可以从任何地方调用的函数或变量,如果是变量,还会自动生成一个查询函数。internal:只能从合约内部或继承的合约中调用的函数或变量。private:只能从定义它们的那个合约中访问的函数或变量。
Solidity 中,函数和状态变量可以有四种不同的可见性级别:external、public、internal和private。这些级别定义了其他合约和帐户对函数和变量的访问权限。
-
external(外部):
- 函数:
external函数只能从合约外部调用,这意味着它们不能被合约内部的其他函数(包括由this关键字调用的函数)调用。external函数通常用于定义合约的接口,它们对于合约外部的交互是可见且可访问的。由于它们是为外部调用优化的,所以当你从外部调用它们时,它们可以比public函数更节省 gas。 - 变量:不能将状态变量声明为
external,只有函数可以使用这个级别。
- 函数:
-
public(公开):
- 函数:
public函数可以在合约内部被调用,也可以通过消息调用从合约外部被调用。换句话说,任何人都可以看到或调用public函数。 - 变量:
public状态变量会自动拥有一个 getter 函数,合约外部的任何人都可以调用这个 getter 函数来获取变量的值。
- 函数:
-
internal(内部):
- 函数:
internal函数只能在合约内部被调用,这包括由该合约派生的合约。它们对于合约外部是不可见的,不能被外部调用。 - 变量:
internal状态变量只能在合约内部被访问,以及由该合约派生的合约中也可以访问。
- 函数:
-
private(私有):
- 函数:
private函数只能被定义它们的合约调用,即使是子合约也无法访问。 - 变量:
private状态变量也是如此,只能在定义它们的合约中被访问,对任何派生合约都是不可见的。
- 函数:
选择合适的可见性级别对于智能合约的安全性至关重要,因为它决定了谁可以调用合约的函数和访问其变量。通常,最佳实践是尽可能地限制访问权限,只在必要时提供更宽松的访问权限。这样做有助于减少合约的攻击面并提高其安全性。
payable
solidity 声明为 payable 的函数都可以从 msg 中接受 eth?
在 Solidity 中,将函数声明为payable是一种特殊的函数类型,它允许这个函数接收以太币(ETH)。当一个函数被标记为payable时,这意味着你可以在发送交易调用这个函数的同时发送以太币。
msg是一个全局变量,它提供了关于当前消息(交易)的信息。msg.value是一个属性,它包含了与消息一起发送的以太币的数量,单位是 wei(1 ETH = 10^18 wei)。只有payable函数才能访问msg.value,因为只有这些函数才被允许处理以太币。
如果你尝试向一个非payable函数发送以太币,交易将会被拒绝,因为智能合约会认为这是一个错误。这是一个重要的安全特性,它确保了只有那些明确声明为可以接收以太币的函数才会处理资金。
以下是一个简单的例子,展示了一个payable函数:
pragma solidity ^0.8.0;
contract ReceiveEther {
// 一个事件,用于记录接收到的以太币
event Received(address sender, uint amount);
// 一个payable函数,允许合约接收以太币并记录事件
function deposit() external payable {
emit Received(msg.sender, msg.value);
}
// 此函数用于查询合约的余额
function getBalance() external view returns (uint) {
return address(this).balance;
}
}
在这个例子中,deposit函数被标记为payable,这意味着它可以接收以太币。当以太币被发送到这个函数时,它会记录一个事件,包含发送者的地址和发送的金额。此外,getBalance函数可以被用来查询合约当前存储的以太币的总量。
记住,即使函数是payable的,发送以太币的交易仍然需要满足所有的条件和要求(例如,合约的逻辑、交易的 gas 费用等),才能成功地将以太币发送到合约。
前端调用这个合约的 deposit 方法,如果调用成功了,调用方钱包 eth 减少,如果没成功就不会减少?
是的,当前端调 用智能合约的deposit方法时,如果调用成功,调用方(通常是用户的钱包地址)的以太币(ETH)余额会减少。减少的金额包括发送给合约的 ETH 以及用于支付交易的 gas 费用。
如果调用失败,有几种可能的情况:
-
交易未被矿工打包:如果交易没有被矿工打包到区块中,例如因为 gas 价格设置得太低,那么 ETH 不会从用户的钱包中扣除。
-
交易失败并回滚:如果交易被矿工打包并执行,但在执行过程中出现了错误(例如合约抛出异常或者执行了
require、revert、assert等导致状态回滚的操作),那么交易会失败并回滚。在这种情况下,发送给合约的 ETH 会退回到用户的钱包,但用户仍然需要支付矿工的 gas 费用,因为矿工已经执行了交易。 -
交易成功但函数逻辑失败:如果交易本身成功,但由于智能合约内的某些逻辑条件未满足(而这些条件并不会导致交易回滚),那么交易依然被认为是成功的。在这种情况下,ETH 和 gas 费用都会从用户的钱包中扣除。
前端通常使用以太坊的 JavaScript 库如 web3.js 或 ethers.js 来与智能合约交互。这些库提供了发送交易的功能,并允许前端应用监听交易的状态。
例如,使用 web3.js 调用deposit方法可能看起来像这样:
// 假 设web3已经初始化并且用户的账户已经解锁
const contract = new web3.eth.Contract(abi, contractAddress);
const valueToSend = web3.utils.toWei('1', 'ether'); // 将1 ETH转换为wei单位
contract.methods
.deposit()
.send({ from: userAddress, value: valueToSend })
.on('transactionHash', function (hash) {
// 交易哈希已经生成,交易已经发送到网络但尚未被确认
})
.on('receipt', function (receipt) {
// 交易已经被确认,ETH已经从用户钱包扣除
})
.on('error', function (error, receipt) {
// 如果有错误发生,用户可能已经支付了gas费用,但发送给合约的ETH会退回
});
在这个例子中,如果deposit方法调用成功,用户会收到一个交易收据(receipt),表示 ETH 已从其钱包扣除。如果调用失败,用户会收到错误信息,但仍可能已经支付了 gas 费。
假设我这个 deposit 方法没有任何逻辑处理发送过来的 eth,调用者也调用成功了,那么这些 eth 会去哪里?
如果你的deposit方法没有任何逻辑来处理发送过来的以太币(ETH),并且调用者成功调用了这个方法,那么发送的 ETH 会直接转入智能合约的地址。智能合约地址就像普通的以太坊地址一样,可以持有 ETH 和其他代币。
这些 ETH 将会一直留在合约中,直到某个函数调用将它们转移出去。这通常通过合约中的另一个函数来实现,该函数具有转移资金的逻辑(例如,向合约的拥有者发送资金或者根据特定的业务逻辑分配资金)。
以下是一个简单的例子,其中包含了一个withdraw函数,允许合约的拥有者提取合约中的所有 ETH:
pragma solidity ^0.8.0;
contract ReceiveEther {
address public owner;
constructor() {
owner = msg.sender;
}
// 一个事件,用于记录接收到的以太币
event Received(address sender, uint amount);
// 一个payable函数,允许合约接收以太币并记录事件
function deposit() external payable {
emit Received(msg.sender, msg.value);
}
// 提款函数,只允许合约的拥有者调用
function withdraw() external {
require(msg.sender == owner, "Only the owner can withdraw");
payable(owner).transfer(address(this).balance);
}
// 此函数用于查询合约的余额
function getBalance() external view returns (uint) {
return address(this).balance;
}
}
在这个例子中,任何人都可以调用deposit方法向合约发送 ETH。这些 ETH 将会存储在合约的地址中,直到拥有者通过调用withdraw方法将它们提取出来。withdraw方法使用了transfer函数来将合约的余额发送到拥有者的地址。这是一种安全的转账方式,因为如果转账失败,它会自动抛出异常并回滚交易。
如果没有合适的提款机制,ETH 可能会永远地被锁定在合约中,因此设计智能合约时应该考虑到资金的管理和提取机制。
如果合约没有 payable 函数也没有 receive 函数,那么 eth 会被接收吗?
不会。如果一个智能合约没有定义payable函数或receive函数,那么它将无法接收 ETH。在这种情况下,任何尝试向该合约发送 ETH 的交易都会失败,并且交易将被撤回(revert)。这意味着交易不会被包含在区块链中,发送的 ETH 将不会从发送者的账户中扣除。
在 Solidity 中,payable修饰符是用来明确指出一个函数可以接收 ETH 的。如果合约中至少有一个函数被标记为payable,那么合约就能够接收 ETH 并将其添加到合约的余额中。另外,receive函数是一个特殊的无名函数(fallback function),它在合约接收纯 ETH 转账(没有调用任何函数)时被调用。
如果没有这些函数,智能合约是不具备接收 ETH 的能力的。这是一个设计上的选择,用来确保合约不会意外地接收 ETH,因为这可能会导致 ETH 被永久锁定在合约中,如果没有适当的机制来提取它们的话。