Solidity代理合约:解锁区块链代码的灵活升级大法

Solidity里的代理合约(ProxyContract),这可是区块链开发里一个超级实用的技术,能让你的智能合约像手机系统一样支持“升级”,同时保持数据和地址不变。区块链的合约一旦部署,默认是不可变的,但用代理合约,你可以把逻辑和数据分开,随时替换逻辑合约,简直是开发者的救星!不过,代理合约也有

Solidity里的代理合约(Proxy Contract),这可是区块链开发里一个超级实用的技术,能让你的智能合约像手机系统一样支持“升级”,同时保持数据和地址不变。区块链的合约一旦部署,默认是不可变的,但用代理合约,你可以把逻辑和数据分开,随时替换逻辑合约,简直是开发者的救星!不过,代理合约也有坑,比如存储冲突、权限管理、调用安全等。

代理合约核心概念

先来搞清楚几个关键点:

  • 代理合约(Proxy):一个中间层合约,负责接收调用并转发到逻辑合约(Implementation),存储数据在代理合约中。
  • 逻辑合约(Implementation):包含实际业务逻辑,升级时替换这个合约。
  • 存储分离:代理合约存储数据,逻辑合约只提供代码,防止存储冲突。
  • 委托调用(delegatecall):代理合约通过delegatecall调用逻辑合约,代码在逻辑合约执行,但上下文(存储、msg.sender等)属于代理合约。
  • 升级机制:通过更新逻辑合约地址实现功能升级,不改变代理合约地址。
  • 安全风险
    • 存储冲突:逻辑合约的存储布局必须与代理一致。
    • 权限控制:谁能升级?没限制可能被黑客篡改。
    • 初始化:新逻辑合约需要正确初始化。
  • OpenZeppelin:提供标准的代理合约实现,如透明代理(Transparent Proxy)和UUPS(Universal Upgradeable Proxy Standard)。
  • Solidity 0.8.x:自带溢出/下溢检查,配合OpenZeppelin确保安全。
  • Hardhat:开发和测试工具,支持部署和验证。

咱们用Solidity 0.8.20,结合OpenZeppelin和Hardhat,从简单代理到复杂UUPS实现,逐步打造安全的代理合约。

环境准备

用Hardhat搭建开发环境,写和测试合约。

mkdir proxy-demo
cd proxy-demo
npm init -y
npm install --save-dev hardhat @nomicfoundation/hardhat-toolbox @openzeppelin/contracts @openzeppelin/contracts-upgradeable
npm install ethers

初始化Hardhat:

npx hardhat init

选择TypeScript项目,安装依赖:

npm install --save-dev ts-node typescript @types/node @types/mocha

目录结构:

proxy-demo/
├── contracts/
│   ├── SimpleProxy.sol
│   ├── LogicV1.sol
│   ├── LogicV2.sol
│   ├── TransparentProxy.sol
│   ├── UUPSProxy.sol
├── scripts/
│   ├── deploy.ts
├── test/
│   ├── Proxy.test.ts
├── hardhat.config.ts
├── tsconfig.json
├── package.json

tsconfig.json

{
  "compilerOptions": {
    "target": "ES2020",
    "module": "CommonJS",
    "strict": true,
    "esModuleInterop": true,
    "moduleResolution": "node",
    "resolveJsonModule": true,
    "outDir": "./dist",
    "rootDir": "./"
  },
  "include": ["hardhat.config.ts", "scripts", "test"]
}

hardhat.config.ts

import { HardhatUserConfig } from "hardhat/config";
import "@nomicfoundation/hardhat-toolbox";

const config: HardhatUserConfig = {
  solidity: "0.8.20",
  networks: {
    hardhat: {
      chainId: 1337,
    },
  },
};

export default config;

跑本地节点:

npx hardhat node

基础代理合约

先写一个简单的代理合约,理解delegatecall和升级机制。

合约代码

contracts/LogicV1.sol

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

contract LogicV1 {
    address public owner;
    uint256 public value;

    function initialize(address _owner) public {
        require(owner == address(0), "Already initialized");
        owner = _owner;
    }

    function setValue(uint256 _value) public {
        require(msg.sender == owner, "Only owner");
        value = _value;
    }

    function getValue() public view returns (uint256) {
        return value;
    }
}

contracts/LogicV2.sol

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

contract LogicV2 {
    address public owner;
    uint256 public value;

    function initialize(address _owner) public {
        require(owner == address(0), "Already initialized");
        owner = _owner;
    }

    function setValue(uint256 _value) public {
        require(msg.sender == owner, "Only owner");
        value = _value * 2; // V2: Double the value
    }

    function getValue() public view returns (uint256) {
        return value;
    }
}

