一种链下绕过非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按钮下空空如也。我们再看Write Contract。
从上图可以看到,本来只是读取管理员信息的操作却显示在了write界面中,显然第一我们不是管理员,第二我们也没有管理员私钥,我们执行写操作是不能成功的。再说,即使成功,我们也只能得到一个交易对象,而无法直接得到返回结果。
那个Connedt to Web3 点击后会连接你的浏览器插件钱包(笔者用的metamask),当网络选择正确时,连接成功后会变绿,显示连接成功和你的地址,这里笔者就不再放图了,这时你就可以直接使用浏览器调用合约了,不需要该死的项目方前端界面了😂😂😂。
从合约的代码片断中我们可以看到,只有管理员才能去读谁是管理员,这个和区块链的公开透明有那么一点点相违背啊。这里也许并不是合约开发者存心和我们为难,不让我们读这个数据。很大可能是直接使用了openzeppelin
中的代理/委托标准模板,那就是openzeppelin
和我们为难了,但不管怎样,这都不是事。
好了,有些扯远了,我们来看怎么通过脚本去调用这个write类型函数并得到结果。
三、测试脚本
首先,我们需要明确一点,我们调用的admin
函数和implementation
函数在调用者是管理员的情况下,的确只读取了本地存储的admin
和implement
地址,并无改变任何状态,其实质相当于是一个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)