内置合约

AdminControl

总览

AdminControl 合约是一款用于合约开发的调试工具。 在交易过程中创建合约时,当前交易的发送者会自动成为合约的管理者(admin)。

管理者 admin 的地址可以通过调用接口 setAdmin(address contractAddr, address newAdmin) 将管理权益转交给其他的普通用户地址零地址。而一个合约不可成为管理者。

合约的管理者具备多个管理权限。管理者可以调用 destroy(address contractAddr) 接口以销毁合约,该操作就像通过合约调用 suicide() 函数那样。而SponsorWhitelist内部合约提供了一些管理员专用的函数。这些函数可以更新赞助者机制中的白名单。我们将在随后进行介绍。

注意:对于所有和管理者权限相关的接口,不论调用成功与否都不会在执行时触发任何错误或异常。 例如,如果一个非管理者地址尝试将管理者地址转移给其自己,该交易会成功但不会造成任何改动。

如果合约拥有非零的管理者地址,ConfluxScan会将合约标记为调试模式。因此请记住,如果你认为你的合约已经准备好进入实际生产环境,你应当将管理者地址设置为零地址。

AdminControl 合约同时也提供了一个可以被任何人调用的查询接口 getAdmin(address contractAddr)

需要注意的细节:

  1. 默认管理者(交易发送者)是在合约开始创建时设置的。因此,如果发送者 A 创建合约 B 并在合约构建时设置管理者为 C ,在合约部署后合约的管理者为 C
  2. 然而,如果发送者 A 调用合约 B ,随后合约 B 创建合约 C 并在合约创建时将管理者设置为 D ,则该设置会失败,原因是: C 合约的管理者是 A ,但创建合约 C 的发起者是 B
  3. Conflux会引入一种特别的规则。在案例2中,如果 D 是零地址,则管理者设置成功。这意味着合约创建时可以显示地声明其不需要管理者。

样例

考虑到您可以已经部署了一个地址为 contract_addr 的合约。管理者可以通过调用AdminControl.setAdmin(contract_addr, new_admin) 以变更管理员以及通过调用AdminControl.destroy(contract_addr) 来销毁合约。

const PRIVATE_KEY = '0xxxxxxx';
const cfx = new Conflux({
  url: 'http://test.confluxrpc.org',
  logger: console,
});
const account = cfx.wallet.addPrivateKey(PRIVATE_KEY); // create account instance

const admin_contract = cfx.InternalContract('AdminControl')
// to change administrator
admin_contract.setAdmin(contract_addr, new_admin).sendTransaction({
  from: account,
}).confirmed();

// to kill the contract
admin_contract.destroy(contract_addr).sendTransaction({
  from: account,
}).confirmed();

SponsorWhitelistControl

总览

Conflux实现了一种赞助机制来补贴用户对智能合约的使用。 因此,只要对合约调用的交易被赞助(通常由Dapps的运营商赞助),使用余额为零的新帐户也能够调用智能合约。 通过引入内置的SponsorControl合约能够记录和管理智能合约的赞助信息。

在进行子调用(Message Call)时,Conflux不会再次检查赞助情况。例如,如果一个普通用户地址 A 调用合约 B ,然后合约 B 调用合约 C, Conflux仅仅会检查 A 是否被合约 B 赞助。如果 A 被赞助,B 会在交易执行过程中负担全部的燃料费用和/或存储抵押费用,包含 B 调用 C 的费用。换句话说,只有交易的发送者才能被赞助,B 不可能被赞助。

SponsorControl合约为每一个用户建立的合约保留了如下的信息:

有两种资源能够被赞助:燃料费用和存储抵押物。

当一个合约被创建的时候,它的 sponsor_for_gassponsor_for_collateral 会被置为零地址,相应的燃料补贴余额也是零。 提供燃料补贴的账户和存储押金补贴的账户都可以通过与 SponsorControl 合约交互完成。合约当前的赞助账户可以直接追加补贴余额,也可以在满足一定条件下提高 sponsor_limit_for_gas_fee。其他普通用户账户如果提供高于当前余额的资金,可以将原先的赞助者取而代之。更具体的细节如下。

赞助者替换

为了替换合约的 sponsor_for_gas ,新的赞助者需要调用函数 setSponsorForGas(address contractAddr, uint upperBound) 并向内置合约转移一笔资金。只有在满足下述条件时才能完成燃料费用赞助者的替换:

  1. 转移的资金应当比合约当前的 sponsor_balance_for_gas 高。
  2. sponsor_limit_for_gas_fee 的新值(被指定为 upperBound 参数)应当不小于原赞助者的限制,除非原本的 sponsor_balance_for_gas 无法负担原赞助者的限制。
  3. 转移的资金应为新限额的1000倍以上,以便足以补贴至少 1000 次调用合约的交易。