contracts/SimpleProxy.sol

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

contract SimpleProxy {
    address public implementation;
    address public owner;

    constructor(address _implementation) {
        owner = msg.sender;
        implementation = _implementation;
    }

    function upgrade(address _newImplementation) public {
        require(msg.sender == owner, "Only owner");
        implementation = _newImplementation;
    }

    fallback() external payable {
        address impl = implementation;
        require(impl != address(0), "Implementation not set");

        assembly {
            let ptr := mload(0x40)
            calldatacopy(ptr, 0, calldatasize())
            let result := delegatecall(gas(), impl, ptr, calldatasize(), 0, 0)
            let size := returndatasize()
            returndatacopy(ptr, 0, size)
            switch result
            case 0 { revert(ptr, size) }
            default { return(ptr, size) }
        }
    }

    receive() external payable {}
}

解析

  • LogicV1
    • 存储:owner(合约拥有者),value(一个数值)。
    • initialize:设置owner,防止重复初始化。
    • setValue:设置value,仅owner可调用。
    • getValue:返回value
  • LogicV2:升级版,setValue将输入值翻倍。
  • SimpleProxy
    • 存储:implementation(逻辑合约地址),owner
    • upgrade:更新implementation,仅owner可调用。
    • fallback:通过delegatecall转发调用到implementation,使用汇编确保低级调用。
    • receive:接收ETH。
  • delegatecall
    • 执行implementation的代码,但使用代理合约的存储。
    • 代理合约保存ownervalue,逻辑合约只提供逻辑。
  • 安全特性
    • onlyOwner限制升级。
    • 检查implementation不为空。

测试

test/Proxy.test.ts

import { ethers } from "hardhat";
import { expect } from "chai";
import { SimpleProxy, LogicV1, LogicV2 } from "../typechain-types";

describe("SimpleProxy", function () {
  let proxy: SimpleProxy;
  let logicV1: LogicV1;
  let logicV2: LogicV2;
  let owner: any, addr1: any;

  beforeEach(async function () {
    [owner, addr1] = await ethers.getSigners();
    const LogicV1Factory = await ethers.getContractFactory("LogicV1");
    logicV1 = await LogicV1Factory.deploy();
    await logicV1.deployed();

    const ProxyFactory = await ethers.getContractFactory("SimpleProxy");
    proxy = await ProxyFactory.deploy(logicV1.address);
    await proxy.deployed();

    const LogicV2Factory = await ethers.getContractFactory("LogicV2");
    logicV2 = await LogicV2Factory.deploy();
    await logicV2.deployed();

    // Initialize through proxy
    const proxyAsLogicV1 = LogicV1Factory.attach(proxy.address);
    await proxyAsLogicV1.initialize(owner.address);
  });

  it("should initialize correctly", async function () {
    const proxyAsLogicV1 = await ethers.getContractFactory("LogicV1").then(f => f.attach(proxy.address));
    expect(await proxyAsLogicV1.owner()).to.equal(owner.address);
  });

  it("should set and get value through proxy", async function () {
    const proxyAsLogicV1 = await ethers.getContractFactory("LogicV1").then(f => f.attach(proxy.address));
    await proxyAsLogicV1.setValue(42);
    expect(await proxyAsLogicV1.getValue()).to.equal(42);
  });

  it("should restrict setValue to owner", async function () {
    const proxyAsLogicV1 = await ethers.getContractFactory("LogicV1").then(f => f.attach(proxy.address));
    await expect(proxyAsLogicV1.connect(addr1).setValue(42)).to.be.revertedWith("Only owner");
  });

  it("should upgrade to LogicV2", async function () {
    const proxyAsLogicV1 = await ethers.getContractFactory("LogicV1").then(f => f.attach(proxy.address));
    await proxyAsLogicV1.setValue(42);
    await proxy.upgrade(logicV2.address);
    const proxyAsLogicV2 = await ethers.getContractFactory("LogicV2").then(f => f.attach(proxy.address));
    await proxyAsLogicV2.setValue(10);
    expect(await proxyAsLogicV2.getValue()).to.equal(20); // V2 doubles the value
    expect(await proxyAsLogicV2.owner()).to.equal(owner.address); // Storage preserved
  });

  it("should restrict upgrade to owner", async function () {
    await expect(proxy.connect(addr1).upgrade(logicV2.address)).to.be.revertedWith("Only owner");
  });
});

跑测试:

