Openzeppelin库详解-AccessControlDefaultAdminRules

文章介绍了AccessControlDefaultAdminRules合约,强化了对默认管理员角色组的操作,包括角色组权限、账户变更流程、延迟参数管理和权限控制。关键功能包括单账户规则、账户变更两步法、延迟参数修改及权限转移的即时性和安全性。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

AccessControlDefaultAdminRules合约是对AccessControl合约的增强,主要是对默认管理员身份组(默认管理员身份组id就是全0的bytes32)的相关操作进行了控制,具体体现在:

1、“默认管理员”角色组(该角色组具有授权、取消授权其他角色组账户的权力,前提是其他角色组的管理员身份id是默认管理员,如果其他身分组设置了指定的管理员身份组,那就跟默认管理元没有关系了)中最多只能有一个账户;

2、进行“默认管理员”角色组中账户变更时,通过两步完成,原始账户先发起账户变更,同时会设置一个时间延迟,新的账户需要在时间延迟后发起接受,才能完成管理员账户转移。在新账户发起接受之前,原始帐户可以取消账户变更;

3、”默认管理员”角色组中账户变更时的延迟参数,支持实时修改;

4、其他任何身份组无权对“默认管理员”

可用实例如下:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import {AccessControlDefaultAdminRules} from "@openzeppelin/contracts/access/extensions/AccessControlDefaultAdminRules.sol";
contract MyAccessControlDefaultAdminRules is AccessControlDefaultAdminRules {
    constructor() AccessControlDefaultAdminRules(
    3 days,
    msg.sender // Explicit initial `DEFAULT_ADMIN_ROLE` holder
    ) {}
}

在初始化阶段,指定了延迟时间(3天)和管理员身份组账户(合约创建账户)。

编译后对外暴露接口为:

在解释各个接口之前,大家要先有延迟生效的概念,即发起了账户变更、延迟参数变更,都是需要等待一段时间后才能生效的,保留了一个可以撤回错误操作的窗口期。

按照逻辑先后顺序先对管理员身份组账户变更操作的接口如下:

1、 beginDefaultAdminTransfer:将“默认管理员”身份组的账户变更为新账户,同时设置生效的时间点,只能由当前“默认管理员”身份组账户发起;

2、cancelDefaultAdminTransfer:在新账户接受成为“默认管理员”身份组账户之前,取消上一步的账户变更,只能由当前“默认管理员”身份组账户发起;

3、acceptDefaultAdminTransfer:接受“默认管理员”身份组的账户变更,只有在第一步设置的生效时间之后才能成功发起,且只能由发起变更时指定的新账户发起;

对”默认管理员”身份组的账户变更中使用到的延迟参数操作接口如下:

1、changeDefaultAdminDelay:修改账户变更时“时间延迟”参数,设置本次修改的生效时间点,只能由当前“默认管理员”身份组账户发起,“时间延迟”参数修改后生效也有延迟;

2、rollbackDefaultAdminDelay;取消上一步“时间延迟”参数的修改,只能由当前“默认管理员”身份组账户发起,该操作只有在前一步设置的生效时间点之前才有效;

权限管理相关的接口如下:

1、grantRole:身份组授权,不允许对“默认管理员”身份组操作;

2、revokeRole:身份组解除授权,不允许对“默认管理员”身份组操作;

3、renounceRole:解除账户对某身份组的授权,只能由解除账户自身发起;如果是对“默认管理员”身份组操作,则也是要结合beginDefaultAdminTransfer进行两步操作,通过beginDefaultAdminTransfer将账户变更为address(0),然后由原始的“默认管理员”身份组账户调用renounceRole进行接受,同样只能在设置的生效时间点之后才能才成功发起,要注意因为没有其他角色组能对“默认管理员”管理,在renounceRole“默认管理员”中唯一的有效账户后,没有版本再为“默认管理员”身份组指定新账户(除非还实现了其他方法调用了内部函数_grantRole);

状态变量查看函数如下:

1、DEFAULT_ADMIN_ROLE:查看“默认管理员”角色id

2、defaultAdmin、owner:查看“默认管理员”中的账户;

3、defaultAdminDelay:查看生效中的延迟参数,该参数会参与生效时间点的计算;

4、defaultAdminDelayIncreaseWait:查看增大延迟参数,默认的最大的等待时间,当前设置的是5 days;