如果上述条件满足,剩余的 sponsor_balance_for_gas 会返还给原赞助人 sponsor_for_gas ,随后转给内置合约的资金被注入 sponsor_balance_for_gassponsor_for_gas 以及 sponsor_limit_for_gas_fee 会根据新赞助人指定的值进行更新。如果上述条件没有满足,会触发异常。

sponsor_for_collateral 的替换与替换燃料赞助者的方式类似,处理没有燃料限额的逻辑。方法是 setSponsorForCollateral(address contractAddr) 。新的赞助者需要向合约转移一笔比目前余额更多的资金。随后当前 sponsor_for_collateral 赞助的金额会被全部退还,即 sponsor_balance_for_collateral 和合约当前所有存储抵押金之和,两个与存储押金赞助相关的字段将按照新赞助者的要求进行相应的变更。

一个合约账户也被允许成为赞助者。

提高赞助额度

赞助者可以在无需更换赞助者的情况下提供额外的赞助资金。在该情况下,赞助人需要调用函数 setSponsorForGas(address contractAddr, uint upperBound)setSponsorForCollateral(address contractAddr) 并满足赞助者替换的三条要求中的条件 2,3. 如果满足相关需求,交易的赞助资金会被加入赞助余额中且 sponsor_limit_for_gas_fee 也会相应地被更新。

白名单列表维护

只有合约本身或合约管理者可以更新合约白名单列表。赞助者则无权变更白名单列表。

合约可以通过调用 addPrivilege(address[] memory) 将任何地址加入到白名单列表中。这意味着如果 sponsor_for_gas 被设置,合约会为白名单列表中的账户支付存储押金。全零地址是一个特殊的地址 0x0000000000000000000000000000000000000000 。如果该地址被加入白名单列表,则所有调用该合约的交易都会被赞助。合约还可以调用方法 removePrivilege(address[] memory) 将部分正常账户从白名单列表中移除。移除一个不存在的地址不会导致错误或异常。

需要注意的细节:

  1. 一个合约地址也可被加入到白名单列表中,但该操作无任何意义,因为只有交易的发送者可以被赞助。

合约的管理者可以使用 addPrivilegeByAdmin(address contractAddr, address[] memory addresses)removePrivilegeByAdmin(address contractAddr, address[] memory addresses) 以维护白名单列表。

样例

假定你有一个如下所示的简单合约。

pragma solidity >=0.4.15;

import "https://github.com/Conflux-Chain/conflux-rust/blob/master/internal_contract/contracts/SponsorWhitelistControl.sol";

contract CommissionPrivilegeTest {
    mapping(uint => uint) public ss;

    function add(address account) public payable {
        SponsorWhitelistControl cpc = SponsorWhitelistControl(0x0888000000000000000000000000000000000001);
        address[] memory a = new address[](1);
        a[0] = account;
        cpc.addPrivilege(a);
    }

    function remove(address account) public payable {
        SponsorWhitelistControl cpc = SponsorWhitelistControl(0x0888000000000000000000000000000000000001);
        address[] memory a = new address[](1);
        a[0] = account;
        cpc.removePrivilege(a);
    }

    function foo() public payable {
    }

    function par_add(uint start, uint end) public payable {
        for (uint i = start; i < end; i++) {
            ss[i] = 1;
        }
    }
}

部署合约且地址为 contract_addr 如果有些人希望赞助燃料费用,他/她可以发送如下的交易:

const PRIVATE_KEY = '0xxxxxxx';
const cfx = new Conflux({
  url: 'http://test.confluxrpc.org',
  logger: console,
});
const account = cfx.wallet.addPrivateKey(PRIVATE_KEY); // create account instance

const sponsor_contract = cfx.InternalContract('SponsorWhitelistControl');
sponsor_contract.setSponsorForGas(contract_addr, your_upper_bound).sendTransaction({
  from: account,
  value: your_sponsor_value
}).confirmed();

如果需要赞助存储抵押物,可以简单的将 setSponsorForGas(contract_addr, your_upper_bound) 替代为 setSponsorForCollateral(contract_addr) 即可。