npx hardhat test
  • 解析
    • 部署:LogicV1SimpleProxy,通过代理调用initialize
    • 操作:通过代理设置value为42,验证存储在代理合约。
    • 升级:切换到LogicV2setValue(10)返回20,owner保持不变。
    • 权限:addr1无法升级。
  • 存储ownervalue存储在代理合约的槽位,delegatecall确保逻辑合约操作代理的存储。

存储冲突问题

如果逻辑合约的存储布局变化,会导致数据错乱。

错误示例

contracts/BadLogicV2.sol

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

contract BadLogicV2 {
    uint256 public value; // Slot 0 (wrong order)
    address public owner; // Slot 1 (wrong order)

    function initialize(address _owner) public {
        require(owner == address(0), "Already initialized");
        owner = _owner;
    }

    function setValue(uint256 _value) public {
        require(msg.sender == owner, "Only owner");
        value = _value * 2;
    }

    function getValue() public view returns (uint256) {
        return value;
    }
}

测试:

test/Proxy.test.ts(添加):

it("should fail with storage collision", async function () {
  const proxyAsLogicV1 = await ethers.getContractFactory("LogicV1").then(f => f.attach(proxy.address));
  await proxyAsLogicV1.setValue(42);
  const BadLogicV2Factory = await ethers.getContractFactory("BadLogicV2");
  const badLogicV2 = await BadLogicV2Factory.deploy();
  await badLogicV2.deployed();
  await proxy.upgrade(badLogicV2.address);
  const proxyAsBadLogicV2 = BadLogicV2Factory.attach(proxy.address);
  expect(await proxyAsBadLogicV2.owner()).to.not.equal(owner.address); // Storage messed up
});
  • 问题LogicV1的存储是owner(slot 0), value(slot 1),BadLogicV2value(slot 0), owner(slot 1),升级后owner被覆盖为value的值。
  • 解决:新逻辑合约必须保持存储布局一致,或使用storage关键字显式声明槽位。

透明代理(Transparent Proxy)

用OpenZeppelin的透明代理,解决管理调用冲突。

contracts/LogicV1.sol(更新,使用OpenZeppelin升级库):

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";

contract LogicV1 is Initializable, OwnableUpgradeable {
    uint256 public value;

    function initialize(address _owner) public initializer {
        __Ownable_init(_owner);
    }

    function setValue(uint256 _value) public onlyOwner {
        value = _value;
    }

    function getValue() public view returns (uint256) {
        return value;
    }
}

contracts/LogicV2.sol(更新):

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";

contract LogicV2 is Initializable, OwnableUpgradeable {
    uint256 public value;

    function initialize(address _owner) public initializer {
        __Ownable_init(_owner);
    }

    function setValue(uint256 _value) public onlyOwner {
        value = _value * 2;
    }

    function getValue() public view returns (uint256) {
        return value;
    }
}

contracts/TransparentProxy.sol

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol";

contract TransparentProxy is TransparentUpgradeableProxy {
    constructor(address logic, address admin, bytes memory data)
        TransparentUpgradeableProxy(logic, admin, data)
    {}
}

解析

  • LogicV1/V2
    • 使用InitializableOwnableUpgradeable,支持代理初始化。
    • initializer修饰符确保初始化只执行一次。
  • TransparentProxy
    • 继承OpenZeppelin的TransparentUpgradeableProxy
    • 构造函数:设置逻辑合约地址、管理员地址、初始化数据。
    • 透明机制:管理员调用直接访问代理合约(升级功能),普通用户调用转发到逻辑合约。
  • 安全特性
    • 防止管理员调用逻辑合约功能。
    • OwnableUpgradeable管理权限。
    • 存储布局由OpenZeppelin规范化。

测试

test/Proxy.test.ts(更新):

import { ethers } from "hardhat";
import { expect } from "chai";
import { TransparentProxy, LogicV1, LogicV2 } from "../typechain-types";

