ERC-7794: Grant Registry
一个跨网络注册合约,用于管理和跟踪补助金,增强补助金计划的透明度和互操作性。
Authors | Guilherme Neves (@0xneves) |
---|---|
Created | 2024-10-22 |
Discussion Link | https://ethereum-magicians.org/t/erc-7794-grant-registry/20791 |
摘要
本提案介绍了一个 Grant Registry 合约,旨在管理财务、研究或基于项目的补助金,这些补助金为跨多个区块链的项目提供资金。该合约通过将数据组织成不同的类别来标准化这些补助金的注册、管理和跟踪,从而实现不可变字段和可变字段之间的明确分离。它支持模块化的拨款跟踪,并允许外部链接到链下文档。该注册表发出生命周期事件,使外部协议能够有效地访问补助金数据,从而提高透明度、互操作性并增强对补助金计划绩效的洞察力。
动机
以太坊生态系统目前缺乏一种跨不同链和计划管理和跟踪补助金的标准化方法,导致效率低下和碎片化。每个补助金计划都有其自己独特的接口、流程和管理机制,这为资助者和受助者都设置了障碍。这些问题阻碍了透明度,使资金拨付的跟踪变得复杂,并且难以评估不同网络中补助金计划的整体有效性。
补助金计划之间缺乏互操作性进一步加剧了这个问题,因为项目和贡献者经常跨多个区块链工作。这使得以一致的方式聚合数据、监控里程碑和评估受助者绩效具有挑战性。
Grant Registry 合约通过引入统一标准来解决这些问题,该标准确保所有补助金都可以被注册、跟踪和管理,而无论底层链或计划如何。这种方法不仅简化了补助金的生命周期管理,还有助于社区之间更好的协作,从而提高竞争力和更好地跟踪进度。此外,数据的标准化为更深入的分析打开了大门,使协议能够以更精简和透明的方式衡量补助金的影响。
规范
本文档中的关键词“必须(MUST)”,“禁止(MUST NOT)”,“需要(REQUIRED)”,“应该(SHALL)”,“不应(SHALL NOT)”,“应当(SHOULD)”,“不应当(SHOULD NOT)”,“推荐(RECOMMENDED)”,“不推荐(NOT RECOMMENDED)”,“可以(MAY)”和“可选(OPTIONAL)”应按照 RFC 2119 和 RFC 8174 中的描述进行解释。
合约接口
// SPDX-License-Identifier: CC0-1.0
pragma solidity ^0.8.20;
interface IGrantRegistry {
/**
* @dev 当社区名称长度无效时抛出(例如,太短或太长)。
*/
error InvalidCommunityNameLength();
/**
* @dev 当调用者不是当前补助金管理者时抛出。
*/
error InvalidGrantManager();
/**
* @dev 当具有提供的 ID 的补助金已被注册时抛出。
*/
error GrantAlreadyRegistered();
/**
* @dev 尝试添加已存在于集合中的受助者时抛出。
*/
error GranteeAlreadyAdded();
/**
* @dev 尝试删除在集合中未找到的受助者时抛出。
*/
error GranteeNotFound();
/**
* @dev 尝试添加或引用无效的外部链接时抛出。
*/
error InvalidExternalLink();
/**
* @dev 当提供无效索引时抛出(例如,数组越界)。
*/
error InvalidIndex();
/**
* @dev 当里程碑日期无效时抛出(例如,早于补助金的开始日期)。
*/
error InvalidStartDate();
/**
* @dev 尝试添加已存在的里程碑日期时抛出。
*/
error MilestoneDateAlreadyAdded();
/**
* @dev 尝试删除或引用未找到的里程碑日期时抛出。
*/
error MilestoneDateNotFound();
/**
* @dev 当注册新的补助金时发出。
* @param grantId 补助金的唯一标识符。
* @param id 补助金的唯一数字 ID。
* @param chainid 补助金注册的链 ID。
* @param community 发放补助金的社区名称。
* @param grantManager 补助金管理者的地址。
*/
event GrantRegistered(
bytes32 indexed grantId,
uint256 indexed id,
uint256 chainid,
string indexed community,
address grantManager
);
/**
* @dev 当补助金的所有权转移时发出。
* @param grantId 补助金的唯一标识符。
* @param newGrantManager 新的补助金管理者的地址。
*/
event OwnershipTransferred(
bytes32 indexed grantId,
address indexed newGrantManager
);
/**
* @dev 当新的受助者被添加到补助金时发出。
* @param grantId 补助金的唯一标识符。
* @param grantee 新的受助者的地址。
*/
event GranteeAdded(bytes32 indexed grantId, address indexed grantee);
/**
* @dev 当受助者从补助金中移除时发出。
* @param grantId 补助金的唯一标识符。
* @param grantee 被移除的受助者的地址。
*/
event GranteeRemoved(bytes32 indexed grantId, address indexed grantee);
/**
* @dev 当补助金的开始日期被设置时发出。
* @param grantId 补助金的唯一标识符。
* @param startDate 代表开始日期的时间戳。
*/
event StartDateSet(bytes32 indexed grantId, uint256 startDate);
/**
* @dev 当新的里程碑日期被添加到补助金时发出。
* @param grantId 补助金的唯一标识符。
* @param milestoneDate 被添加的里程碑的时间戳。
*/
event MilestoneDateAdded(bytes32 indexed grantId, uint256 milestoneDate);
/**
* @dev 当里程碑日期从补助金中移除时发出。
* @param grantId 补助金的唯一标识符。
* @param milestoneDate 被移除的里程碑的时间戳。
*/
event MilestoneDateRemoved(bytes32 indexed grantId, uint256 milestoneDate);
/**
* @dev 当付款被添加到里程碑时发出。
* @param grantId 补助金的唯一标识符。
* @param milestoneDate 里程碑的时间戳。
* @param fundingToken 用于付款的代币。
* @param fundingAmount 付款的金额。
*/
event DisbursementAdded(
bytes32 indexed grantId,
uint256 milestoneDate,
address indexed fundingToken,
uint256 fundingAmount
);
/**
* @dev 当付款从里程碑中移除时发出。
* @param grantId 补助金的唯一标识符。
* @param milestoneDate 里程碑的时间戳。
*/
event DisbursementRemoved(bytes32 indexed grantId, uint256 milestoneDate);
/**
* @dev 当付款状态被更新时发出。
* @param grantId 补助金的唯一标识符。
* @param milestoneDate 里程碑的时间戳。
* @param isDisbursed 布尔值,指示是否已进行付款。
*/
event DisbursementMade(
bytes32 indexed grantId,
uint256 milestoneDate,
bool isDisbursed
);
/**
* @dev 当外部链接被添加到补助金时发出。
* @param grantId 补助金的唯一标识符。
* @param link 被添加的外部 URL。
*/
event ExternalLinkAdded(bytes32 indexed grantId, string link);
/**
* @dev 当外部链接从补助金中移除时发出。
* @param grantId 补助金的唯一标识符。
* @param link 被移除的外部 URL。
*/
event ExternalLinkRemoved(bytes32 indexed grantId, string link);
/**
* @dev 使用提供的详细信息注册新的补助金。`grantId` 通过哈希补助金的详细信息和当前时间戳生成。
*
* 要求:
*
* - `grantManager` 地址不能为零地址。
* - `community` 名称不能为空。
* - 补助金必须尚未注册。
*
* 发出 {GrantRegistered} 事件。
*
* @param id 补助金计划的唯一标识符。
* @param chainid 补助金注册的链 ID。
* @param community 发放补助金的社区或协议的名称。
* @param grantManager 补助金管理者的地址。
* @return 生成的 `grantId` 作为 bytes32 值。
*/
function registerGrant(
uint256 id,
uint256 chainid,
string memory community,
address grantManager
) external returns (bytes32);
/**
* @dev 将补助金的所有权转移给新的补助金管理者。
*
* 要求:
*
* - 调用者必须是当前的补助金管理者。
* - `newGrantManager` 地址不能为零地址。
*
* 发出 {OwnershipTransferred} 事件。
*
* @param grantId 补助金的唯一标识符。
* @param newGrantManager 新的补助金管理者的地址。
*/
function transferOwnership(bytes32 grantId, address newGrantManager) external;
/**
* @dev 将新的受助者添加到补助金。
*
* 要求:
*
* - 调用者必须是当前的补助金管理者。
* - `grantee` 地址不能为零地址。
*
* 发出 {GranteeAdded} 事件。
*
* @param grantId 补助金的唯一标识符。
* @param grantee 要添加的受助者的地址。
*/
function addGrantee(bytes32 grantId, address grantee) external;
/**
* @dev 从补助金中移除现有的受助者。
*
* 要求:
*
* - 调用者必须是当前的补助金管理者。
* - `grantee` 地址必须存在于补助金中。
*
* 发出 {GranteeRemoved} 事件。
*
* @param grantId 补助金的唯一标识符。
* @param grantee 要移除的受助者的地址。
*/
function removeGrantee(bytes32 grantId, address grantee) external;
/**
* @dev 设置补助金的开始日期。
*
* 要求:
*
* - 调用者必须是当前的补助金管理者。
*
* 发出 {StartDateSet} 事件。
*
* @param grantId 补助金的唯一标识符。
* @param startDate 代表开始日期的时间戳。
*/
function setStartDate(bytes32 grantId, uint256 startDate) external;
/**
* @dev 为补助金添加新的里程碑日期。
*
* 要求:
*
* - 调用者必须是当前的补助金管理者。
* - 里程碑日期不能已存在于补助金中。
*
* 发出 {MilestoneDateAdded} 事件。
*
* @param grantId 补助金的唯一标识符。
* @param milestoneDate 代表里程碑日期的时间戳。
*/
function addMilestoneDate(bytes32 grantId, uint256 milestoneDate) external;
/**
* @dev 从补助金中移除里程碑日期。
*
* 要求:
*
* - 调用者必须是当前的补助金管理者。
* - 里程碑日期必须存在于补助金中。
*
* 发出 {MilestoneDateRemoved} 事件。
*
* @param grantId 补助金的唯一标识符。
* @param milestoneDate 要移除的里程碑日期的时间戳。
*/
function removeMilestoneDate(bytes32 grantId, uint256 milestoneDate) external;
/**
* @dev 覆盖特定里程碑的付款。
*
* 要求:
*
* - 调用者必须是当前的补助金管理者。
* - 里程碑日期必须存在于补助金中。
*
* 发出 {DisbursementAdded} 事件。
*
* @param grantId 补助金的唯一标识符。
* @param milestoneDate 与付款关联的里程碑日期。
* @param fundingToken 用于资助的代币地址。
* @param fundingAmount 要支付的代币数量。
*/
function addDisbursement(
bytes32 grantId,
uint256 milestoneDate,
address fundingToken,
uint256 fundingAmount
) external;
/**
* @dev 移除特定里程碑的付款。
*
* 要求:
*
* - 调用者必须是当前的补助金管理者。
* - 里程碑日期必须存在于补助金中。
*
* 发出 {DisbursementRemoved} 事件。
*
* @param grantId 补助金的唯一标识符。
* @param milestoneDate 与付款关联的里程碑日期。
*/
function removeDisbursement(bytes32 grantId, uint256 milestoneDate) external;
/**
* @dev 更新特定里程碑的付款状态。
*
* 要求:
*
* - 调用者必须是当前的补助金管理者。
* - 里程碑日期必须存在于补助金中。
*
* 发出 {DisbursementMade} 事件。
*
* @param grantId 补助金的唯一标识符。
* @param milestoneDate 与付款关联的里程碑日期。
* @param isDisbursed 一个布尔值,指示是否已进行付款。
*/
function setDisbursementStatus(
bytes32 grantId,
uint256 milestoneDate,
bool isDisbursed
) external;
/**
* @dev 添加与补助金相关的外部链接。
*
* 要求:
*
* - 调用者必须是当前的补助金管理者。
* - 链接不能为空。
*
* 发出 {ExternalLinkAdded} 事件。
*
* @param grantId 补助金的唯一标识符。
* @param link 要添加的外部 URL。
*/
function addExternalLink(bytes32 grantId, string memory link) external;
/**
* @dev 移除与补助金关联的外部链接。
*
* 要求:
*
* - 调用者必须是当前的补助金管理者。
* - 索引必须在外部链接数组的范围内。
*
* 发出 {ExternalLinkRemoved} 事件。
*
* @param grantId 补助金的唯一标识符。
* @param index 要移除的外部链接的索引。
*/
function removeExternalLink(bytes32 grantId, uint256 index) external;
/**
* @dev 通过 ID 检索特定补助金的详细信息
* @param grantId 补助金的唯一标识符
* @return 包含 id,chainid 和 community 标签的 `Grant` 结构体
*/
function getGrant(bytes32 grantId) external view returns (Grant memory);
/**
* @dev 检索特定补助金的当前补助金管理者
* @param grantId 补助金的唯一标识符
* @return 补助金管理者的地址
*/
function getGrantManager(bytes32 grantId) external view returns (address);
/**
* @dev 检索与特定补助金关联的受助者列表
* @param grantId 补助金的唯一标识符
* @return 表示受助者的地址数组
*/
function getGrantees(
bytes32 grantId
) external view returns (address[] memory);
/**
* @dev 检索特定补助金的开始日期和里程碑日期列表
* @param grantId 补助金的唯一标识符
* @return 开始日期和里程碑日期数组
*/
function getMilestonesDates(
bytes32 grantId
) external view returns (uint256, uint256[] memory);
/**
* @dev 检索补助金中特定里程碑的付款详细信息
* @param grantId 补助金的唯一标识符
* @param milestoneDate 请求付款详细信息的里程碑日期
* @return 包含代币地址,资助金额和付款状态的 `Disbursements` 结构体
*/
function getDisbursement(
bytes32 grantId,
uint256 milestoneDate
) external view returns (Disbursements memory);
/**
* @dev 检索与特定补助金关联的外部链接列表
* @param grantId 补助金的唯一标识符
* @return 表示外部链接的字符串数组
*/
function getExternalLinks(
bytes32 grantId
) external view returns (string[] memory);
}
当调用 registerGrant
函数时:
grantManager
必须提交一个有效的 grantManager 地址,该地址不能为零地址。community
标签 必须 是一个非空字符串。grant
ID 必须 是唯一的,并且尚未在系统中注册。
当编辑整体补助金详细信息时:
grantManager
必须 是当前的补助金管理者才能更改补助金。
当添加 milestoneDate
时:
milestoneDate
必须 不存在于 milestonesDates 集合中。
当编辑 disbursments 时:
milestoneDate
必须 是与补助金关联的有效里程碑日期。
当添加 externalLinks
时:
- 字符串 必须 不为空。
原理
该 Grant Registry 合约的设计是由对灵活且模块化的系统的需求驱动的,该系统支持跨不同链的各种补助金计划。以下概述了关键设计决策的 rationale:
-
字段分离:将字段划分为不同的类别,例如标识、补助金数据和与付款相关的信息,可以更有效地利用链上存储。像 id、chainid 和 community 这样的不可变字段与可变字段分开保存,确保核心标识元素保持不变,而里程碑和参与者等其他方面可以在整个补助金生命周期中更新。
-
模块化付款处理:并非所有补助金计划都选择在链上执行付款。通过允许通过外部链接管理付款,该合约保持模块化,并适应不同的用例。喜欢在链下处理付款的计划仍然可以使用注册表进行状态跟踪,从而确保在不同的生态系统中具有广泛的适用性。
-
动态团队管理:participants 结构使用 EnumerableSet 作为受助者,从而允许基于团队的补助金。此功能有助于跟踪贡献和调整补助金团队,从而实现更全面的声誉系统和透明度。
此设计旨在创建一个可扩展、高效的系统,该系统可以随着不同补助金计划的需求而发展,同时保持诸如透明度、模块化和低 gas 使用之类的关键优势。
向后兼容性
未发现向后兼容性问题。
参考实现
// SPDX-License-Identifier: CC0-1.0
pragma solidity ^0.8.20;
import { IGrantRegistry } from "./IGrantRegistry.sol";
import { EnumerableSet } from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol";
contract GrantRegistry is IGrantRegistry {
using EnumerableSet for EnumerableSet.AddressSet;
using EnumerableSet for EnumerableSet.UintSet;
/**
* @dev 用于存储每个补助金的详细信息的映射,由其唯一的 grantId 键控。
*/
mapping(bytes32 => Grant) private _grants;
/**
* @dev 存储有关每个补助金的参与者(管理者和受助者)的信息,由 grantId 键控。
* 此映射允许跟踪每个补助金的补助金管理者和关联的受助者。
*/
mapping(bytes32 => Participants) private _participants;
/**
* @dev 存储每个补助金的里程碑相关数据,由 grantId 键控。
* 这包括开始日期、里程碑日期和与每个里程碑相关的付款。
*/
mapping(bytes32 => Milestones) private _milestones;
/**
* @dev 存储与每个补助金相关的外部链接,例如提案 URL 或相关文档,由 grantId 键控。
* 外部链接提供对有关补助金的链下信息的引用。
*/
mapping(bytes32 => string[]) private _externalLinks;
/**
* @dev 参见 {IGrantRegistry-registerGrant}。
*/
function registerGrant(
uint256 id,
uint256 chainid,
string memory community,
address grantManager
) external returns (bytes32) {
bytes32 grantId = keccak256(
abi.encodePacked(id, chainid, community, block.timestamp)
);
if (grantManager == address(0)) revert InvalidGrantManager();
if (bytes(community).length == 0) revert InvalidCommunityNameLength();
if (bytes(_grants[grantId].community).length > 0)
revert GrantAlreadyRegistered();
_grants[grantId] = Grant(id, chainid, community);
_participants[grantId].grantManager = grantManager;
emit GrantRegistered(grantId, id, chainid, community, grantManager);
return grantId;
}
/**
* @dev 参见 {IGrantRegistry-transferOwnership}。
*/
function transferOwnership(
bytes32 grantId,
address newGrantManager
) external {
_requireManager(grantId);
if (newGrantManager == address(0)) revert InvalidGrantManager();
_participants[grantId].grantManager = newGrantManager;
emit OwnershipTransferred(grantId, newGrantManager);
}
/**
* @dev 参见 {IGrantRegistry-addGrantee}。
*/
function addGrantee(bytes32 grantId, address grantee) external {
_requireManager(grantId);
if (grantee == address(0)) revert InvalidGrantManager();
bool success = _participants[grantId].grantees.add(grantee);
if (!success) revert GranteeAlreadyAdded();
emit GranteeAdded(grantId, grantee);
}
/**
* @dev 参见 {IGrantRegistry-removeGrantee}。
*/
function removeGrantee(bytes32 grantId, address grantee) external {
_requireManager(grantId);
bool success = _participants[grantId].grantees.remove(grantee);
if (!success) revert GranteeNotFound();
emit GranteeRemoved(grantId, grantee);
}
/**
* @dev 参见 {IGrantRegistry-setStartDate}。
*/
function setStartDate(bytes32 grantId, uint256 startDate) external {
_requireManager(grantId);
_milestones[grantId].startDate = startDate;
emit StartDateSet(grantId, startDate);
}
/**
* @dev 参见 {IGrantRegistry-addMilestoneDate}。
*/
function addMilestoneDate(bytes32 grantId, uint256 milestoneDate) external {
_requireManager(grantId);
bool success = _milestones[grantId].milestonesDates.add(milestoneDate);
if (!success) revert MilestoneDateAlreadyAdded();
emit MilestoneDateAdded(grantId, milestoneDate);
}
/**
* @dev 参见 {IGrantRegistry-removeMilestoneDate}。
*/
function removeMilestoneDate(
bytes32 grantId,
uint256 milestoneDate
) external {
_requireManager(grantId);
bool success = _milestones[grantId].milestonesDates.remove(milestoneDate);
if (!success) revert MilestoneDateNotFound();
emit MilestoneDateRemoved(grantId, milestoneDate);
}
/**
* @dev 参见 {IGrantRegistry-addDisbursement}。
*/
function addDisbursement(
bytes32 grantId,
uint256 milestoneDate,
address fundingToken,
uint256 fundingAmount
) external {
_requireManager(grantId);
_requireMilestoneDate(grantId, milestoneDate);
_milestones[grantId].disbursements[milestoneDate] = Disbursements(
fundingToken,
fundingAmount,
false
);
emit DisbursementAdded(grantId, milestoneDate, fundingToken, fundingAmount);
}
/**
* @dev 参见 {IGrantRegistry-removeDisbursement}。
*/
function removeDisbursement(bytes32 grantId, uint256 milestoneDate) external {
_requireManager(grantId);
_requireMilestoneDate(grantId, milestoneDate);
delete _milestones[grantId].disbursements[milestoneDate];
emit DisbursementRemoved(grantId, milestoneDate);
}
/**
* @dev 参见 {IGrantRegistry-setDisbursementStatus}。
*/
function setDisbursementStatus(
bytes32 grantId,
uint256 milestoneDate,
bool isDisbursed
) external {
_requireManager(grantId);
_requireMilestoneDate(grantId, milestoneDate);
_milestones[grantId].disbursements[milestoneDate].isDisbursed = isDisbursed;
emit DisbursementMade(grantId, milestoneDate, isDisbursed);
}
/**
* @dev 参见 {IGrantRegistry-addExternalLink}。
*/
function addExternalLink(bytes32 grantId, string memory link) external {
_requireManager(grantId);
if (bytes(link).length == 0) revert InvalidExternalLink();
_externalLinks[grantId].push(link);
emit ExternalLinkAdded(grantId, link);
}
/**
* @dev 参见 {IGrantRegistry-removeExternalLink}。
*/
function removeExternalLink(bytes32 grantId, uint256 index) external {
_requireManager(grantId);
if (index >= _externalLinks[grantId].length) revert InvalidIndex();
string memory link = _externalLinks[grantId][index];
_externalLinks[grantId][index] = _externalLinks[grantId][
_externalLinks[grantId].length - 1
];
_externalLinks[grantId].pop();
emit ExternalLinkRemoved(grantId, link);
}
/**
* @dev 确保调用者是给定 grantId 的补助金管理者。
* 如果调用者不是补助金管理者,则会抛出 `InvalidGrantManager`。
* @param grantId 被检查的补助金的唯一标识符。
*/
function _requireManager(bytes32 grantId) internal view {
if (msg.sender != _participants[grantId].grantManager)
revert InvalidGrantManager();
}
/**
* @dev 确保里程碑日期出现在补助金中。
* 如果里程碑日期不存在,则会抛出 `MilestoneDateNotFound`。
* @param grantId 被检查的补助金的唯一标识符。
* @param milestoneDate 被检查的里程碑日期。
*/
function _requireMilestoneDate(
bytes32 grantId,
uint256 milestoneDate
) internal view {
if (!_milestones[grantId].milestonesDates.contains(milestoneDate))
revert MilestoneDateNotFound();
}
/**
* @dev 参见 {IGrantRegistry-getGrant}。
*/
function getGrant(bytes32 grantId) external view returns (Grant memory) {
return _grants[grantId];
}
/**
* @dev 参见 {IGrantRegistry-getGrantManager}。
*/
function getGrantManager(bytes32 grantId) external view returns (address) {
return _participants[grantId].grantManager;
}
/**
* @dev 参见 {IGrantRegistry-getGrantees}。
*/
function getGrantees(
bytes32 grantId
) external view returns (address[] memory) {
return _participants[grantId].grantees.values();
}
/**
* @dev 参见 {IGrantRegistry-getMilestonesDates}。
*/
function getMilestonesDates(
bytes32 grantId
) external view returns (uint256, uint256[] memory) {
return (
_milestones[grantId].startDate,
_milestones[grantId].milestonesDates.values()
);
}
/**
* @dev 参见 {IGrantRegistry-getDisbursement}。
*/
function getDisbursement(
bytes32 grantId,
uint256 milestoneDate
) external view returns (Disbursements memory) {
return _milestones[grantId].disbursements[milestoneDate];
}
/**
* @dev 参见 {IGrantRegistry-getExternalLinks}。
*/
function getExternalLinks(
bytes32 grantId
) external view returns (string[] memory) {
return _externalLinks[grantId];
}
}
此实现的关键考虑因素:
-
Gas 优化:
grantId
利用不可变的标识字段来最大限度地减少大量的 gas 消耗。这确保了关键信息能够有效地与 keccak256 一起使用,而可变数据可以在项目发展后提交或修改,而不会影响识别方法。 -
使用 EnumerableSet:通过利用 EnumerableSet 来管理参与者和里程碑日期,该合约允许动态更新,例如团队组成更改或新里程碑。这种方法提供了灵活性,而又不牺牲有效跟踪更改的能力。
安全考虑事项
未发现安全问题。
版权
版权及相关权利通过 CC0 放弃。
Citation
Please cite this document as:
Guilherme Neves (@0xneves), "ERC-7794: Grant Registry [DRAFT]," Ethereum Improvement Proposals, no. 7794, October 2024. [Online serial]. Available: https://eips.ethereum.org/EIPS/eip-7794.