之后你可以使用 addPrivilegeremovePrivilege 为你的合约维护 whitelist 。特殊的全零地址 0x0000000000000000000000000000000000000000 则表示 所有人都处于 whitelist 中。需要谨慎使用。

you_contract.add(white_list_addr).sendTransaction({
  from: account,
})

you_contract.remove(white_list_addr).sendTransaction({
  from: account,
})

随后在 whitelist 中的账户在调用 you_contract.foo()you_contract.par_add(1, 10) 时,不会支付任何费用。

Staking

总览

Conflux引入权益质押机制的原因有两个:一、权益机制提供了一种对占用存储空间更好的收费方式(相比于“一次付费,永久占用”)。二、该机制还有助于定义分散治理中的投票权。

在顶层,Conflux实现了一个内置的Staking合约,以记录所有账户的权益信息。通过向该合约发送交易,用户(包括外部用户和智能合约)可以存入/提取资金,也被称为合约内的权益。

用户可以通过调用 deposit(uint amount) 来存入用于抵押的金额,随后 amount 数量的资金将从其 balance 移至 stakingBalance. 需要注意的是该函数不是 payable 的,用户只需要指定抵押的金额而无需将资金转入到内部合约中。

用户还可以通过调用 withdraw(uint amount) 来提取余额。调用者可以调用该函数从Conflux质押合约提取部分代币。抵押资金将会及时的转入用户余额中。所有提款的顺序将按照先到先服务的方式进行处理。

锁定与投票权

通过锁定质押余额,用户可获取投票权以进一步进行链上治理。通过调用 voteLock(uint amount, uint unlock_block_number) 函数,一个帐户可以做出如下承诺:“我的 stakingBalance 在未来 unlock_block_number 中将始终具有至少 amount 的资金”。单个账户可以做出多个承诺,比如说“今年我将至少持有10CFX,并且在明年至少持有5CFX。” **一旦做出承诺,无法取消!**但是该账户可通过锁定更多的金额覆盖原有的承诺。每当账户尝试提取 stakingBalance 时,内部合约会检查剩余余额是否与锁定承诺吻合。

在此处我们将通过几个样例介绍锁定余额的逻辑细节。假设Conflux在今年的剩余时间将产生 x 个区块,在明年的产生 y 个区块。由于Conflux网络每秒能生成两个区块,因此 y 近似等于 2 * 60 * 60 * 24 * 365 。而 x 取决于您何时阅读到该文章。

  1. 假设一个账户的 stakingBalance 中有10CFX,如果其调用 voteLock(100 * 10^18, x) ,说明账户尝试锁定 100CFX. 但由于其缺少足够的 stakingBalance,交易执行失败。
  2. 然而,如果该账户调用 voteLock(8 * 10^18, x) ,则交易会成功。
  3. 随后,如果该账户调用 voteLock(6 * 10^18, x+y),交易同样会成功。这意味着交易执行后 2CFX 在今年结束时解锁,而另外 6CFX 会被锁定直到明年结束。
  4. 如果账户再调用 voteLock(0, x),没有任何事情会发生。在交易执行过程中交易不会触发错误。内置合约会将此调用视为无意义的承诺:该帐户在步骤 2、3 做出的旧承诺有效的前提下,再次承诺在今年结束前至少锁定 0 CFX.
  5. 如果该账户调用 voteLock(9 * 10^18, x+y),则两个较老的承诺将会因为“锁定 9CFX 直到明年结束是一个更强的承诺”被覆盖。

锁定对利息无任何影响。当账户成功取出抵押余额时,利息会将照常计算。

在任何时间,每一个锁定的金额将根据其解锁时间被分配0到1的表决权。锁定期超过1年的部分将拥有全额的投票权利。查看 Conflux Protocol Specification 章节 8.3.2 获取更多信息。

样例

const PRIVATE_KEY = '0xxxxxxx';
const cfx = new Conflux({
  url: 'http://test.confluxrpc.org',
  logger: console,
});
const account = cfx.wallet.addPrivateKey(PRIVATE_KEY); // create account instance

const staking_contract = cfx.InternalContract('Staking');
// deposit some amount of tokens
staking_contract.deposit(your_number_of_tokens).sendTransaction({
  from: account,
}).confirmed();

// withdraw some amount of tokens
staking_contract.withdraw(your_number_of_tokens).sendTransaction({
  from: account,
}).confirmed();

// lock some tokens until some block number
staking_contract.voteLock(your_number_of_tokens, your_unlock_block_number).sendTransaction({
  from: account,
}).confirmed();

ConfluxContext

