Solidity

onlyOwner 使用代理合約檢查失敗

  • March 13, 2022

我正在使用可升級契約的代理模式測試委託呼叫。onlyOwner在我添加修飾符之前,事情進展順利。

我的契約

pragma solidity 0.5.8;

contract TestLogicInterface {
   function getX() public view returns (uint);
   function setX(uint _newX) public;
   function owner() public view returns(address);
   function setOwner(address _owner) public;
   function testMessageSender() public view returns(address, address);
}

contract TestState {
   uint x = 12;

   function getX() public view returns (uint) {
       return x;
   }

   function setX(uint _newX) public {
       x = _newX;
   }
}

contract TestLogic {

   TestState public _dataLayer;

   address public owner;

   constructor(address _dl, address _owner) public {
       _dataLayer = TestState(_dl);
       owner = _owner;
   }

   modifier onlyOwner {
       require(msg.sender == owner, "Only owner is allowed");
       _;
   }

   function setOwner(address _owner) public {
       owner = _owner;
   }

   function setX(uint _newX) public onlyOwner {
       _dataLayer.setX(_newX);
   }

   function getX() public view returns (uint) {
       return _dataLayer.getX();
   }

   function testMessageSender() public view returns(address, address){
       return (msg.sender, owner);
   }
}

contract TestProxy {
   address public targetAddress;

   constructor(address _lc) public {
       setTargetAddress(_lc);
   }

   function setTargetAddress(address _address) public {
       require(_address != address(0));
       targetAddress = _address;
   }

   function () external {
       address contractAddr = targetAddress;
       assembly {
           let ptr := mload(0x40)
           calldatacopy(ptr, 0, calldatasize)
           let result := delegatecall(gas, contractAddr, ptr, calldatasize, 0, 0)
           let size := returndatasize
           returndatacopy(ptr, 0, size)

           switch result
           case 0 { revert(ptr, size) }
           default { return(ptr, size) }
       }

   }
}

測試案例

const TestState = artifacts.require("TestState")
const TestLogic = artifacts.require("TestLogic")
const TestProxy = artifacts.require("TestProxy")
const LogicInterface = artifacts.require("TestLogicInterface")

let proxy, states, logic, proxyInterfaced

contract('test upgrade', async(accounts) => {

   beforeEach(async() =>{
       state = await TestState.deployed()
       logic = await TestLogic.deployed()
       proxy = await TestProxy.deployed()
       proxyInterfaced = await LogicInterface.at(proxy.address);
       await proxyInterfaced.setOwner(accounts[0])
   })

   it("logic_state", async() => {
       let x = (await proxyInterfaced.getX()).toNumber()
       console.log({x})

       let msgSender = await proxyInterfaced.testMessageSender()
       console.log({msgSender})

       await proxyInterfaced.setX(11, {gas: 300000})
   })

})

我添加了另一個函式testMessageSender來檢查 msg.sender 是否正確並與所有者匹配。該函式的結果是:

msg.sender: '0x4c256B6945a3FFCbf93463D8c0ff914C533bC0Aa',
owner: '0x4c256B6945a3FFCbf93463D8c0ff914C533bC0Aa'

顯然它們都是相同的,那為什麼我的測試案例沒有執行。我收到一個錯誤:

錯誤:返回錯誤:處理事務時出現 VM 異常:還原 只允許所有者 – 給出的原因:只允許所有者。

如果我刪除修飾符,一切都會完美執行。我錯過了什麼?

編輯1:

我更新了我的setX函式並添加了事件來檢查我的斷言是否正確。

event test(address _owner, address _sender);
function setX(uint _newX) public {
   _dataLayer.setX(_newX);
   emit test(owner, msg.sender);
}

我可以看到當我從測試中呼叫這個函式時,會觸發 2 個事件而不是一個帶有以下數據的事件。

   test(_owner: 0x4c256B6945a3FFCbf93463D8c0ff914C533bC0Aa (address), _sender: 0xC63268D4082Dd102cC2730cD606B0f9EedC8B9E2 (address))
  test(_owner: 0x4c256B6945a3FFCbf93463D8c0ff914C533bC0Aa (address), _sender: 0x4c256B6945a3FFCbf93463D8c0ff914C533bC0Aa (address))

