Skip to main content

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 关键字指定),它可以从合约外部调用,通常是通过以下两种方式:

  1. 通过发送交易:用户或外部账户可以发送一个交易到智能合约的地址,调用这个函数。
  2. 从其他合约调用:另一个智能合约可以通过外部调用的方式调用这个函数。

由于这个函数没有使用 viewpure 修饰符,它可以修改智能合约的状态,例如存储数据或者发出事件。在这个函数的例子中,它会发出一个 SessionKeyAdded 事件,这是区块链上的一个日志记录,可以被外部的监听器监测到。

view pure

https://solidity-cn.readthedocs.io/zh/develop/contracts.html?highlight=view#view

以下不可以使用 view

  1. 【变量】 修改状态变量。
  2. 【变量】 产生事件。
  3. 【合约 创建销毁】 创建其它合约,使用 selfdestruct。
  4. 【调用】通过调用发送以太币。
  5. 【调用】调用任何没有标记为 view 或者 pure 的函数。
  6. 【调用】使用低级调用。
  7. 【调用】使用包含特定操作码的内联汇编。

以下不可以使用 pure

  1. 【状态变量】读取状态变量。
  2. 【实例变量】访问 this.balance 或者 <address>.balance
  3. 【全局变量】访问 block,tx, msg 中任意成员 (除 msg.sig 和 msg.data 之外)。
  4. 【调用】调用任何未标记为 pure 的函数。
  5. 【汇编】使用包含某些操作码的内联汇编

fallback

类似于 switch default 用来兜底

你可以把 Solidity 中的 fallback 函数想象成类似于其他编程语言中的switch语句的default分支。它是一个“兜底”的机制,用于处理所有未明确匹配的情况。如果一个智能合约接收到一个调用,但是调用的数据不匹配任何合约中定义的函数签名,fallback 函数就会被执行。

这里有一些类比:

  • switch语句中,如果没有任何case与表达式的值匹配,default分支会被执行。
  • 在智能合约中,如果调用合约的数据不匹配任何现有函数(或者根本没有调用数据),fallback 函数(或receive函数,如果是纯 ETH 发送并且合约中定义了receive函数)就会被执行。

Fallback 函数是智能合约设计中的一个安全特性,确保合约可以优雅地处理意外的交互,包括直接接收 ETH。然而,由于其在合约交互中的特殊角色,编写 fallback 函数时需要格外小心,确保它不会意外地触发不期望的行为,同时要注意它在执行时的 gas 消耗。


在 Solidity 中,fallback 函数是一个合约中的特殊函数,它没有名字,不接受任何参数,也不返回任何值。它可以是 external 的,并且有两种主要的用途:

  1. 接收以太:当一个合约接收到以太(ETH),但没有匹配到任何其他函数的签名时,或者根本就没有数据被发送,fallback 函数会被调用。这允许智能合约接收以太币,就像一个普通的以太坊地址一样。

  2. 合约交互的最后手段:如果一个调用目标合约的函数不存在(也就是说,没有匹配的函数签名),那么 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 是合约的可继承属性, 并可能被派生合约覆盖

https://solidity-cn.readthedocs.io/zh/develop/contracts.html?highlight=view#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

info

如果函数体中或者某个修饰器中有一个显式的return语句被执行,那么:

  • 函数体中的return会结束函数的执行并返回结果,但是还会继续执行当前修饰器中_之后的代码,以及之前应用的修饰器中_之后的代码。
  • 如果return出现在某个修饰器中,它会阻止后续修饰器和函数体中的代码执行,但是仍然会执行当前修饰器以及之前应用的修饰器中_之后的代码。

是的,正是由于每个修饰器结束后都会执行_占位符对应的代码导致的。在 Solidity 中,修饰器的_符号实际上代表了修饰器装饰的函数体的位置,或者如果有多个修饰器,它代表下一个修饰器的位置。

当你调用一个使用了一个或多个修饰器的函数时,这些修饰器会按照它们被应用的顺序执行。每个修饰器可以包含在_之前和之后的代码:

  • _之前的代码在进入函数体或下一个修饰器之前执行。
  • _之后的代码在函数体执行完毕或从下一个修饰器返回之后执行。

如果函数体中或者某个修饰器中有一个显式的return语句被执行,那么:

  • 函数体中的return会结束函数的执行并返回结果,但是还会继续执行当前修饰器中_之后的代码,以及之前应用的修饰器中_之后的代码。
  • 如果return出现在某个修饰器中,它会阻止后续修饰器和函数体中的代码执行,但是仍然会执行当前修饰器以及之前应用的修饰器中_之后的代码。

这种行为确保了即使函数体提前返回,修饰器中的逻辑依然可以得到执行,这对于清理工作、日志记录和状态恢复等操作来说非常重要。因此,在设计修饰器时,你需要考虑到这种逻辑,确保不管函数执行路径如何,修饰器的行为都是预期的。

变量|函数 可见性

info
  1. external:只能从合约外部调用的函数。
  2. public:可以从任何地方调用的函数或变量,如果是变量,还会自动生成一个查询函数。
  3. internal:只能从合约内部或继承的合约中调用的函数或变量。
  4. private:只能从定义它们的那个合约中访问的函数或变量。

Solidity 中,函数和状态变量可以有四种不同的可见性级别:externalpublicinternalprivate。这些级别定义了其他合约和帐户对函数和变量的访问权限。

  1. external(外部):

    • 函数external函数只能从合约外部调用,这意味着它们不能被合约内部的其他函数(包括由this关键字调用的函数)调用。external函数通常用于定义合约的接口,它们对于合约外部的交互是可见且可访问的。由于它们是为外部调用优化的,所以当你从外部调用它们时,它们可以比public函数更节省 gas。
    • 变量:不能将状态变量声明为external,只有函数可以使用这个级别。
  2. public(公开):

    • 函数public函数可以在合约内部被调用,也可以通过消息调用从合约外部被调用。换句话说,任何人都可以看到或调用public函数。
    • 变量public状态变量会自动拥有一个 getter 函数,合约外部的任何人都可以调用这个 getter 函数来获取变量的值。
  3. internal(内部):

    • 函数internal函数只能在合约内部被调用,这包括由该合约派生的合约。它们对于合约外部是不可见的,不能被外部调用。
    • 变量internal状态变量只能在合约内部被访问,以及由该合约派生的合约中也可以访问。
  4. 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 费用。

如果调用失败,有几种可能的情况:

  1. 交易未被矿工打包:如果交易没有被矿工打包到区块中,例如因为 gas 价格设置得太低,那么 ETH 不会从用户的钱包中扣除。

  2. 交易失败并回滚:如果交易被矿工打包并执行,但在执行过程中出现了错误(例如合约抛出异常或者执行了requirerevertassert等导致状态回滚的操作),那么交易会失败并回滚。在这种情况下,发送给合约的 ETH 会退回到用户的钱包,但用户仍然需要支付矿工的 gas 费用,因为矿工已经执行了交易。

  3. 交易成功但函数逻辑失败:如果交易本身成功,但由于智能合约内的某些逻辑条件未满足(而这些条件并不会导致交易回滚),那么交易依然被认为是成功的。在这种情况下,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 被永久锁定在合约中,如果没有适当的机制来提取它们的话。