您正在查看: Ethereum-优秀转载 分类下的文章

解析 Solidity 0.6 新引入的 try/catch 特性

在以太坊中对智能合约进行编程与常规开发人员所用的编程有很大不同,并且缺乏基本处理错误工具一直是一个问题,经常导致智能合约逻辑“破裂”。

当EVM中的交易执行触发revert时,所有状态更改都将回滚并中止执行。 因此,来自现代编程语言的开发人员在编写Solidity时,都可能最终会搜索“如何在Solidity中try/catch”来处理这些回滚。

Solidity 0.6的新功能里最酷的功能之一是使用try/catch语句。try/catch 的文档可以点击这里查看

为什么需要 try / catch

try/catch 结构可以用于许多场景:

  • 如果一个调用回滚(revert)了,我们不想终止交易的执行。
  • 我们想在同一个交易中重试调用、存储错误状态、对失败的调用做出处理等等。

在 Solidity 0.6 之前,模拟 try/catch 仅有的方式是使用低级的调用如: call, delegatecall 和 staticcall.
这是一个简单的示例,说明如何内部调用同一合约的另一个函数中实现某种try/catch:

pragma solidity <0.6.0;

contract OldTryCatch {

    function execute(uint256 amount) external {

        // 如果执行失败,低级的call会返回 `false` 
        (bool success, bytes memory returnData) = address(this).call(
            abi.encodeWithSignature(
                "onlyEven(uint256)",
                amount
            )
        );

        if (success) {
            // handle success            
        } else {
            // handle exception
        }
    }

    function onlyEven(uint256 a) public {
        // Code that can revert
        require(a % 2 == 0, "Ups! Reverting");
        // ...
    }
}

当调用 execute(uint256 amount), 输入的参数 amount 会通过低级的call调用传给 onlyEven(uint256) 函数, call 调用会返回 bool 值作为第一个参数来指示调用的成功与否,而不会让整个交易失败。参考文档: Solidity中文 文档-地址类型的成员

请注意,在对 onlyEven(uint256) 的低级call调用返回了false的情况下,它会(必需)还原在低级调用执行中所做的状态更改,但是在调用之前和/或之后的更改不会被还原应用。
这种 try/catch的定制实现虽然有点脆弱,但它既可以用于从同一合约(如刚刚的例子)中调用函数,也可以(更多的)用于外部合约的调用。
这是控制外部调用的错误的一种有用方法,但我们应始终记住,由于执行我们不能信任的外部代码可能会出现安全性问题,因此不建议使用低级调用。
这是为什么 try/catch 特性用于外部调用引入 ,在最新的编译器中,可以这样写:

pragma solidity <0.7.0;

contract CalledContract {    
    function someFunction() external {
        // Code that reverts
        revert();
    }
}


contract TryCatcher {

    event CatchEvent();
    event SuccessEvent();

    CalledContract public externalContract;

    constructor() public {
        externalContract = new CalledContract();
    }

    function execute() external {

        try externalContract.someFunction() {
            // Do something if the call succeeds
            emit SuccessEvent();
        } catch {
            // Do something in any other case
            emit CatchEvent();
        }
    }
}

try/catch 概述

就像前面提到的,新功能 try/catch 仅适用于外部调用。参考文档: Solidity中文 文档-外部调用
如果我们想在合同中的内部调用中使用try/catch模式(如第一个示例),我们仍然可以使用前面介绍的低级调用的方法,或者可以使用全局变量this来调用内部函数,就像外部调用一样。

如上面的例子:

try this.onlyEven(3) {
    ...
} catch {
    ...
}

如果尝试使用新语法进行低级调用,则编译器将提示错误:

每当我们尝试使用try/catch语法进行低级调用时,编译器都会返回TypeError错误提示。
如果仔细阅读了编译器错误信息,则TypeError提示会指出,try/catch甚至可以用于创建合约,让我们来尝试一下:

pragma solidity <0.7.0;

contract CalledContract {

    constructor() public {
        // Code that reverts
        revert();
    }

    // ...
}

contract TryCatcher {

    // ...

    function execute() public {

        try new CalledContract() {
            emit SuccessEvent();
        } catch {
            emit CatchEvent();
        }
    }
}

要注意,在try代码块内的任何内容仍然可以停止执行, try仅针对于call。 例如,在try 成功后,依旧可以 revert 交易,例如下面的例子:

