一种链下绕过非view限制直接读取智能合约某类特殊函数返回结果的技巧

Posted Zero_Nothing

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了一种链下绕过非view限制直接读取智能合约某类特殊函数返回结果的技巧相关的知识,希望对你有一定的参考价值。

一、前言

我们知道,在智能合约中一般分为读取(view/pure)或者写入(改变状态)这两种类型。由于保护数据设置障碍的需要,有时合约开发者并不想别人查看他们的view函数返回值,于是在上面增加了调用者权限限制,更有甚者,故意通过某种技巧(或者是业务需要)将此函数变成非view/pure类型的函数,也就是一个交易。这样线下(前端或者脚本)调用此函数时就是一个交易,是无法直接得到函数返回结果的。

在上一篇文章《一种绕过管理员权限调用智能合约view函数的小技巧》中,我们介绍了绕过权限控制来读取view函数返回结果的技巧,那么问题来了,这里的函数是非ivew的怎么办?如果该函数的实际执行并未改变状态,相当于一个view函数执行,那我们是可以绕过这个限制的。本文就介绍这么一个小技巧,同时也是对《使用ethers.js直接读取智能合约中插槽内容》这一篇文章的收尾问题进行解答,验证我们读取的数据和实际数据是相同的。

二、示例目标合约。

我们还是以《使用ethers.js直接读取智能合约中插槽内容》这一篇文章中的示例合约进行演示,再次贴出该合约地址:https://bscscan.com/address/0x4BfE9489937d6C0d7cD6911F1102c25c7CBc1B5A#code
该合约已经通过浏览器验证,是BSC网络上一个真实运行的合约(当然不是笔者部署的)。我们注意看以下代码:

  bytes32 internal constant IMPLEMENTATION_SLOT = 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc;
  bytes32 internal constant ADMIN_SLOT = 0xb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d6103;

  /**
   * @dev Modifier to check whether the `msg.sender` is the admin.
   * If it is, it will run the function. Otherwise, it will delegate the call
   * to the implementation.
   */
  modifier ifAdmin() {
    if (msg.sender == _admin()) {
      _;
    } else {
      _fallback();
    }
  }
  /**
   * @return The address of the proxy admin.
   */
  function admin() external ifAdmin returns (address) {
    return _admin();
  }

  /**
   * @return The address of the implementation.
   */
  function implementation() external ifAdmin returns (address) {
    return _implementation();
  }

有看过我们面文章的人一看就知道我摘出来的代码的含义,没有看过的也不打紧,我简单再解释一下。这个代码块刚开始定义了admin和implement的插槽索引,然后定义了一个ifAdmin的函数修饰符,在其定义里我们看到,如果调用者是管理员,就执行接下来的函数体,如果不是,就执行_fallback()函数,这里的_fallback()其实调用委托地址相同的函数了,弄明白这个需要懂得代理/委托这种可升级模式。读者也可以在网上搜索或者在我的其它文章中读到代理/委托相关内容,这里不再解释。

从上面的代码中我们可以看到,只有管理员才能查询管理员地址和implementation地址,并且它们都是非view函数的,无法直接在浏览器界面上使用Read Contract 调用,而是出现在 Write Contract界面中,如下图:
Read Contract按钮下空空如也
上面那张图显示了Read Contract按钮下空空如也。我们再看Write Contract。
读取信息出现在写合约界面中
从上图可以看到,本来只是读取管理员信息的操作却显示在了write界面中,显然第一我们不是管理员,第二我们也没有管理员私钥,我们执行写操作是不能成功的。再说,即使成功,我们也只能得到一个交易对象,而无法直接得到返回结果。

那个Connedt to Web3 点击后会连接你的浏览器插件钱包(笔者用的metamask),当网络选择正确时,连接成功后会变绿,显示连接成功和你的地址,这里笔者就不再放图了,这时你就可以直接使用浏览器调用合约了,不需要该死的项目方前端界面了😂😂😂。