describe("TransparentProxy", function () {
  let proxy: TransparentProxy;
  let logicV1: LogicV1;
  let logicV2: LogicV2;
  let owner: any, addr1: any, admin: any;

  beforeEach(async function () {
    [owner, addr1, admin] = await ethers.getSigners();
    const LogicV1Factory = await ethers.getContractFactory("LogicV1");
    logicV1 = await LogicV1Factory.deploy();
    await logicV1.deployed();

    const ProxyFactory = await ethers.getContractFactory("TransparentProxy");
    const initData = LogicV1Factory.interface.encodeFunctionData("initialize", [owner.address]);
    proxy = await ProxyFactory.deploy(logicV1.address, admin.address, initData);
    await proxy.deployed();

    const LogicV2Factory = await ethers.getContractFactory("LogicV2");
    logicV2 = await LogicV2Factory.deploy();
    await logicV2.deployed();
  });

  it("should initialize correctly", async function () {
    const proxyAsLogicV1 = await ethers.getContractFactory("LogicV1").then(f => f.attach(proxy.address));
    expect(await proxyAsLogicV1.owner()).to.equal(owner.address);
  });

  it("should set and get value", async function () {
    const proxyAsLogicV1 = await ethers.getContractFactory("LogicV1").then(f => f.attach(proxy.address));
    await proxyAsLogicV1.setValue(42);
    expect(await proxyAsLogicV1.getValue()).to.equal(42);
  });

  it("should restrict setValue to owner", async function () {
    const proxyAsLogicV1 = await ethers.getContractFactory("LogicV1").then(f => f.attach(proxy.address));
    await expect(proxyAsLogicV1.connect(addr1).setValue(42)).to.be.revertedWith("Ownable: caller is not the owner");
  });

  it("should upgrade to LogicV2", async function () {
    const proxyAsLogicV1 = await ethers.getContractFactory("LogicV1").then(f => f.attach(proxy.address));
    await proxyAsLogicV1.setValue(42);
    const ProxyAdminFactory = await ethers.getContractFactory("ProxyAdmin", admin);
    const proxyAdmin = await ProxyAdminFactory.deploy();
    await proxyAdmin.deployed();
    await proxyAdmin.connect(admin).upgrade(proxy.address, logicV2.address);
    const proxyAsLogicV2 = await ethers.getContractFactory("LogicV2").then(f => f.attach(proxy.address));
    await proxyAsLogicV2.setValue(10);
    expect(await proxyAsLogicV2.getValue()).to.equal(20);
    expect(await proxyAsLogicV2.owner()).to.equal(owner.address);
  });

  it("should restrict upgrade to admin", async function () {
    const ProxyAdminFactory = await ethers.getContractFactory("ProxyAdmin", admin);
    const proxyAdmin = await ProxyAdminFactory.deploy();
    await proxyAdmin.deployed();
    await expect(proxyAdmin.connect(addr1).upgrade(proxy.address, logicV2.address)).to.be.revertedWith("Ownable: caller is not the owner");
  });
});
  • 解析
    • 部署:LogicV1TransparentProxy,通过initData初始化。
    • 操作:设置value为42,验证存储。
    • 升级:用ProxyAdmin切换到LogicV2setValue(10)返回20。
    • 权限:只有admin可升级。
  • 透明机制:管理员调用直接操作代理,普通用户调用转发到逻辑合约。

UUPS代理(Universal Upgradeable Proxy Standard)

UUPS将升级逻辑移到逻辑合约,减少代理合约存储。

contracts/UUPSLogicV1.sol

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";

contract UUPSLogicV1 is Initializable, OwnableUpgradeable, UUPSUpgradeable {
    uint256 public value;

    function initialize(address _owner) public initializer {
        __Ownable_init(_owner);
        __UUPSUpgradeable_init();
    }

    function setValue(uint256 _value) public onlyOwner {
        value = _value;
    }

    function getValue() public view returns (uint256) {
        return value;
    }

    function _authorizeUpgrade(address newImplementation) internal override onlyOwner {}
}

contracts/UUPSLogicV2.sol

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";

contract UUPSLogicV2 is Initializable, OwnableUpgradeable, UUPSUpgradeable {
    uint256 public value;

    function initialize(address _owner) public initializer {
        __Ownable_init(_owner);
        __UUPSUpgradeable_init();
    }

    function setValue(uint256 _value) public onlyOwner {
        value = _value * 2;
    }

    function getValue() public view returns (uint256) {
        return value;
    }

    function _authorizeUpgrade(address newImplementation) internal override onlyOwner {}
}

contracts/UUPSProxy.sol

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol";

contract UUPSProxy is ERC1967Proxy {
    constructor(address logic, bytes memory data) ERC1967Proxy(logic, data) {}
}

解析

  • UUPSLogicV1/V2
    • 继承UUPSUpgradeable,包含升级逻辑。
    • _authorizeUpgrade:限制升级权限。
  • UUPSProxy
    • 继承ERC1967Proxy,存储逻辑地址在特定槽位。
    • 构造函数:设置逻辑合约和初始化数据。
  • UUPS优势
    • 代理合约更轻量,升级逻辑在逻辑合约。
    • 可通过自毁移除升级功能。
  • 安全特性
    • onlyOwner控制升级。
    • Initializer防止重复初始化。
    • 标准化的存储槽(ERC1967)。

测试

test/Proxy.test.ts(更新):