5、pendingDefaultAdmin:获取待生效“默认管理员”身份组账户,和生效时间点;

6、pendingDefaultAdminDelay:获取待生效的“时间延迟”参数,和该参数的生效时间点;

最后就是权限管理接口grantRole、revokeRole,除了不能操作“默认管理员”身份组,其他同AccessControl。

举例如下:

用A表示当前“默认管理员”角色组账户,A想要发起管理员账户转移,目标账户为B,则可以用账户A调用beginDefaultAdminTransfer(B);账户B想要立刻接受成为管理员账户,所以当天账户B发起了acceptDefaultAdminTransfer(),但是发现接受失败了,因为账户B想要接受成功,必须要等待3天;在第2天的时候,A突然发现其实应该要转移给账户C,于是调用了cancelDefaultAdminTransfer(),将向B转移管理员身份这个动作撤销,然后立刻调用beginDefaultAdminTransfer(C),将管理员身份转移给C;B在等待了3天后,再次发起acceptDefaultAdminTransfer(),仍然失败,因为他已经不是潜在管理员账户;A在发起向C转移管理员的第2天,想让C再多等几天,于是发起了changeDefaultAdminDelay(10 days),但是这个修改时间延迟的动作本身也会延迟生效,延迟时间为5天(系统预设的延迟时间增加时最大等待时间),所以C在beginDefaultAdminTransfer(C)发起的3天后发起acceptDefaultAdminTransfer(),交易能够成功,此刻时间延迟参数修改为10天还没有生效,时间延迟仍然为3天,C成为新的“默认管理员”身份组账户;如果A在发起向C转移管理员的当天,想让C少等几天,于是发起了changeDefaultAdminDelay(1 days),但是这个修改时间延迟的动作本身也会延迟生效,延迟时间为2天(需要等待新的时间延迟域旧的时间延迟之间的差值),所以C在beginDefaultAdminTransfer(C)发起的2天后发起acceptDefaultAdminTransfer(),交易失败,此刻时间延迟参数修改为1天还没有生效,时间延迟仍然为3天;

代码详解

状态变量与构造函数如下:

    // pending admin pair read/written together frequently
    //待生效“默认管理员”身份组账户,在没有账户变更情况下,为address(0)
    address private _pendingDefaultAdmin;
    //管理员账户变更生效时间点,在没有账户变更情况下,为0
    uint48 private _pendingDefaultAdminSchedule; // 0 == unset

    //有效的延迟时间参数,在计算生效时间点时,会参与计算
    uint48 private _currentDelay;

    //当前有效的“默认管理员”身份组账户
    address private _currentDefaultAdmin;

    // pending delay pair read/written together frequently
    //待生效的延迟时间参数
    uint48 private _pendingDelay;

    //待生效的延迟时间参数的生效时间点,即_pendingDelaySchedule时间点之后,_currentDelay在逻辑上(有可能_currentDelay的内容还没有被替换为_pendingDelay的值)的取值是就是_pendingDelay
    uint48 private _pendingDelaySchedule; // 0 == unset

    /**
     * @dev Sets the initial values for {defaultAdminDelay} and {defaultAdmin} address.
     */
    constructor(uint48 initialDelay, address initialDefaultAdmin) {
        if (initialDefaultAdmin == address(0)) {
            revert AccessControlInvalidDefaultAdmin(address(0));
        }
        _currentDelay = initialDelay;
        _grantRole(DEFAULT_ADMIN_ROLE, initialDefaultAdmin);
    }

综合看来,核心参数只有两个,一个是“默认管理员”身份组账户,一个是延迟参数,其他都是围绕这两个参数的待生效参数和生效时间点参数。

先看下“默认管理员”账户变更beginDefaultAdminTransfer的逻辑:

    //账户变函数
    function beginDefaultAdminTransfer(address newAdmin) public virtual onlyRole(DEFAULT_ADMIN_ROLE) {
        _beginDefaultAdminTransfer(newAdmin);
    }
    /*账户变更函数具体实现逻辑:
    首先生成生效时间点,在当前时间基础上加上“延迟时间”参数;
    然后将待生效的新账户和待生效的时间点设置到账户待生效相关字段中;
    */
    function _beginDefaultAdminTransfer(address newAdmin) internal virtual {
        uint48 newSchedule = SafeCast.toUint48(block.timestamp) + defaultAdminDelay();
        _setPendingDefaultAdmin(newAdmin, newSchedule);
        emit DefaultAdminTransferScheduled(newAdmin, newSchedule);
    }

    /*计算“时间延迟”参数:
    如果“时间延迟”参数被发起过修改(判定方式是,“时间延迟”参数的待生效时间节                
    点(_pendingDelaySchedule)已经设置,即:_isScheduleSet(schedule)=true);然后判断如果当前 
    时间已经过了生效时间点,即_hasSchedulePassed(schedule)=true,那么说明待生效的“时间延迟”参数 
    已经可以起作用了,所以这时返回待生效的“时间延迟”参数作为真实的延迟时间。
    如果上述条件没有同时满足,则说明目前的“时间延迟”参数_currentDelay是有效的,则返回作为真实的延迟时间
    */
    function defaultAdminDelay() public view virtual returns (uint48) {
        uint48 schedule = _pendingDelaySchedule;
        return (_isScheduleSet(schedule) && _hasSchedulePassed(schedule)) ? _pendingDelay : _currentDelay;
    }

再看下接受“默认管理员”账户变更acceptDefaultAdminTransfer的逻辑:

    //控制了该方法只能由待生效账户调用
    function acceptDefaultAdminTransfer() public virtual {
        (address newDefaultAdmin, ) = pendingDefaultAdmin();
        if (_msgSender() != newDefaultAdmin) {
            // Enforce newDefaultAdmin explicit acceptance.
            revert AccessControlInvalidDefaultAdmin(_msgSender());
        }
        _acceptDefaultAdminTransfer();
    }
    //先判定账户待生效的时间点是否已经满足
    //满足之后,进行“默认管理员”身份组账户变更具体操作,包括:
    //解除旧帐户在“默认管理员”身份组账户中的授权
    //向“默认管理员”身份组进行新账户授权
    //初始化账户变更待生效相关参数
    //可能有人会疑问为何没有将newAdmin赋值给_currentDefaultAdmin,其实在_grantRole中已经包含了这个步骤
    function _acceptDefaultAdminTransfer() internal virtual {
        (address newAdmin, uint48 schedule) = pendingDefaultAdmin();
        if (!_isScheduleSet(schedule) || !_hasSchedulePassed(schedule)) {
            revert AccessControlEnforcedDefaultAdminDelay(schedule);
        }
        _revokeRole(DEFAULT_ADMIN_ROLE, defaultAdmin());
        _grantRole(DEFAULT_ADMIN_ROLE, newAdmin);
        delete _pendingDefaultAdmin;
        delete _pendingDefaultAdminSchedule;
    }
    //重写解除授权内部函数,在操作“默认管理员”身份组账户时,还要进行当前管理员账户的初始化
    function _revokeRole(bytes32 role, address account) internal virtual override returns (bool) {
        if (role == DEFAULT_ADMIN_ROLE && account == defaultAdmin()) {
            delete _currentDefaultAdmin;
        }
        return super._revokeRole(role, account);
    }
    //重写授权内部函数,在操作“默认管理员”身份组账户时,控制只有现在有效管理员账户为初始化状态时,才能进行后续操作,有效管理员账户为初始化状态只有两种情况,一种是新账户调用了_revokeRole解除了旧帐户授权,一种是旧帐户解除自身授权:将账户比那更为address(0),然后调用了renounceRole进行生效,renounceRole中调用了super.renounceRole(role, account),然后super.renounceRole中有调用内部函数_revoke,因为_revoke在当前合约被重写了,所以会采用当前合约的版本,所以进行delete _currentDefaultAdmin;
    //将当前有效管理账户赋值为传入的新账户地址
    function _grantRole(bytes32 role, address account) internal virtual override returns (bool) {
        if (role == DEFAULT_ADMIN_ROLE) {
            if (defaultAdmin() != address(0)) {
                revert AccessControlEnforcedDefaultAdminRules();
            }
            _currentDefaultAdmin = account;
        }
        return super._grantRole(role, account);
    }

 然后看下取消账户变更cancelDefaultAdminTransfer的逻辑:

    //撤销已经发起的“默认管理员”身份组账户变更操作,只有当前有效管理员账户才能发起,这个操作在逻辑上只有新账户还没有接受之前才是有效的,否则因为onlyRole(DEFAULT_ADMIN_ROLE)限制,旧账户不再有权限调用该方法
    function cancelDefaultAdminTransfer() public virtual onlyRole(DEFAULT_ADMIN_ROLE) {
        _cancelDefaultAdminTransfer();
    }

    //通过将待生效账户和待生效的时间点均初始化为0实现撤销变更,即刻生效
    function _cancelDefaultAdminTransfer() internal virtual {
        _setPendingDefaultAdmin(address(0), 0);
    }

    //设置待生效的“默认管理员”身份组新账户、新账户生效的时间点
    function _setPendingDefaultAdmin(address newAdmin, uint48 newSchedule) private {

        //获取存量的待生效时间点参数
        (, uint48 oldSchedule) = pendingDefaultAdmin();

        _pendingDefaultAdmin = newAdmin;
        _pendingDefaultAdminSchedule = newSchedule;

        // An `oldSchedule` from `pendingDefaultAdmin()` is only set if it hasn't been accepted.
        
        //如果待生效时间点有设置内容,说明之前发起过账户变更,只是还未生效(包括时间未到和新账户还未接受两种情况),因此抛出账户变更撤销事件
        if (_isScheduleSet(oldSchedule)) {
            // Emit for implicit cancellations when another default admin was scheduled.
            emit DefaultAdminTransferCanceled();
        }
    }

