Solidity における proxy storage clash

スマートコントラクトの upgradability を実現するためによく使われるいわゆる proxy パターンを実装する際に必ず回避しないといけない proxy storage clash を実際に起こしてみましょうの回。

まず、適当なコントラクトを用意します。

pragma solidity ^0.8.6;

contract Account {
    address public owner;

    function setOwner(address newOwner) external {
        owner = newOwner;
    }
}

次に、適当な proxy コントラクトを用意します。

pragma solidity ^0.8.6;

contract Proxy {
    address public implementation;

    constructor(address impl) {
        implementation = impl;
    }

    fallback() external payable {
        _delegate(implementation);
    }

    receive() external payable {
        _delegate(implementation);
    }

    function _delegate(address impl) internal {
        assembly {
            calldatacopy(0, 0, calldatasize())
            let result := delegatecall(gas(), impl, 0, calldatasize(), 0, 0)
            returndatacopy(0, 0, returndatasize())
            switch result
            case 0 {
                revert(0, returndatasize())
            }
            default {
                return(0, returndatasize())
            }
        }
    }
}

挙動を検証するために @nomiclabs/hardhat-waffle を使ってテストコードを書いてみます。

const { expect } = require("chai");

describe("Proxy", () => {
  let Account;
  let account;

  let Proxy;
  let proxy;

  let owner;
  let other;

  beforeEach(async () => {
    Account = await ethers.getContractFactory("Account");
    account = await Account.deploy();
    await account.deployed();

    Proxy = await ethers.getContractFactory("Proxy");
    proxy = await Proxy.deploy(account.address);
    await proxy.deployed();

    [owner, other] = await ethers.getSigners();
  });

  it("storage clash", async () => {
    expect(await proxy.implementation()).to.equal(account.address);

    await owner.sendTransaction({
      to: proxy.address,
      data: Account.interface.encodeFunctionData("setOwner", [other.address]),
    });

    expect(await proxy.implementation()).to.equal(other.address);
  });
});

このテストを例えば

$ npx hardhat test

などで実行すると、問題なく通ります。

が、このテストが通ったということは、delegatecall された setOwner の引数である newOwner によって proxy コントラクトの implementation が上書きされた、すなわち proxy storage clash が発生したということです。相当ヤバいです。

一般的な回避方法が知りたい方は The State of Smart Contract Upgrades を参照してください。スマートコントラクトの upgradability に関する基礎がまとまった良記事です。

comments powered by Disqus