第二個說 msg.sender 和 owner 相同,而第一個說它們不同。我現在更疑惑了。

為什麼會觸發 2 個事件而不是 1 個?

編輯2:

如果我不使用狀態契約並將狀態保留在邏輯契約本身中,那麼一切都會完美。

我注意到另一個荒謬的行為。如果我將我的方法TestState從更改getXgetterX. 測試案例失敗,如果我將它們重命名為 getX 和 setX,一切都會再次執行。

狀態功能名稱和邏輯合約之間是否存在關聯?

免責聲明:我沒有玩過這個或測試過這個理論,但我認為我看到你的想法有缺陷。

你在代理契約中有這個:

contract TestProxy {
   address public targetAddress;

如果我沒記錯的話,那將在代理的插槽 0 中放置一個地址,因為它是提到的第一個變數。

然後,代理 delegateCalls 到:

contract TestLogic {
   ...
   address public owner;

這是另一個地址,也位於插槽 0 中,因為它首先被提及。但是,該合約將在代理的上下文中執行並使用代理的狀態。

如果我沒記錯的話,這個組織會和代理的組織發生衝突。無意中覆蓋重要資訊不會帶來任何好處。覆蓋將在 的上下文中發生TestLogic's constructor。事情可能會變得很奇怪。

由於您的代理儲存重要資訊的方式,任何實施契約都可能發生這種情況。它發生在您添加時,onlyOwner因為它是您嘗試的第一件事,它在實施契約的狀態中儲存了一些東西 - 第一件事就是寫入。

我認為您可以通過將代理的資訊(實施契約)儲存在安全、防碰撞的位置來解決此問題。你可以用一點技巧來做到這一點。

生成一個抗碰撞位置並直接寫入該插槽。您必須使用程序集來“接管”儲存佈局的管理。

contract TestProxy {
   // address public targetAddress; // NO!
   bytes32 private constant IMPL_ADDRESS_KEY = keccak256("Implementation address key");

   constructor(address _lc) public {
       setTargetAddress(_lc);
   }

   function setTargetAddress(address _address) public {
       require(_address != address(0));
       // targetAddress = _address;  // No!
       bytes32 implAddressStorageKey = IMPL_ADDRESS_KEY;
       address a = _address; // not sure this is strictly necessary
       //solium-disable-next-line security/no-inline-assembly
       assembly {
           sstore(implAddressStorageKey, a)
       }
   }

如果不清楚,那省去了與實施契約的典型儲存佈局發生衝突的典型儲存佈局。我們或多或少隨機選擇一個插槽,然後使用彙編寫入該插槽。

想要回來嗎?

function implAddress() public view returns(address) {
   address i;
   bytes32 implAddressKey = IMPL_ADDRESS_KEY;
   //solium-disable-next-line security/no-inline-assembly
   assembly {
       i := sload(implAddressKey)
   }
   require(i != UNDEFINED, "Internal error. The implementation is undefined.");
   return i;
}

如果我把我的編輯搞砸以適應你的場景,你可以用一個例子來獲得一些想法:https ://github.com/rob-Hitchens/TrustlessUpgrades/blob/master/contracts/Proxy.sol

主要區別在於使用系統資料庫來保存實施契約。內心深處也是同樣的問題。代理需要一種方法來保存不會被實施契約儲存佈局弄糊塗的契約資訊。

希望能幫助到你。

我認為delegatecall是一種邪惡,因為它違反直覺。當您使用程式碼設置所有者時await proxyInterfaced.setOwner(accounts[0]),該值儲存在 TestProxy 而不是 TestLogic 中,getter 函式也是如此:當您執行程式碼await proxyInterfaced.setX(11, {gas: 300000})時,setX 函式首先在 TestProxy 合約上下文中讀取 _dataLayer!程式碼讀取了合約TestProxy的第一個槽,它的值為addr_TestLogic!不是 addr_TestState!這就解釋了為什麼該事件被觸發了兩次。

您可以在 TestLogicInterface 添加一個函式,function _dataLayer() public view returns(address);然後檢查值:await proxyInterfaced._dataLayer()

引用自:https://ethereum.stackexchange.com/questions/76865