ConfluxContext 内置合约可用于在 Solidity 中获取当前网络信息,目前包含:epochNumber, posHeight, finalizedEpochNumber

pragma solidity >=0.4.15;

contract ConfluxContext {
    /*** Query Functions ***/
    /**
     * @dev get the current epoch number
     * @return the current epoch number
     */
    function epochNumber() public view returns (uint256) {}
    /**
     * @dev get the height of the referred PoS block in the last epoch
`    * @return the current PoS block height
     */
    function posHeight() public view returns (uint256) {}
    /**
     * @dev get the epoch number of the finalized pivot block.
     * @return the finalized epoch number
     */
    function finalizedEpochNumber() public view returns (uint256) {}
}

PoSRegister

简介

PoSRegister合约主要为PoW用户在PoS上注册提供了相应的通道。通过质押与锁仓一定数量的代币后调用合约中的方法从而使得PoW账户与PoS账户进行绑定。绑定后在锁仓代币数量足够的前提下实现增加与减少投票权重的功能。该合约主要实现的功能有:注册(绑定)PoS全节点;增加,减少投票权重;查询PoS节点当前投票权重;PoS地址与PoW地址间的互相转换

前提概念

  1. conflux有两条链,一条PoW链,主要处理事务;一条PoS链,对PoW的区块进行验证
  2. 质押并锁仓的CFX的个数最少为1000
  3. 1 PoS 投票权重 = 1000 CFX
  4. 增加,减少投票权重的单位是 vote power
  5. 锁定到提现有一个时间限制,在staking合约中调用voteLock()时需要指定解锁时间

前置条件

方法阐述

register

概念

PoW账户与PoS账户进行绑定的通道, 当PoW账户的锁仓金额达到1000CFX时,可以调用该方法来为PoS节点与PoW建立连接,目前1000CFX可以换取1 vote power

前置条件

输入参数

输出参数

increaseStake

概念

在PoW账户与PoS账户建立连接, 若用户有提高自己投票权重的需要, 可以调用该方法将锁仓后的CFX置换为投票权重

前置条件

输入参数

输出参数

retire

概念

在PoW账户与PoS账户建立连接, 若PoW账户有解除和PoS账户相绑定的需要,可调用该方法通过部分或全部解除锁仓来实现绑定的解除功能

前置条件

输入参数

输出参数

getVotes

概念

通过调用该方法来获取质押的token的votes和解除锁仓后的token的votes

前置条件

输入参数

输出参数

identifierToAddress

概念

根据PoS账户地址获取对应的PoW地址

前置条件

输入参数

输出参数

addressToIdentifier

概念

根据PoW账户地址获取对应的PoS地址

前置条件

输入参数

输出参数

代码

// SPDX-License-Identifier: MIT
pragma solidity >=0.5.0;

interface PoSRegister {
    /**
     * @dev Register PoS account
     * @param indentifier - PoS account address to register
     * @param votePower - votes count
     * @param blsPubKey - BLS public key
     * @param vrfPubKey - VRF public key
     * @param blsPubKeyProof - BLS public key's proof of legality, used to against some attack, generated by conflux-rust fullnode
     */
    function register(
        bytes32 indentifier,
        uint64 votePower,
        bytes calldata blsPubKey,
        bytes calldata vrfPubKey,
        bytes[2] calldata blsPubKeyProof
    ) external;

    /**
     * @dev Increase specified number votes for msg.sender
     * @param votePower - count of votes to increase
     */
    function increaseStake(uint64 votePower) external;

    /**
     * @dev Retire specified number votes for msg.sender
     * @param votePower - count of votes to retire
     */
    function retire(uint64 votePower) external;

    /**
     * @dev Query PoS account's lock info. Include "totalStakedVotes" and "totalUnlockedVotes"
     * @param identifier - PoS address
     */
    function getVotes(bytes32 identifier) external view returns (uint256, uint256);

    /**
     * @dev Query the PoW address binding with specified PoS address
     * @param identifier - PoS address
     */
    function identifierToAddress(bytes32 identifier) external view returns (address);

    /**
     * @dev Query the PoS address binding with specified PoW address
     * @param addr - PoW address
     */
    function addressToIdentifier(address addr) external view returns (bytes32);

    /**
     * @dev Emitted when register method executed successfully
     */
    event Register(bytes32 indexed identifier, bytes blsPubKey, bytes vrfPubKey);

    /**
     * @dev Emitted when increaseStake method executed successfully
     */
    event IncreaseStake(bytes32 indexed identifier, uint64 votePower);

    /**
     * @dev Emitted when retire method executed successfully
     */
    event Retire(bytes32 indexed identifier, uint64 votePower);
}

CrossSpaceCall

简介

CrossSpaceCall 合约为conflux中coreSpace与ESpace沟通的桥梁。该合约能够实现在coreSpace中的账户在ESpace中互相转账,部署合约,调用合约及查询与CoreSpace中账户相对应的ESpace中账户的的nonce与balance

前提概念

  1. 在conlux中有CoreSpace与ESpace两个域,对应着conflux与以太坊两条链;
  2. CoreSpace中的账户与ESpace中的账户有一一对应关系;

前置条件

方法阐述

createEVM

概念

通过调用该方法,在espace中创建合约(合约地址可通过事件获得,也可通过解析call方法调用合约返回的数据来获得相应的合约地址)

注: 由于call方法不改变状态,且是将当前账户的nonce+1来作为生成合约地址的数据,因此需要先调用call方法获得合约地址,再去调用createEVM来获得相应的合约地址

输入参数

输出参数

transferEVM

概念

向PoW账户映射的Espace账户中转帐

前置条件

输入参数

输出参数

callEVM

概念

通过该方法调用ESpace下的合约(合约地址可通过事件获得,也可通过解析call方法调用合约返回的数据来获得相应的合约地址)

输入参数

输出参数

staticCallEVM

概念

通过该方法静态调用ESpace下的合约

输入参数

输出参数

withdrawFromMapped

概念

从ESpace账户中收回相应的代币至其映射的CoreSpace账户

输入参数

输出参数

mappedBalance

概念

查询CoreSpace账户映射的CoreSpace账户的余额

输入参数

输出参数

mappedNonce

概念

查询CoreSpace账户映射的CoreSpace账户的nonce

输入参数

输出参数

代码

// SPDX-License-Identifier: MIT
pragma solidity >=0.5.0;

interface CrossSpaceCall {

    event Call(bytes20 indexed sender, bytes20 indexed receiver, uint256 value, uint256 nonce, bytes data);

    event Create(bytes20 indexed sender, bytes20 indexed contract_address, uint256 value, uint256 nonce, bytes init);

    event Withdraw(bytes20 indexed sender, address indexed receiver, uint256 value, uint256 nonce);

    event Outcome(bool success);

    function createEVM(bytes calldata init) external payable returns (bytes20);
    
    function transferEVM(bytes20 to) external payable returns (bytes memory output);

    function callEVM(bytes20 to, bytes calldata data) external payable returns (bytes memory output);

    function staticCallEVM(bytes20 to, bytes calldata data) external view returns (bytes memory output);

    function withdrawFromMapped(uint256 value) external;

    function mappedBalance(address addr) external view returns (uint256);

    function mappedNonce(address addr) external view returns (uint256);
}

ParamsControl

一般而言,区块链需要通过硬分叉(hardfork)才能更改区块奖励等全局参数。Conflux引入ParamsControl内置合约,使得DAO能够在链上投票来调整Conflux的链参数,如PoW的每个区块的出块奖励,或PoS链上的利率。ParamsControl内置合约由CIP-94引入,CIP-94 介绍页面进行了一定的介绍。

合约接口

// SPDX-License-Identifier: MIT

pragma solidity >=0.8.0;

interface ParamsControl {
    struct Vote {
        uint16 topic_index;
        uint256[3] votes;
    }

    /*** Query Functions ***/
    /**
     * @dev cast vote for parameters
     * @param vote_round The round to vote for
     * @param vote_data The list of votes to cast
     */
    function castVote(uint64 vote_round, Vote[] calldata vote_data) external;

    /**
     * @dev read the vote data of an account
     * @param addr The address of the account to read
     */
    function readVote(address addr) external view returns (Vote[] memory);

    /**
     * @dev Current vote round
     */
    function currentRound() external view returns (uint64);

    /**
     * @dev read the total votes of given round
     * @param vote_round The vote number
     */
    function totalVotes(uint64 vote_round) external view returns (Vote[] memory);

    /**
     * @dev read the PoS stake for the round.
     */
    function posStakeForVotes(uint64) external view returns (uint256);

    event CastVote(uint64 indexed vote_round, address indexed addr, uint16 indexed topic_index, uint256[3] votes);
    event RevokeVote(uint64 indexed vote_round, address indexed addr, uint16 indexed topic_index, uint256[3] votes);
}