使用ethers.js直接读取智能合约中插槽内容

赵永新
2023-12-01

我在上一篇《代理/实现模式下合约插槽索引计算》中的最后,提到了一个合约示例,(BSC 区块链,地址为:0x4BfE9489937d6C0d7cD6911F1102c25c7CBc1B5A)不知道有没有读者有兴趣去看一下,在那个示例合约中,有一段代码我摘出来:

/**
   * @return adm The admin slot.
   */
  function _admin() internal view returns (address adm) {
    bytes32 slot = ADMIN_SLOT;
    assembly {
      adm := sload(slot)
    }
  }
  
  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合约地址的(既没有提供view类型函数,同时还加了一个ifAdmin限制)。怎么办呢?不急,区块链本身就是一个公开透明世界计算机,这就意味着它的数据是对所有人公开的,没有私有这个说法。私有这个限制只是对于链上合约之间交互而言,它并不代表无法从链下获取(毕竟,区块最终是存在节点计算机的硬盘上的)。

ethers.js 中有一个getStorageAt函数,就可以直接读取插槽内容。上一篇文章提到过,智能合约的底层数据库为K/V型,不管是K还是V,都是32字节(256位)大小的。因此,我们读出来的数据也是32字节(256位),需要根据实际情况判断它到底是一个地址,一个整数还是一个布尔值。很明显,我们这两个插槽内容都是地址。

我们直接上代码:

//此文件用来获取某合约的插槽信息
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")


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)
}

start()

运行后输出的结果为:

admin_info: 0x0000000000000000000000005379f32c8d5f663eacb61eef63f722950294f452
admin_address: 0x5379F32C8D5F663EACb61eeF63F722950294f452

impl_info: 0x000000000000000000000000cac73a0f24968e201c2cc326edbc92a87666b430
impl_address: 0xcac73A0f24968e201c2cc326edbC92A87666b430

这里的代码我稍微解释一下,admin_slotimpl_slot均是从源合约中直接复制过来的(见上一篇文章的插槽索引计算)。getStorageAt函数返回的其实是一个16进制字符串,它的长度是多少呢?上面提到读取K/V时返回的V是32字节(256 bit)大小,因此返回的字符串长度是32 * 2 + 2 = 66 (一个16进制字符串是4bit ,因此一个字节字符串的长度就是2,最后 +2 是因为前面多了一个0x)。

而我们的地址是长度是多少呢,是40位16进制(160bit),因此我们从长度索引第26开始,后面的就是地址的值了。最后我们使用getAddress函数将它转换成一个校验后的地址(同时验证我们从字符串得到的是一个格式正确的地址,如果格式不正确转换会抛出错误)。

那我们得到的这两个地址到底对不对呢?我们下次再进行验证!!!

 类似资料: