Conflux 内置合约功能介绍

Conflux引入了一些内置的内部合约,以便更好地进行系统维护和链上治理。而本文档将介绍如何使用这些内置合约。

id title custom_edit_url keywords
internal_contract Internal Contract https://github.com/Conflux-Chain/conflux-rust/edit/master/internal_contract/README.md conflux ,contract

(重要:在Tethys主网中该接口会被变更。该文档已与最新版本同步。)

Conflux引入了一些内嵌的合约,以便更好的进行系统维护及链上治理。目前Conflux含有三种内置合约:AdminControl 合约,SponsorWhitelistControl 合约以及 Staking 合约。

这些合约提供的solidity功能性api在此处定义。

这些功能只能通过调用 CALLSTATICCALL 操作进行调用。使用操作 CALLCODEDELEGATECALL 会触发错误。

这三个内置合约的地址如下所示:

  • AdminControl: 0x0888000000000000000000000000000000000000
  • SponsorWhitelistControl: 0x0888000000000000000000000000000000000001
  • Staking: 0x0888000000000000000000000000000000000002

本文中的所有样例代码会使用js-conflux-sdk进行开发。

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_gas :是提供燃料补贴的账户;
  • sponsor_for_collateral :是提供存储抵押补贴的账户;
  • sponsor_balance_for_gas :可用于燃料补贴的余额;
  • sponsor_balance_for_collateral : 可用于提供存储抵押补贴的余额;
  • sponsor_limit_for_gas_fee :是向每笔交易赞助燃料费用的上界;
  • whitelist :有资格获得补贴的普通用户地址列表,全零地址则代表所有用户地址。只有合约自身和管理员有权限改动该列表。

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

  • *对于燃料费用: 如果一笔交易使用非空的 sponsor_for_gas 调用智能合约且交易发送者处于合约的 whitelist 列表内,且交易指定的燃料费用在 sponsor_limit_for_gas_fee 范围内,交易的燃料消耗将从合约的 sponsor_balance_for_gas 中支付(如果足够的话),而不是由交易发送者的账户余额支付,如果 sponsor_balance_for_gas 无法承担燃料消耗,则交易失败。否则,交易发送者应支付燃料费用。
  • 对于存储抵押物: 如果一笔交易使用非空的 sponsor_balance_for_collateral 调用智能合约且交易发送者处于合约的 whitelist 列表内,在执行交易的过程中存储抵押物将从智能合约的 sponsor_balance_for_collateral 中扣除,并将这些修改后的存储条目所有者相应设置为合约地址。 否则,交易发送方应在执行过程中支付存储抵押物。

当一个合约被创建的时候,它的 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内嵌质押合约提取部分代币。这会触发利息结算。抵押资金和利息将会及时的转入用户余额中。所有提款的顺序将按照先到先服务的方式进行处理。

利率

目前的年化利率为4.08%。 复利是以区块的颗粒度来实现的。

如果在执行区块 B 中的交易时,尝试提取 价值 v 并且在区块 B' 中抵押的资金,其利息计算公式如下:

interest issued = v * (1 + 4% / 63072000)^T - v

其中 T = BlockNo(B)−BlockNo(B') 是以区块数目衡量的质押时长,而 63072000 是在区块生成时间为 0.5 秒前提下 365 天生成区块数目的期望值。

锁定与投票权

通过锁定质押余额,用户可获取投票权以进一步进行链上治理。通过调用 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();