从合约的代码片断中我们可以看到,只有管理员才能去读谁是管理员,这个和区块链的公开透明有那么一点点相违背啊。这里也许并不是合约开发者存心和我们为难,不让我们读这个数据。很大可能是直接使用了openzeppelin中的代理/委托标准模板,那就是openzeppelin和我们为难了,但不管怎样,这都不是事。

好了,有些扯远了,我们来看怎么通过脚本去调用这个write类型函数并得到结果。

三、测试脚本

首先,我们需要明确一点,我们调用的admin函数和implementation函数在调用者是管理员的情况下,的确只读取了本地存储的adminimplement地址,并无改变任何状态,其实质相当于是一个view函数。(如果我们不是管理员调用会报错,即使是view函数也会报错)。

要解决这个问题首先得是管理员,当然我们不是管理员,也拿不到私钥,怎么办呢?上一篇文章《一种绕过管理员权限调用智能合约view函数的小技巧》已经解决这个问题了。

其次,这个不是view函数怎么办?如果我们在该合约的浏览器页面下的Contract ABI里找到这个admin函数,相应的ABI内容为:
{"inputs":[],"name":"admin","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"nonpayable","type":"function"}

我们注意到stateMutability标记的为nonpayable,而不是view

因此,我们无法直接使用它公开的ABI了,我们需要自己编一个view类型的自用。

const abi = [
    "function admin() view returns(address)",
    "function implementation() view returns(address)"
]

我们今天的脚本在前两篇文章的脚本上稍微改一下就能得到了,完整脚本如下:

const { ethers,utils } = require("ethers");

const bsc_rpc_url = "https://bsc-dataseed2.defibit.io"
const provider = new ethers.providers.JsonRpcProvider(bsc_rpc_url)
const proxy_address = "0x4BfE9489937d6C0d7cD6911F1102c25c7CBc1B5A"
const admin_slot = ethers.BigNumber.from("0xb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d6103")
const impl_slot = ethers.BigNumber.from("0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc")

const abi = [
    "function admin() view returns(address)",
    "function implementation() view returns(address)"
]
const proxy_contract = new ethers.Contract(proxy_address,abi,provider)

async function checkResult(admin_address,impl_address) {
    try {
        let admin = await proxy_contract.admin({
            "from":admin_address
        })
        console.log("check admin_address:", admin === admin_address)
        let impl = await proxy_contract.implementation({
            "from":admin_address
        })
        console.log("check impl_address:", impl === impl_address)
    }catch(e){
        console.log(e)
    }
}

async function start() {
    const admin_info = await provider.getStorageAt(proxy_address , admin_slot)
    console.log("admin_info:",admin_info)
    const admin_address = utils.getAddress("0x" + admin_info.substring(26))
    console.log("admin_address:",admin_address)
    console.log()

    const impl_info = await provider.getStorageAt(proxy_address , impl_slot)
    console.log("impl_info:",impl_info)
    const impl_address = utils.getAddress("0x" + impl_info.substring(26))
    console.log("impl_address:",impl_address)
    console.log()
    checkResult(admin_address,impl_address)
}

start()

我们直接node运行脚本,得到的输出为:

admin_info: 0x0000000000000000000000005379f32c8d5f663eacb61eef63f722950294f452
admin_address: 0x5379F32C8D5F663EACb61eeF63F722950294f452

impl_info: 0x000000000000000000000000cac73a0f24968e201c2cc326edbc92a87666b430
impl_address: 0xcac73A0f24968e201c2cc326edbC92A87666b430

check admin_address: true
check impl_address: true

注意:我们这里的流程是先利用插槽索引得到管理员地址,再利用管理员地址去调用这个admin函数,这样双重验证了管理员地址正确这个结果(第一重是非管理员调用会报错,我们通过了;第二重是返回的结果和我们读插槽的结果一致)。虽然这里的admin函数是非view的,我们仍然直接从合约中得到了函数的返回结果(而不是得到了一个交易对象)。

再次重申一点:这个技巧只能针对那些特定的非view函数

四、另外一种阻止view函数的办法

有的时候可以故意将view函数写成非view不让你看数据的,非要给你制造一点麻烦,例如下面的合约:

/**
 *Submitted for verification at Etherscan.io on 2021-06-29
*/

pragma solidity =0.6.6;

contract NoViewTest {
    uint private _seed = 66;

    function getSeed() external returns(uint) {
        if (false) {
            _seed = _seed;
        }
        return _seed;
    }
}

通过一段永不执行的写操作让view 函数变成了 非 view 函数,这样编译的时候不会提示你该函数要定义为view函数。

那么我们可不可以使用上面的方式来直接调用该函数并得到结果66呢?

我们实际操作一下,笔者已经将该合约部署在kovan测试网上,地址为:https://kovan.etherscan.io/address/0xe00d0bc01f11dc5c5f66611a6cf37c3f3847fe1a#code

测试脚本为:noViewTest.js

const { ethers } = require("ethers");

const infura_key = "your_infura_key"
const provider = new ethers.providers.InfuraProvider("kovan",infura_key)
const contract_address = "0xe00d0bc01f11dc5c5f66611a6cf37c3f3847fe1a"
const abi = [
    "function getSeed() view returns(uint)"
]
const test_contract = new ethers.Contract(contract_address,abi,provider)

async function getSeed() {
    try {
        let seed = await test_contract.getSeed()
        console.log("seed:", seed.toString())  //66
    }catch(e){
        console.log(e)
    }
}

getSeed()

我们运行node noViewTest.js,可以得到输出为66,验证我们上面提到的方法是有效的。

什么?还没有infura_key?赶快免费申请一个吧。

五、结束语

如果一个函数只是view函数,即不写数据,不改变状态(注:发log,发以太币也是属于改变状态),即使它加了调用权限并且设法弄成了非view函数(假装为写交易),我们都可以设法读取该函数的返回结果的。所以,还是公开透明吧!

当然,这是有前提的(比如别人会开源,或者通过浏览器验证公开代码,不让我们去猜代码)。

我们本次是链下使用脚本,那么,链上直接使用其它智能合约访问可否绕过这个限制呢?我们下次再尝试!当然有兴趣的读者可以自己先尝试一下。

在这里,笔者验证了一下,因为篇幅很小,所以直接加在后面了。

验证合约为:

pragma solidity =0.6.6;

contract NoViewTest {
    uint private _seed = 66;

    function getSeed() external returns(uint) {
        if (false) {
            _seed = _seed;
        }
        return _seed;
    }
}

interface INoViewTest {
    function getSeed() external view returns(uint);
}

contract NoViewCall {
    INoViewTest public test = INoViewTest(0xE00D0BC01F11Dc5C5F66611A6cf37c3F3847fE1A);

    function getSeed() external view returns(uint) {
        return test.getSeed();
    }

}

合约部署后的地址为:
https://kovan.etherscan.io/address/0x4A2b70Cab25E566AbF998Ed620e2C5CCA025d375#readContract
打开上述地址,直接点击getSeed,就能看到查询结果为66了。

在这里插入图片描述

希望本文章对以太坊和区块链学习(开发)者能提供一点点帮助。

以上是关于一种链下绕过非view限制直接读取智能合约某类特殊函数返回结果的技巧的主要内容,如果未能解决你的问题,请参考以下文章

Chainlink——白皮书简析(whitepaper v2)

以太坊私有链下智能合约部署

一文读懂混合型智能合约:如何结合链上与链下计算资源?

ADAMoracle预言机连接链下数据实现智能合约数据接收

链上合约(On-chain) 和 链下合约(Off-chain) 是什么 区别

一种灵活的智能合约协作方式