import { ethers } from "hardhat";
import { expect } from "chai";
import { UUPSProxy, UUPSLogicV1, UUPSLogicV2 } from "../typechain-types";

describe("UUPSProxy", function () {
  let proxy: UUPSProxy;
  let logicV1: UUPSLogicV1;
  let logicV2: UUPSLogicV2;
  let owner: any, addr1: any;

  beforeEach(async function () {
    [owner, addr1] = await ethers.getSigners();
    const LogicV1Factory = await ethers.getContractFactory("UUPSLogicV1");
    logicV1 = await LogicV1Factory.deploy();
    await logicV1.deployed();

    const ProxyFactory = await ethers.getContractFactory("UUPSProxy");
    const initData = LogicV1Factory.interface.encodeFunctionData("initialize", [owner.address]);
    proxy = await ProxyFactory.deploy(logicV1.address, initData);
    await proxy.deployed();

    const LogicV2Factory = await ethers.getContractFactory("UUPSLogicV2");
    logicV2 = await LogicV2Factory.deploy();
    await logicV2.deployed();
  });

  it("should initialize correctly", async function () {
    const proxyAsLogicV1 = await ethers.getContractFactory("UUPSLogicV1").then(f => f.attach(proxy.address));
    expect(await proxyAsLogicV1.owner()).to.equal(owner.address);
  });

  it("should set and get value", async function () {
    const proxyAsLogicV1 = await ethers.getContractFactory("UUPSLogicV1").then(f => f.attach(proxy.address));
    await proxyAsLogicV1.setValue(42);
    expect(await proxyAsLogicV1.getValue()).to.equal(42);
  });

  it("should upgrade to LogicV2", async function () {
    const proxyAsLogicV1 = await ethers.getContractFactory("UUPSLogicV1").then(f => f.attach(proxy.address));
    await proxyAsLogicV1.setValue(42);
    await proxyAsLogicV1.upgradeTo(logicV2.address);
    const proxyAsLogicV2 = await ethers.getContractFactory("UUPSLogicV2").then(f => f.attach(proxy.address));
    await proxyAsLogicV2.setValue(10);
    expect(await proxyAsLogicV2.getValue()).to.equal(20);
    expect(await proxyAsLogicV2.owner()).to.equal(owner.address);
  });

  it("should restrict upgrade to owner", async function () {
    const proxyAsLogicV1 = await ethers.getContractFactory("UUPSLogicV1").then(f => f.attach(proxy.address));
    await expect(proxyAsLogicV1.connect(addr1).upgradeTo(logicV2.address)).to.be.revertedWith("Ownable: caller is not the owner");
  });
});
  • 解析
    • 部署:UUPSLogicV1UUPSProxy,通过initData初始化。
    • 操作:设置value为42。
    • 升级:调用upgradeTo切换到LogicV2setValue(10)返回20。
    • 权限:addr1无法升级。
  • UUPS特点:升级逻辑在逻辑合约,代理更轻量。

部署脚本

scripts/deploy.ts

import { ethers } from "hardhat";

async function main() {
  const [owner, admin] = await ethers.getSigners();

  const LogicV1Factory = await ethers.getContractFactory("LogicV1");
  const logicV1 = await LogicV1Factory.deploy();
  await logicV1.deployed();
  console.log(`LogicV1 deployed to: ${logicV1.address}`);

  const ProxyFactory = await ethers.getContractFactory("TransparentProxy");
  const initData = LogicV1Factory.interface.encodeFunctionData("initialize", [owner.address]);
  const proxy = await ProxyFactory.deploy(logicV1.address, admin.address, initData);
  await proxy.deployed();
  console.log(`TransparentProxy deployed to: ${proxy.address}`);

  const UUPSLogicV1Factory = await ethers.getContractFactory("UUPSLogicV1");
  const uupsLogicV1 = await UUPSLogicV1Factory.deploy();
  await uupsLogicV1.deployed();
  console.log(`UUPSLogicV1 deployed to: ${uupsLogicV1.address}`);

  const UUPSProxyFactory = await ethers.getContractFactory("UUPSProxy");
  const uupsProxy = await UUPSProxyFactory.deploy(uupsLogicV1.address, initData);
  await uupsProxy.deployed();
  console.log(`UUPSProxy deployed to: ${uupsProxy.address}`);
}

main().catch((error) => {
  console.error(error);
  process.exitCode = 1;
});

跑部署:

npx hardhat run scripts/deploy.ts --network hardhat
  • 解析:部署透明代理和UUPS代理,记录地址。
点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
天涯学馆
天涯学馆
0x9d6d...50d5
资深大厂程序员,12年开发经验,致力于探索前沿技术!