function execute() public {

    try externalContract.someFunction() {
        // 尽管外部调用成功了, 依旧可以回退交易。
        revert();
    } catch {
       ...
    }

因此,请注意:try 代码块内的 revert 不会被catch本身捕获。

返回值和作用域内变量
Try / catch 允许使用从外部调用返回值和作用域内变量。

构造调用的例子:

contract TryCatcher {

    // ...

    function execute() public {

        try new CalledContract() returns(CalledContract returnedInstance) {
            // returnedInstance  是新部署合约的地址
            emit SuccessEvent();
        } catch {
            emit CatchEvent();
        }
    }
}

外部调用:

contract CalledContract {    
    function getTwo() public returns (uint256) {
        return 2;
    }
}

contract TryCatcher {

    CalledContract public externalContract;

    // ...

    function execute() public returns (uint256, bool) {

        try externalContract.getTwo() returns (uint256 v) {
            uint256 newValue = v + 2;
            return (newValue, true);
        } catch {
            emit CatchEvent();
        }

        // ...
    }
}

注意本地变量newValue 和返回值只在 try 代码块内有效。这同样适用于在catch块内声明的任何变量。
要在catch语句中使用返回值,我们可以使用以下语法:

contract TryCatcher {

    event ReturnDataEvent(bytes someData);

    // ...

    function execute() public returns (uint256, bool) {

        try externalContract.someFunction() {
            // ...
        } catch (bytes memory returnData) {            
            emit ReturnDataEvent(returnData);
        }
    }
}

外部调用返回的数据将转换为bytes ,并可以在catch 块内进行访问。 注意,该catch 中考虑了各种可能的 revert 原因,并且如果由于某种原因解码返回数据失败,则将在调用合约的上下文中产生该失败-因此执行try/catch的交易也会失败。

指定 catch 条件子句

Solidity 的 try/catch也可以包括特定的catch条件子句。 已经可以使用的第一个特定的catch条件子句是:

contract TryCatcher {

    event ReturnDataEvent(bytes someData);
    event CatchStringEvent(string someString);
    event SuccessEvent();

    // ...

    function execute() public {

        try externalContract.someFunction() {
            emit SuccessEvent();
        } catch Error(string memory revertReason) {
            emit CatchStringEvent(revertReason);
        } catch (bytes memory returnData) {
            emit ReturnDataEvent(returnData);
        }
    }
}

在这里,如果还原是由require(condition,"reason string")或revert("reason string")引起的,则错误签名与catch Error(string memory revertReason)子句匹配,然后与之匹配块被执行。 在任何其他情况下,(例如, assert失败)都会执行更通用的 catch (bytes memory returnData) 子句。

注意,catch Error(string memory revertReason)不能捕获除上述两种情况以外的任何错误。 如果我们仅使用它(不使用其他子句),最终将丢失一些错误。 通常,必须将catch或catch(bytes memory returnData)与catch Error(string memory revertReason)一起使用,以确保我们涵盖了所有可能的revert原因。

在一些特定的情况下,如果catch Error(string memory revertReason) 解码返回的字符串失败,catch(bytes memory returnData)(如果存在)将能够捕获它。

计划在将来的Solidity版本中使用更多条件的catch子句。

Gas 失败

如果交易没有足够的gas执行,则out of gas error 是不能捕获到的。

在某些情况下,我们可能需要为外部调用指定gas,因此,即使交易中有足够的gas,如果外部调用的执行需要的gas比我们设置的多,内部out of gas 错误可能会被低级的catch子句捕获。

pragma solidity <0.7.0;

contract CalledContract {

    function someFunction() public returns (uint256) {
        require(true, "This time not reverting");
    }
}

contract TryCatcher {

    event ReturnDataEvent(bytes someData);
    event SuccessEvent();

    CalledContract public externalContract;

    constructor() public {
        externalContract = new CalledContract();
    }

    function execute() public {
 // Setting gas to 20
        try externalContract.someFunction.gas(20)() {
            // ...
        } catch Error(string memory revertReason) {
            // ...
        } catch (bytes memory returnData) {
            emit ReturnDataEvent(returnData);
        }
    }
}

当gas设置为20时,try调用的执行将用掉所有的 gas,最后一个catch语句将捕获异常:catch (bytes memory returnData)。 相反,将gas设置为更大的量(例如:2000)将执行try块会成功。

结论

  • 总结一下,这里是使用Solidity新添加的try/catch时要记住的事项:
  • 这是仅仅提供给外部调用的特性,如上所述。部署新合约也被视为外部调用。
  • 该功能能够捕获仅在调用内部产生的异常。调用后的 try 代码块是在成功之后执行。不会捕获try 代码块中的任何异常。
  • 如果函数调用返回一些变量,则可以在以下执行块中使用它们(如以上示例中所述)。
    • 如果执行了 try 成功代码块,则必须声明与函数调用实际返回值相同类型的变量。
    • 如果执行了低级的catch块,则返回值是类型为bytes的变量。任何特定条件的catch子句都有其自己的返回值类型。
  • 请记住,低级catch (bytes memory returnData) 子句能够捕获所有异常,而特定条件的catch子句只捕获对应的错误。处理各种异常时,请考虑同时使用两者。
  • 在为 try 外部调用设置特定的gas使用量时,低级的catch子句会捕获最终的out of gas错误。 但如果交易本身没有足够的 gas执行代码,则out of gas是没法捕获的。

本文翻译自 openzeppelin 论坛,原文
转载:https://learnblockchain.cn/article/868

Solidity进阶之静态分析

静态分析是相对容易掌握的工具,对开发复杂的Defi应用非常有帮助。

简介

合约的安全性自动化检测有静态分析、动态分析和形式化验证。静态分析不执行合约代码,通过对合约代码做模式匹配或者语义分析来检测漏洞。动态分析需要执行合约,通过大量的模糊测试来观察合约的状态是否会出现问题。形式化验证是将合约的业务逻辑用数学表达式来描述,只要证明数学表达式是正确的,则合约的业务逻辑也是正确的(不代表合约的实现没有问题)。

静态分析的优点是使用简单,速度快,但只能检测已知的安全漏洞。动态分析能检测出未知的安全问题,但是成本高、速度慢。形式化验证的使用范围窄,比较适用于一些公共库合约。

开发者对合约做静态分析是最基本的要求,使用静态分析工具可以快速检测是否存在一些常见的漏洞,比如:

  • 权限缺失,比如Oracle的更新没有设置权限
  • 重入,这个出的问题最多
  • 整数溢出
  • DDOS,攻击或者缺陷会导致合约无法执行正常的业务逻辑
  • 价格操纵

但静态分析工具不能检测出跟业务逻辑特定相关的问题,还需要开发人员通过自检去做人工静态分析。

Solhint

Solhint能提供一些代码规范和安全检查,一些推荐的代码规范比如:

  • 对字符串使用双引号
  • 使用驼峰命名规则
  • 明确指定状态变量的可见性
  • 避免使用call,delegatecall等底层操作码
  • 避免使用tx.origin
  • 同一个方法中避免多次使用msg.value
  • 避免使用block.number和block.timestamp

Solhint的能力较弱,只能做到语法层面的一些检查,但对规范代码比较有用。

Semgrep

Semgrep是一个通用型的静态分析工具,支持多种语言,对solidity的支持目前还较弱。Semgrep跟Solhint一样也是采用模式匹配来进行检测,Solhint的规则是内置的,Semgrep能自定义规则。比如下面这个规则compound-sweeptoken-not-restricted

rules:
 -
    id: compound-sweeptoken-not-restricted
    message: function sweepToken is allowed to be called by anyone
    metadata:
        references:
        - https://medium.com/chainsecurity/trueusd-compound-vulnerability-bc5b696d29e2
        - https://chainsecurity.com/security-audit/compound-ctoken/
        - https://blog.openzeppelin.com/compound-comprehensive-protocol-audit/
        - https://etherscan.io/address/0xa035b9e130f2b1aedc733eefb1c67ba4c503491f # Compound
        category: access-control
        tags:
        - compound
        - tusd
    patterns:
    - pattern-inside: |
        function sweepToken(...) {
        ...
        }
    - pattern: token.transfer(...);
    - pattern-not-inside: |
        require(msg.sender == admin, "...");
        ...
    languages: 
    - solidity
    severity: WARNING

这个规则专门针对的Compound曾经出现过的TUSD漏洞,由于有很多其它链的项目fork了Compound,因此这个规则可以快速检测出这些项目是否有类似的问题。

Slither

Slither的功能包括:

  • 漏洞自动检测
  • 提供代码优化建议
  • 展现代码的拓扑结构
  • 通过API能自定义漏洞检测规则

Slither的原理是将Solidty抽象语法树(AST)作为输入:

  • 第一步,先解析出合约间的继承图、控制循环图(CFG)和表达式。
  • 第二步,将合约代码转换成SlitherIR(一种内部表达码)。
  • 第三步,对SlitherIR执行一系列单一静态分析(SSA)来完成漏洞检测。

Solhint和Semgrep都是在语法级别进行规则匹配,相比而言Slither能在语义级别进行分析。Slither也可以通过插件来实现自定义的漏洞检测规则,实现上要比Semgrep这种配置文件的方式复杂点。

自检

相对于自动检测工具而言,开发者的自检能完成更复杂的静态分析。比如

address[] public minters;
function setMinter() external {
    minters.push(msg.sender);
}

静态分析工具没法知道修改minters这个状态变量需要什么权限,因为这属于业务逻辑的范围。再比如

if (a > 100) {
    b++;
}

如果开发者误将a >= 100写成了a > 100,这种业务逻辑错误静态分析工具也没法处理。

合约的业务逻辑都是主要在接口中实现的,因此接口检查就很重要:

  • 参数是否有校验,尤其是需要注意是否有任意输入。
  • 接口必须是external或者public吗?如果把一个internal或者private接口暴露出去会非常危险。
  • 需要加payable吗?不加的话没法接收eth,但是若无必要则一定不要加。
  • 接口会修改状态变量吗?修改这些变量需要权限吗?这一点往往是静态分析工具无法检测到的漏洞。
  • 通过call或者delegatecall的调用对象是可信的吗?
  • 外部调用需要设置gasLimit吗?外表调用的返回结果需要处理吗?
  • 在有外部调用的代码前后遵守了Checks-Effects-Interactions规范吗?
  • 外部调用能重入到合约中的其它接口然后通过旁路回到本接口吗?

如果接口涉及到Token的转移,则需要的检查有:

  • 如果Token转移过程中内部扣费会影响业务逻辑吗?
  • Token转移过程中会有钩子函数回调发送者或者接收者吗?
  • Token如果是可升级合约,对业务逻辑有影响吗?
  • 有使用地址的eth余额参与控制逻辑吗?eth的余额是可以通过挖矿或者selfdestruct强制增加的。

对于借贷相关的合约,一般需要使用价格,则需要的检查有:

  • Offchain oracle是可靠的吗?
  • Onchain oracle的价格容易被操纵吗?
  • LP token的价格计算算法是正确的吗?

总之,所有的检查都围绕几个核心:

  • 敏感的权限是否能被转移到任意地址
  • 资产是否有可能被较小的代价转走
  • 资产是否有可能无法取出

结束语

静态分析是相对容易掌握的工具,对开发复杂的Defi应用非常有帮助。不同的静态分析工具可以结合使用,可以先使用Solhint来规范代码,然后使用Semgrep来识别已知的漏洞,接着使用Slither来识别一些语义级别的问题。

开发者更需要自己检查代码,最好是邀请同行互审。最后还是需要审计机构审计代码,不过也不要迷信审计机构,尤其是当Defi的业务逻辑比较复杂的时候,审计机构不一定能精确地理解每一个业务逻辑。

转载自:https://learnblockchain.cn/article/4270

Solidity智能合约安全:防止重入攻击的4种方法

使用检查、影响和交互模式(简称CEI:Checks, Effects, and Interactions)、互斥锁、Pull 支付方式以及gas限制都是防止可重入攻击的有效技术。

Solidity智能合约安全:防止重入攻击的4种方法

重入是一种编程技术,在这种技术中,一个函数(“A”)的执行被一个外部函数调用打断,在外部函数调用的逻辑中,能够递归地调用原函数(“A”)。在某些情况下,重复地重新进入一个函数来执行外部逻辑可能是可取的,不一定是错误。然而,这种技术不建议用于智能合约,因为它将控制流执行释放给不受信任的合约,而器可能用于盗取资金 。因此,在执行对外部合约的调用时,应使用反重入模式和防护措施来防止这种类型的攻击发生。

有三种主要技术来防止重入:

  • 使用 检查、影响、交互(CEI)
  • 使用 防重入互斥锁
  • 使用Pull 支付方式

此外,还有一种技术可能是有效的,但不推荐使用,他是使用 Gas Limit 限制。

检查、影响、交互

CEI模式是一种简单而有效的防止重入的方法。检查指的是判断是否符合条件(验证真实性)。影响指的是由交互产生的状态修改。最后,交互指的是函数或合约之间的调用。

下面是一个错误的示范(因为交互在影响之前)

// contract_A: holds user's funds

function withdraw() external {
  uint userBalance = userBalances[msg.sender];

  require(userBalance > 0);

  (bool success,) = msg.sender.call{ value: userBalance }("");
  require(success,);

  userBalances[msg.sender] = 0;
}

这里是攻击者的receive函数

// contract_B: reentrancy attack

receive() external payable {
  if (address(contract_A).balance >= msg.value) {
    contract_A.withdraw();
  }
}

攻击者的receive函数收到提款后,本应该只返回success,但却检查contract_A是否包含更多的资金。如果是,contract_B会再次调用提款函数,递归直到contract_A所有资金用完。

下面是一个使用CEI模式的提款函数的例子:

function withdraw() external {
  uint userBalance = userBalances[msg.sender];

  require(userBalance > 0);
  userBalances[msg.sender] = 0;

  (bool success,) = msg.sender.call{ value: userBalance }("");
  require(success,);
}

通过在向contract_A转移资金之前将用户在contract_B的账户余额清零,当contract_B发起重入攻击时,提取函数中的条件将为假,执行将被回退。正如这个案例所强调的那样,一行代码的位置可能引起有重大漏洞与重入性安全之间的巨大区别。

重入保护互斥锁

重入防护互斥锁(mutex)可以被构造成一个函数或函数修改器,但其逻辑很简单:一个布尔锁被放置在易受重入影响的函数调用周围。locked的初始状态为假(unlocked),在易受攻击的函数执行开始前,它被立即设置为真(locked),然后在其终止后被设置回假(unlocked)。

下面是一个使用上面的提款函数的例子:

bool internal locked = false;

function withdraw() external {
  require(!locked);
  locked = true;

  uint userBalance = userBalances[msg.sender];
  require(userBalance > 0);
  (bool success,) = msg.sender.call{ value: userBalance }("");
  require(success,);
  userBalances[msg.sender] = 0;

  locked = false;
}

虽然这个提款函数没有遵循CEI模式,但简单的布尔 locked 变量可以防止重入,因此同样可以防止重入攻击。
因为在重入时,第一个require语句条件将为false,会回退交易。

Pull(拉) 方式支付

最后这种技术被Open Zeppelin推荐为最佳实践。然而,它在自动化方面有一个小小的折衷。Pull方式支付是通过中间托管账号发送资金来避免直接和潜在的危险合约交互来实现安全。

在这里,合约资金被发送到一个中间托管账号。

function sendPayment(address user, address escrow) external {
  require(msg.sender == authorized);

  uint userBalance = userBalances[user];

  require(userBalance > 0);

  userBalances[user] = 0;

  (bool success,) = escrow.call{ value: userBalance }("");
  require(success,);
}

而在托管账户的资金,则由接收方来提取:

function pullPayment() external {
  require(msg.sender == receiver);

  uint payment = account(this).balance;

  (bool success,) = msg.sender.call{ value: payment }("");
  require(success,);
}

通过中间托管账号发送资金,合约资金受到保护,不会受到重入攻击。如果托管人持有多个账户的资金,可能会受到重入攻击,所以在适用的情况下,应该实现CEI模式和(或)重入保护互斥锁。

Gas Limit限制

最后,通过Gaslimit 限制也可以防止重入攻击,但这不应该被视为一种安全策略,因为Gas成本取决于以太坊的操作码,而操作码gas是可以改变的。另一方面,智能合约代码是不可改变的。不过, send, transfer, 和 call 这些函数之间的区别是值得了解的。

send和 transfer函数本质上是相同的,但如果交易失败,transfer会回退,而send则不会,而是会返回 false。

// transfer will revert if the transaction fails
address(receiver).transfer(amount);

// send will not revert if the transaction fails
address(receiver).send(amount);

关于重入问题, send和 transfer都有2300个单位的Gas限制。使用这些函数应该可以防止重入性攻击的发生,因为他们没有足够的Gas来递归调用到原函数来利用资金。

与 send和 transfer不同,call没有Gas限制,为了执行复杂的多合约交易(当然,也包括重入攻击),会转发其所有剩余Gas。

结论

一次成功的重入攻击后果可能是毁灭性的,可能会耗尽受害者合约中的所有资金,因此,意识到潜在的漏洞并实现有效的保障措施是很重要的。

无论是否存在漏洞,CEI模式都应该被默认实现,这只是一种良好的做法。额外的安全可以通过使用重入锁和(或)Pull支付方式来完成。最后,Gas限制可以防止重入,但不应该被视为一种安全策略。

本翻译由 Duet Protocol 赞助支持。
转载:https://learnblockchain.cn/article/4162

无需gas代币和ERC20-Permit还任重而道远

ERC20-Permit(EIP-2612)下,如何避免 使用进行两步交易:授权+ transferFrom!

快进至EIP-2612

从那时起,DAI和Uniswap一直在朝着名为EIP-2612的新标准的方向发展,该标准可以取消 approve + transferFrom,同时还允许无 gas 通证转账。 DAI是第一个为其ERC-20通证添加新的permit功能的公司。它允许用户在链下签署授权的交易,生成任何人都可以使用并提交给区块链的签名。这是解决gas 支付问题的基本的第一步,并且消除了用户不友好的两步过程:发送approve和之后的 transferFrom。

让我们详细研究一下EIP。

原始的错误方法

总体而言,该过程非常简单。用户不在发起授权(approve)交易,而是对approve(spender, amount)签名。签名结果可以被任何人传递到 permit函数,在 permit函数我们只需使用 ecrecover来检索签名者地址,接着用 approve(signer,spender,amount)。

这种方式可用于让其他人为交易支付 gas 费用,也可以删除掉常见的授权(approve)+ transferFrom模式:

之前方法:

  • 用户提交token.approve(myContract.address, amount)交易。
  • 等待交易确认。
  • 用户提交第二个 myContract.doSomething()交易,该交易内部使用 token.transferFrom。

现在:

  • 用户进行授权签名:签名信息signature=(myContract.address,amount)。
  • 用户向 myContract.doSomething(signature)提交签名。
  • myContract使用 token.permit增加配额,并调用 token.transferFrom 获取代币。

之前需要两笔交易,现在只需要一笔!

Permit 细节:防止滥用和重播

我们面临的主要问题是签名可能会多次使用或在原本不打算使用的其他地方使用。为防止这种情况,我们添加了几个参数。在底层,我们使用的是已经存在的,广泛使用的EIP-712标准。

1. EIP-712 域哈希(Domain Hash)

使用EIP-712,我们为ERC-20定义了一个域分隔符

bytes32 eip712DomainHash = keccak256(
    abi.encode(
        keccak256(
            "EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"
        ),
        keccak256(bytes(name())), // ERC-20 Name
        keccak256(bytes("1")),    // Version
        chainid(),
        address(this)
    )
);

这样可以确保仅在正确的链ID上将签名用于我们给定的通证合约地址。chainID是在以太坊经典分叉之后引入(以太坊经典network id 依旧为 1), 用来精确识别在哪一个网络。 可以在此处查看现有chain ID的列表

2. Permit 哈希结构

现在我们可以创建一个Permit的签名:

bytes32 hashStruct = keccak256(
    abi.encode(
        keccak256("Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)"),
        owner,
        spender,
        amount,
        nonces[owner],
        deadline
    )
);

此hashStruct将确保签名只能用于

  • Permit 函数
  • 从owner授权
  • 授权spender
  • 授权给定的value (金额)
  • 仅在给定的deadline之前有效
  • 仅对给定的 nonce有效
    nonce可确保某人无法重播签名,即在同一合约上多次使用该签名。

3. 最终哈希

现在我们可以用兼容 EIP-191的712哈希构建(以0x1901开头)最终签名:

bytes32 hash = keccak256(
    abi.encodePacked(uint16(0x1901), eip712DomainHash, hashStruct)
);

4. 验证签名

在此哈希上,我们可以使用ecrecover 获得该函数的签名者:

address signer = ecrecover(hash, v, r, s);
require(signer == owner, "ERC20Permit: invalid signature");
require(signer != address(0), "ECDSA: invalid signature");

无效的签名将产生一个空地址,这就是最后一次检查的目的。

5. 增加Nonce 和 授权

现在,最后我们只需要增加所有者的Nonce并调用授权函数即可:

nonces[owner]++;
_approve(owner, spender, amount);

你可以在此处看到完整的实现示例。

已有的ERC20-Permit 实现

DAI ERC20-Permit

DAI是最早引入 permit的通证之一,如此处所述。实现与EIP-2612略有不同:

  1. 没有使用 value,而只使用一个bool allowed,并将allowance 设置为0或MAX_UINT256
  2. deadline参数称为expiry

Uniswap ERC20-Permit

Uniswap实现与当前的EIP-2612保持一致,请参见这里。它允许你调用removeLiquidityWithPermit,从而省去了额外的授权步骤。

如果你想体验一下该过程,请转到https://app.uniswap.org/#/pool 并切换到Kovan网络。不用增加资金的流动性。现在尝试将其删除。单击“Approve”后,你会注意到此MetaMask弹出窗口如下图所示。

这不会提交交易,而只会创建具有给定参数的签名。你可以对其进行签名,并在第二步中使用生成的签名调用removeLiquidityWithPermit。总而言之:只需提交一份交易。

ERC20-Permit 代码库

我已经创建了可以导入的ERC-20-Permit代码库。你可以在https://github.com/soliditylabs/ERC20-Permit 中找到它
其使用:

你可以通过npm安装来简单地使用它:

$ npm install @soliditylabs/erc20-permit --save-dev

像这样将其导入到你的ERC-20合约中:

// SPDX-License-Identifier: MIT
pragma solidity ^0.7.0;

import {ERC20, ERC20Permit} from "@soliditylabs/erc20-permit/contracts/ERC20Permit.sol";

contract ERC20PermitToken is ERC20Permit {
    constructor (uint256 initialSupply) ERC20("ERC20Permit-Token", "EPT") {
        _mint(msg.sender, initialSupply);
    }
}

前端使用

你可以在我的测试中代码在这里看到如何使用eth-permit库创建有效的签名。它会自动获取正确的随机数,并根据当前标准设置参数。它还支持DAI样式的许可证签名创建。完整文档可在https://github.com/dmihal/eth-permit 获得

关于调试的一句话:这可能很痛苦。关闭任何单个参数都将导致revert: Invalid signature。祝你好运找出原因。

在撰写本文时,似乎仍然有一个已知 issue,它可能会或也可能不会影响你,具体取决于你的Web3提供程序。如果确实对你有影响,请使用通过patch-package安装这里的补丁

无需gas代币解决方案

现在回想起我在悉尼的经历,单靠这个标准并不能解决问题,但这是解决该问题的第一个基本模块。现在,你可以为其创建加油站网络,例如Open GSN。部署合约,该网络只需通过permit + transferFrom即可进行通证转账。 GSN内部运行的节点将获取许可签名并提交。

谁支付 gas 费?这将取决于特定的场景。也许Dapp公司支付这些费用作为其客户获取成本(CAC)的一部分。也许用转移的通证支付了GSN节点费用。要弄清所有细节,我们还有很长的路要走。

一如既往的小心使用

请注意,该标准尚未最终确定。当前与Uniswap实现相同,但将来可能会有所变化。如果标准再次更改,我将保持库的更新。我的图书馆代码也未经审核,使用后果自负。

本翻译由 Cell Network 赞助支持。
转载自:https://learnblockchain.cn/article/1790

"Stack Too Deep(堆栈太深)" 解决方案

恐怖的三个词

你只需要在合约中添加一个微小的更改即可。你认为这只需要几秒钟。没错,添加代码只花了不到一分钟的时间。你很高兴你快速解决了这个问题,你输入了compile命令。这么小的更改,你确信代码是正确的。

然而,你确看到以下错误消息:

InternalCompilerError:Stack Too Deep, try removing local variables.(堆栈太深,请尝试删除一些局部变量。)
哎哟。这里发生了什么?如果你之前写过智能合约,这很可能是一个非常熟悉的错误消息,并且在不可预测的时间出现。但是通常在你时间紧迫的时候。

不过请放心,这不是你的错。如果你正在为这个错误而苦苦挣扎,那么你不是唯一的一个。

看看最近的调查,您最讨厌Solidity哪个方面:

为什么会出现此错误?

原因是在EVM堆栈中如何引用变量方面存在限制。尽管其中可以包含16个以上的变量,但是一旦尝试引用16或更高槽位中的变量,将失败。因此,并非总是很清楚为什么某些代码会失败,然后进行一些随机更改似乎可以解决问题。

但是我不想介绍太多让你厌倦的理论。这是一篇实用的博客文章。

如何解决

现在到底有什么通用方法可以解决此问题?让我们看一下处理错误的五种方法:

  1. 使用更少的变量
  2. 利用函数
  3. 代码块作用域范围
  4. 利用结构体
  5. 一些黑技巧

好吧,第一个显而易见。如果可以,请尝试重构代码以使用更少的变量。办法很直接,让我们继续前进看看其他 4 个方法。
对于其他四个,我们来看一个堆栈太深的示例代码以及四种修复它的方法。

Stack Too Deep 的例子

让我们看下面的代码。它将抛出困扰我们的堆栈太深的错误消息。我们可以对它可以做些什么呢?

// SPDX-License-Identifier: MIT
pragma solidity 0.7.1;

contract StackTooDeepTest1 {
    function addUints(
        uint256 a,uint256 b,uint256 c,uint256 d,uint256 e,uint256 f,uint256 g,uint256 h,uint256 i
    ) external pure returns(uint256) {

        return a+b+c+d+e+f+g+h+i;
    }
}

1.使用内部函数

是的,使用内部函数将使错误消失。例如,我们可以将其分为三个函数调用,每个函数调用加起来会包含三个uint。神奇的是,堆栈太深的错误会迫使我们编写更好的代码。

// SPDX-License-Identifier: MIT
pragma solidity 0.7.1;

contract StackTooDeepTest1 {
   function addUints(
        uint256 a,uint256 b,uint256 c,uint256 d,uint256 e,uint256 f,uint256 g,uint256 h,uint256 i
    ) external pure returns(uint256) {

        return _addThreeUints(a,b,c) + _addThreeUints(d,e,f) + _addThreeUints(g,h,i);
    }

    function _addThreeUints(uint256 a, uint256 b, uint256 c) private pure returns(uint256) {
        return a+b+c;
    }
}

2.利用块作用域

Uniswap启发,你也可以使用块作用域。只需将大括号括在部分代码中:

// SPDX-License-Identifier: MIT
pragma solidity 0.7.1;

contract StackTooDeepTest2 {
    function addUints(
        uint256 a,uint256 b,uint256 c,uint256 d,uint256 e,uint256 f,uint256 g,uint256 h,uint256 i
    ) external pure returns(uint256) {

        uint256 result = 0;

        {
            result = a+b+c+d+e;
        }

        {
            result = result+f+g+h+i;
        }

        return result;
    }
}

3. 通过传递结构体

这是只使用较少变量的一种方法。将数据放入结构中。出于可读性原因,也是一个好主意。

// SPDX-License-Identifier: MIT
pragma solidity 0.7.1;
pragma experimental ABIEncoderV2;

contract StackTooDeepTest3 {
    struct UintPair {
        uint256 value1;
        uint256 value2;
    }

    function addUints(
        UintPair memory a, UintPair memory b, UintPair memory c, UintPair memory d, uint256 e
    ) external pure returns(uint256) {

        return a.value1+a.value2+b.value1+b.value2+c.value1+c.value2+d.value1+d.value2+e;
    }
}

4.解析msg.data

这种方法的最初想法来自用户Stackexchange的k06a,这需要点黑技巧,所以我通常不建议这样做。但是如果你尝试了所有其他尝试都没有成功?可以尝试一下:

// SPDX-License-Identifier: MIT
pragma solidity 0.7.1;

contract StackTooDeepTest4 {
    function addUints(
        uint256 /*a*/,uint256 /*b*/,uint256 c,uint256 d,uint256 e,uint256 f,uint256 g,uint256 h,uint256 i
    ) external pure returns(uint256) {
      return _fromUint(msg.data)+c+d+e+f+g+h+i;
    }

    function _fromUint(bytes memory data) internal pure returns(uint256 value) {
        uint256 value1;
        uint256 value2;

        assembly {
            value1 := mload(add(data, 36))
            value2 := mload(add(data, 68))
            value  := add(value1, value2)
        }
    }
}

这是如何工作的,就是通过解析msg.data。所有发送到合约的数据都存储此变量,因此我们可以注释掉变量a和b,但仍接收它们的值。 msg.data的前4个字节是函数选择器数据。之后是我们的前两个uint256,每个32位。

使用 msg.data 的方法仅适用于外部函数。一种变通方法是将其与公共函数一起使用, 方法是通过this.myPublicFunction()调用那些公共函数。

也许现在的堆栈对你来说足够了。:)

本翻译由 Cell Network 赞助支持。
转载:https://learnblockchain.cn/article/1629