可升級合約的代理返回錯誤值
https://github.com/vporton/test-web3-read-int128產生錯誤值。為什麼?如何糾正它?
npx buidler run scripts/mytest.js ... -23309975763188660897098281009735482885
(應該是
2
)。pragma solidity ^0.7.0; contract Files { int128 public arToETHCoefficient = 2; } pragma solidity ^0.7.0; import './Relay.sol'; contract FilesRelayer is Relay { constructor(address initAddr) Relay(initAddr) { } } pragma solidity ^0.7.0; import './Proxy.sol'; contract Relay is Proxy { address public currentVersion; address public owner; modifier onlyOwner() { require(msg.sender == owner); _; } constructor(address initAddr) { currentVersion = initAddr; owner = msg.sender; // this owner may be another contract with multisig, not a single contract owner } function changeContract(address newVersion) external onlyOwner() { currentVersion = newVersion; } function changeRelayer(address _owner) external onlyOwner() { owner = _owner; } function _implementation() internal override view returns (address) { return currentVersion; } } /** * License: MIT * * Copyright (c) 2018 zOS Global Limited. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ pragma solidity ^0.7.1; /** * @notice Implements delegation of calls to other contracts, with proper * forwarding of return values and bubbling of failures. * It defines a fallback function that delegates all calls to the address * returned by the abstract _implementation() internal function. * @dev Forked from https://github.com/zeppelinos/zos-lib/blob/8a16ef3ad17ec7430e3a9d2b5e3f39b8204f8c8d/contracts/upgradeability/Proxy.sol * Modifications: * 1. Reformat and conform to Solidity 0.6 syntax (5/13/20) */ abstract contract Proxy { /** * @dev Fallback function. * Implemented entirely in `_fallback`. */ fallback() external payable { _fallback(); } /** * @return The Address of the implementation. */ function _implementation() internal virtual view returns (address); /** * @dev Delegates execution to an implementation contract. * This is a low level function that doesn't return to its internal call site. * It will return to the external caller whatever the implementation returns. * @param implementation Address to delegate. */ function _delegate(address implementation) internal { assembly { // Copy msg.data. We take full control of memory in this inline assembly // block because it will not return to Solidity code. We overwrite the // Solidity scratch pad at memory position 0. calldatacopy(0, 0, calldatasize()) // Call the implementation. // out and outsize are 0 because we don't know the size yet. let result := delegatecall( gas(), implementation, 0, calldatasize(), 0, 0 ) // Copy the returned data. returndatacopy(0, 0, returndatasize()) switch result // delegatecall returns 0 on error. case 0 { revert(0, returndatasize()) } default { return(0, returndatasize()) } } } /** * @dev Function that is run as the first thing in the fallback function. * Can be redefined in derived contracts to add functionality. * Redefinitions must call super._willFallback(). */ function _willFallback() internal virtual {} /** * @dev fallback implementation. * Extracted to enable manual triggering. */ function _fallback() internal { _willFallback(); _delegate(_implementation()); } }
const bre = require("@nomiclabs/buidler"); async function main() { // Buidler always runs the compile task when running scripts through it. // If this runs in a standalone fashion you may want to call compile manually // to make sure everything is compiled // await bre.run('compile'); // We get the contract to deploy const Files = await ethers.getContractFactory("Files"); const files = await Files.deploy(); await files.deployed(); console.log("Files deployed to:", files.address); const FilesRelayer = await ethers.getContractFactory("FilesRelayer"); const deployResult = await FilesRelayer.deploy(files.address); if (deployResult.newlyDeployed) { console.log(`contract FilesRelayer deployed at ${deployResult.address} in block ${deployResult.receipt.blockNumber} using ${deployResult.receipt.gasUsed} gas`); } console.log("web3 version:", web3.version); // const interface = [{inputs: [], name: "arToETHCoefficient", outputs: [{internalType: "int128", name: "", type: "int128"}], stateMutability: "view", type: "function"}] const interface = JSON.parse(require('fs').readFileSync('artifacts/Files.json')).abi; contractInstance = new web3.eth.Contract(interface, deployResult.address); console.log(await contractInstance.methods.arToETHCoefficient().call()); } // We recommend this pattern to be able to use async/await everywhere // and properly handle errors. main() .then(() => process.exit(0)) .catch(error => { console.error(error); process.exit(1); });
您可能已經猜到儲存存在根本問題。我會盡力解釋。
“代理”的整個想法是代理正在執行,因此發生的一切都在代理合約的上下文中展開,並使用代理合約進行儲存。“實現”合約提供了代理注入和按需執行的程式碼,因此可以以大致通常的方式編寫合約,但需要避免一些致命的錯誤。
這將是更多的概念性描述,因此您可以理解。在內部,EVM 使用映射命名空間中的邏輯“槽”。它使用合約地址(為了唯一性)加上字節碼(編譯器)提供的插槽來計算雜湊,這是 EVM 中的絕對邏輯位置。
考慮:
contract A { uint a; // slot 0 uint b; // slot 1 }
撇開優化等可能會重新安排事情不談,讓我們繼續這個簡單的例子,並說編譯器為命名變數計算出插槽 0 和 1。
現在,如果 Proxy 委託給合約 A,那麼合約 A 將在 Proxy 的插槽 0 和 1 上亂塗亂畫,因為這是字節碼所說的。但是,如果 Proxy 正在使用這些插槽,請說:
contract Proxy { address x; // slot 0 address y; // slot 1 }
那麼,當 A 決定儲存某些東西或相反時,x 和 y 就會變得稀少。合約會將他們找到的位轉換為他們期望的類型,但這可能會很混亂。不需要的覆蓋不會帶來任何好處。
兩個要點:
- 您不能安全地將任何值儲存在代理合約的正常狀態變數中,這是您的中繼實際所做的(解析實現地址)。它可能會被踩踏,然後下一個電話將被委派給不合適的地方。
- 您不能使用建構子在實現契約中分配儲存值,因為它將在錯誤的上下文中執行,因此不是代理的儲存,因此當您從代理訪問建構子時,它的行為就好像建構子從未執行過。
這有點進退兩難,因為 Proxy 可能需要一個實現地址和一個管理地址。那東西怎麼存放?
解決方法:
此範例來自 OpenZeppelin UpgradeableProxy.sol 合約。
/** * @dev Emitted when the implementation is upgraded. */ event Upgraded(address indexed implementation); /** * @dev Storage slot with the address of the current implementation. * This is the keccak-256 hash of "eip1967.proxy.implementation" subtracted by 1, and is * validated in the constructor. */ bytes32 private constant _IMPLEMENTATION_SLOT = 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc; /** * @dev Returns the current implementation address. */ function _implementation() internal override view returns (address impl) { bytes32 slot = _IMPLEMENTATION_SLOT; // solhint-disable-next-line no-inline-assembly assembly { impl := sload(slot) } }
它使用彙程式序從一個特定的地方獲取一個希望是抗碰撞的值。這是二傳手:
/** * @dev Stores a new address in the EIP1967 implementation slot. */ function _setImplementation(address newImplementation) private { require(Address.isContract(newImplementation), "UpgradeableProxy: new implementation is not a contract"); bytes32 slot = _IMPLEMENTATION_SLOT; // solhint-disable-next-line no-inline-assembly assembly { sstore(slot, newImplementation) } }
從本質上講,你已經為任何你想在代理級別持久化的東西這樣做了,這樣它就不會被實現契約覆蓋。
建構子
實現不應在建構子中設置任何值。等待合約被部署並呼叫一個函式,通常是
init()
通過代理,所以無論它做什麼都會在代理的上下文中執行並將值寫入代理的儲存。只是要清楚:
當在函式之外時,它
uint x = 99;
會默默地轉換為:uint x; constructor() ... { x = 99;
在編譯時,它不會像預期的那樣與代理一起工作。你想要類似的東西
function init() ... { // invoke through proxy x = 99; // goes to proxy storage }
希望能幫助到你。