在区块链上,合约一旦部署便不可修改,这是去中心化的根本保障。但对于复杂应用来说,这却成了一把双刃剑:
于是,出现了 代理合约升级模式(第 14 课介绍过):
但是:
于是,社区提出了 Diamond Standard(EIP-2535) —— 允许一个代理合约挂载多个逻辑模块(Facet),形成插件化架构。
EIP-2535 的核心思想是:
合约应该像一个操作系统,可以随时增加或删除功能,而不是“一次性写死”。
设计目标:
一个 Diamond 系统一般包含:
addFacet / replaceFacet / removeFacet 方法传统 Proxy 调用:
用户 → Proxy → delegatecall → Logic
Diamond 调用:
用户 → Diamond → 查找 Facet 地址 → delegatecall → Facet
区别:
在 delegatecall 中,执行代码用的是 Facet 的逻辑,但读写的是 Diamond 的存储。
所以:
常见写法(StorageLib.sol):
library LibAppStorage {
bytes32 constant DIAMOND_STORAGE_POSITION = keccak256("diamond.standard.app.storage");
struct AppStorage {
uint256 value;
mapping(address => uint256) balances;
}
function diamondStorage() internal pure returns (AppStorage storage ds) {
bytes32 position = DIAMOND_STORAGE_POSITION;
assembly {
ds.slot := position
}
}
}每个 Facet 使用 LibAppStorage 获取同一份存储,避免覆盖。
我们写一个最小可运行的 Diamond 合约系统。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
contract Diamond {
mapping(bytes4 => address) public facets;
address public owner;
modifier onlyOwner() {
require(msg.sender == owner, "Not owner");
_;
}
constructor() {
owner = msg.sender;
}
function addFacet(bytes4 selector, address facet) external onlyOwner {
facets[selector] = facet;
}
fallback() external payable {
address facet = facets[msg.sig];
require(facet != address(0), "Function not found");
assembly {
calldatacopy(0, 0, calldatasize())
let result := delegatecall(gas(), facet, 0, calldatasize(), 0, 0)
returndatacopy(0, 0, returndatasize())
switch result
case 0 { revert(0, returndatasize()) }
default { return(0, returndatasize()) }
}
}
receive() external payable {}
}// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
contract FacetA {
uint256 public value;
function setValue(uint256 _v) external {
value = _v;
}
}// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
contract FacetB {
uint256 public number;
function add(uint256 a, uint256 b) external returns (uint256) {
number = a + b;
return number;
}
}// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "forge-std/Test.sol";
import "../src/Diamond.sol";
import "../src/FacetA.sol";
import "../src/FacetB.sol";
contract DiamondTest is Test {
Diamond diamond;
FacetA facetA;
FacetB facetB;
function setUp() public {
diamond = new Diamond();
facetA = new FacetA();
facetB = new FacetB();
diamond.addFacet(FacetA.setValue.selector, address(facetA));
diamond.addFacet(FacetB.add.selector, address(facetB));
}
function testFacetACall() public {
(bool ok, ) = address(diamond).call(
abi.encodeWithSelector(FacetA.setValue.selector, 42)
);
assertTrue(ok, "FacetA call failed");
}
function testFacetBCall() public {
(bool ok, bytes memory data) = address(diamond).call(
abi.encodeWithSelector(FacetB.add.selector, 1, 2)
);
assertTrue(ok, "FacetB call failed");
uint256 result = abi.decode(data, (uint256));
assertEq(result, 3);
}
}运行:
➜ tutorial git:(main) ✗ forge test --match-path test/Diamond.t.sol -vvv
[⠊] Compiling...
[⠒] Compiling 4 files with Solc 0.8.30
[⠑] Solc 0.8.30 finished in 522.72ms
Compiler run successful!
Ran 2 tests for test/Diamond.t.sol:DiamondTest
[PASS] testFacetACall() (gas: 33464)
[PASS] testFacetBCall() (gas: 34743)
Suite result: ok. 2 passed; 0 failed; 0 skipped; finished in 4.38ms (1.35ms CPU time)
Ran 1 test suite in 162.88ms (4.38ms CPU time): 2 tests passed, 0 failed, 0 skipped (2 total tests)思考题:
这一课我们系统学习了 Diamond Standard(EIP-2535) 的设计思想、简化实现与应用场景。
下一课(第 18 课)我们将转向 安全专题,研究常见攻击手法(Reentrancy、Front-running、DoS)与防御手段,从进攻视角理解安全设计。