最后看下解除自身授权renounceRole的逻辑:

    //解除账户对某个身份组的授权,必须要由被解除账户自身发起,发起该操作后,默认管理员账户为0,所有默认管理账户账户能发起的方法均无效
    //如果操作的是“默认管理员”身份组,那么则需要通过两步来完成,第一步,通过beginDefaultAdminTransfer将管理员地址变更为address(0),第二步,在有效时间点之后调用renounceRole进行确认
    function renounceRole(bytes32 role, address account) public virtual override(AccessControl, IAccessControl) {
        //如果是“默认管理员”身份组,则要求目的账户得是原始的管理员账户
        if (role == DEFAULT_ADMIN_ROLE && account == defaultAdmin()) {
            //获取待生效的新账户和待生效时间点
            (address newDefaultAdmin, uint48 schedule) = pendingDefaultAdmin();
            //必须同时满足待生效账户是0、待生效时间点已经设置且待生效时间点已经过去
            if (newDefaultAdmin != address(0) || !_isScheduleSet(schedule) || !_hasSchedulePassed(schedule)) {
                revert AccessControlEnforcedDefaultAdminDelay(schedule);
            }
            //在上述条件都满足后,初始化待生效时间点
            delete _pendingDefaultAdminSchedule;
        }
        //调用父类renounceRole,这里要注意:父类的renounceRole实现了两个功能,一个是进行对当前方法调用地址的控制,保证只能由解除授权的账户自身发起renounceRole操作;另一个是调用了_revokeRole,因为当前合约重写了_revokeRole,所以要看当前_revokeRole的实现逻辑:在父类的_revokeRole的基础上,增加了对当前默认管理员账户初始化为0的操作
        super.renounceRole(role, account);
    }

上面是“默认管理员”身份组账户管理相关操作,下面看下“时间延迟参数”相关操作:

修改“时间延迟参数”changeDefaultAdminDelay的逻辑如下:

    //修改“时间延迟参数”,即状态变量_currentDelay和_pendingDelay
    function changeDefaultAdminDelay(uint48 newDelay) public virtual onlyRole(DEFAULT_ADMIN_ROLE) {
        _changeDefaultAdminDelay(newDelay);
    }

    //内部方法
    function _changeDefaultAdminDelay(uint48 newDelay) internal virtual {
        //设置“时间延迟参数”的待生效时间点,在当前时间基础上加上等待时间,等待时间通过_delayChangeWait获得
        uint48 newSchedule = SafeCast.toUint48(block.timestamp) + _delayChangeWait(newDelay);
        _setPendingDelay(newDelay, newSchedule);
        emit DefaultAdminDelayChangeScheduled(newDelay, newSchedule);
    }

    //计算“时间延迟参数”变更时需要延迟的时间
    function _delayChangeWait(uint48 newDelay) internal view virtual returns (uint48) {
        //获取当前生效的延迟时间
        uint48 currentDelay = defaultAdminDelay();

        // When increasing the delay, we schedule the delay change to occur after a period of "new delay" has passed, up
        // to a maximum given by defaultAdminDelayIncreaseWait, by default 5 days. For example, if increasing from 1 day
        // to 3 days, the new delay will come into effect after 3 days. If increasing from 1 day to 10 days, the new
        // delay will come into effect after 5 days. The 5 day wait period is intended to be able to fix an error like
        // using milliseconds instead of seconds.
        //
        // When decreasing the delay, we wait the difference between "current delay" and "new delay". This guarantees
        // that an admin transfer cannot be made faster than "current delay" at the time the delay change is scheduled.
        // For example, if decreasing from 10 days to 3 days, the new delay will come into effect after 7 days.

        //如果新设置的“时间延迟参数”大于当前生效中的“时间延迟参数”,那么返回新设置“时间延迟参数”和系统预设的延迟参数增大时的默认最大值两者中的较小值;
        //如果新设置的“时间延迟参数”小于等于当前生效中的“时间延迟参数”,则需要等待新参数和旧参数的时间差值再生效“时间延迟参数”,这保证延迟参数的生效不会影响已经在进行中的账户转移等待时间,比如旧的延时时间为10天,发起账户转移后,新账户要等待10天才能发起接受,这是基于旧的“时间延迟参数”达成的共识。举个例子来看为何能够保证如何保证上述逻辑:在发起账户转移后,立即修改延迟参数为1天,因为要等待9天这个新参数才能生效,所以新账户等待时间在9天内还是保持为旧的“时间延迟参数”,第十天虽然已经变更新为参数,但新参数仍然要求等待一天,所以总的等待时间仍不短与10天,如果在账户转移的第二天修改延迟参数为1天,新账户等待时间子账户转移发起的10天内还是保持为旧的“时间延迟参数”,也不影响总延时时间为10天
        return
            newDelay > currentDelay
                ? uint48(Math.min(newDelay, defaultAdminDelayIncreaseWait())) // no need to safecast, both inputs are uint48
                : currentDelay - newDelay;
    }

    //设置待生效的“时间延迟参数”和生效时间点,
    function _setPendingDelay(uint48 newDelay, uint48 newSchedule) private {
        //先判断旧的生效时间是否有配置,如果有配置,后续要判断是否要先生效旧的待生效“时间延迟参数”
        uint48 oldSchedule = _pendingDelaySchedule;

        if (_isScheduleSet(oldSchedule)) {
            if (_hasSchedulePassed(oldSchedule)) {
                // Materialize a virtual delay
                //如果当前时间晚于生效时间点,则先生效旧的待生效“时间延迟参数”
                _currentDelay = _pendingDelay;
            } else {
                // Emit for implicit cancellations when another delay was scheduled.
                emit DefaultAdminDelayChangeCanceled();
            }
        }

        _pendingDelay = newDelay;
        _pendingDelaySchedule = newSchedule;
    }

取消“时间延迟参数”rollbackDefaultAdminDelay的逻辑为:

    //只有有效管理员账户才能发起,即刻生效
    function rollbackDefaultAdminDelay() public virtual onlyRole(DEFAULT_ADMIN_ROLE) {
        _rollbackDefaultAdminDelay();
    }

    //修改待生效时间延迟参数和时间延迟参数生效时间点为0
    function _rollbackDefaultAdminDelay() internal virtual {
        _setPendingDelay(0, 0);
    }

以太坊是一个平台,它上面提供各种模块让用户来搭建应用,如果将搭建应用比作造房子,那么以太坊就提供了墙面、屋顶、地板等模块,用户只需像搭积木一样把房子搭起来,因此在以太坊上建立应用的成本和速度都大大改善。具体来说,以太坊通过一套图灵完备的脚本语言(Ethereum Virtual Machinecode,简称EVM语言)来建立应用,它类似于汇编语言。我们知道,直接用汇编语言编程是非常痛苦的,但以太坊里的编程并不需要直接使用EVM语言,而是类似C语言、python、Lisp等高级语言,再通过编译器转成Evm语言。上面所说的平台之上的应用,其实就是合约,这是以太坊的核心。合约是一个活在以太坊系统里的自动代理人,他有一个自己的以太币地址,当用户向合约的地址里发送一笔交易后,该合约就被激活,然后根据交易中的额外信息,合约会运行自身的代码,最后返回一个结果,这个结果可能是从合约的地址发出另外一笔交易。需要指出的是,以太坊中的交易,不单只是发送以太币而已,它还可以嵌入相当多的额外信息。如果一笔交易是发送给合约的,那么这些信息就非常重要,因为合约将根据这些信息来完成自身的业务逻辑。合约所能提供的业务,几乎是无穷无尽的,它的边界就是你的想象力,因为图灵完备的语言提供了完整的自由度,让用户搭建各种应用。白皮书举了几个例子,如储蓄账户、用户自定义的子货币等。 2013年年末,以太坊创始人Vitalik Buterin发布了以太坊初版白皮书,启动了项目。2014年7月24日起,以太坊进行了为期42天的以太币预售。2016年初,以太坊的技术得到市场认可,价格开始暴涨,吸引了大量开发者以外的人进入以太坊的世界。中国三大比特币交易所之二的火币网及OKCoin币行都于2017年5月31日正式上线以太坊。 [1] 自从进入2016年以来,那些密切关注数字货币产业的人都急切地观察着第二代加密货币平台以太坊的发展动向。作为一种比较新的利用比特币技术的开发项目,以太坊致力于实施全球去中心化且无所有权的的数字技术计算机来执行点对点合约。简单来说就是,以太坊是一个你无法关闭的世界计算机。加密架构与图灵完整性的创新型结合可以促进大量的新产业的出现。反过来,传统行业的创新压力越来越大,甚至面临淘汰的风险。比特币网络事实上是一套分布式的数据,而以太坊则更进一步,她可以看作是一台分布式的计算机:区块链是计算机的ROM,合约是程序,而以太坊的矿工们则负责计算,担任CPU的角色。这台计算机不是、也不可能是免费使用的,不然任何人都可以往里面存储各种垃圾信息和执行各种鸡毛蒜皮的计算,使用它至少需要支付计算费和存储费,当然还有其它一些费用。最为知名的是2017年初以摩根大通、芝加哥交易所集团、纽约梅隆银行、汤森路透、微软、英特尔、埃森哲等20多家全球top金融机构和科技公司成立的企业以太坊联盟。而以太坊催生的加密货币以太币近期又成了继比特币之后受追捧的资产。  智能合约的潜在应用很多。彭博社商业周刊称它是“所有人共享但无法篡改的软件”。更高级的软件有可能用以太坊创建网络商店。  以太坊是一个平台,它上面提供各种模块让用户来搭建应用,如果将搭建应用比作造房子,那么以太坊就提供了墙面、屋顶、地板等模块,用户只需像搭积木一样把房子搭起来,因此在以太坊上建立应用的成本和速度都大大改善。具体来说,以太坊通过一套图灵完备的脚本语言(Ethereum Virtual Machinecode,简称EVM语言)来建立应用,它类似于汇编语言。我们知道,直接用汇编语言编程是非常痛苦的,但以太坊里的编程并不需要直接使用EVM语言,而是类似C语言、python、Lisp等高级语言,再通过编译器转成Evm语言。上面所说的平台之上的应用,其实就是合约,这是以太坊的核心。合约是一个活在以太坊系统里的自动代理人,他有一个自己的以太币地址,当用户向合约的地址里发送一笔交易后,该合约就被激活,然后根据交易中的额外信息,合约会运行自身的代码,最后返回一个结果,这个结果可能是从合约的地址发出另外一笔交易。需要指出的是,以太坊中的交易,不单只是发送以太币而已,它还可以嵌入相当多的额外信息。如果一笔交易是发送给合约的,那么这些信息就非常重要,因为合约将根据这些信息来完成自身的业务逻辑。合约所能提供的业务,几乎是无穷无尽的,它的边界就是你的想象力,因为图灵完备的语言提供了完整的自由度,让用户搭建各种应用。白皮书举了几个例子,如储蓄账户、用户自定义的子货币等。 2013年年末,以太坊创始人Vitalik Buterin发布了以太坊初版白皮书,启动了项目。2014年7月24日起,以太坊进行了为期42天的以太币预售。2016年初,以太坊的技术得到市场认可,价格开始暴涨,吸引了大量开发者以外的人进入以太坊的世界。中国三大比特币交易所之二的火币网及OKCoin币行都于2017年5月31日正式上线以太坊。 [1] 
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值