内置合约
- AdminControl
- SponsorWhitelistControl
- Staking
- ConfluxContext
- PoSRegister
- CrossSpaceCall
- ParamsControl
AdminControl
总览
AdminControl
合约是一款用于合约开发的调试工具。 在交易过程中创建合约时,当前交易的发送者会自动成为合约的管理者(admin)。
管理者 admin
的地址可以通过调用接口 setAdmin(address contractAddr, address newAdmin)
将管理权益转交给其他的普通用户地址或零地址。而一个合约不可成为管理者。
合约的管理者具备多个管理权限。管理者可以调用 destroy(address contractAddr)
接口以销毁合约,该操作就像通过合约调用 suicide()
函数那样。而SponsorWhitelist内部合约提供了一些管理员专用的函数。这些函数可以更新赞助者机制中的白名单。我们将在随后进行介绍。
注意:对于所有和管理者权限相关的接口,不论调用成功与否都不会在执行时触发任何错误或异常。 例如,如果一个非管理者地址尝试将管理者地址转移给其自己,该交易会成功但不会造成任何改动。
如果合约拥有非零的管理者地址,ConfluxScan会将合约标记为调试模式。因此请记住,如果你认为你的合约已经准备好进入实际生产环境,你应当将管理者地址设置为零地址。
AdminControl
合约同时也提供了一个可以被任何人调用的查询接口 getAdmin(address contractAddr)
。
需要注意的细节:
- 默认管理者(交易发送者)是在合约开始创建时设置的。因此,如果发送者
A
创建合约B
并在合约构建时设置管理者为C
,在合约部署后合约的管理者为C
。 - 然而,如果发送者
A
调用合约B
,随后合约B
创建合约C
并在合约创建时将管理者设置为D
,则该设置会失败,原因是:C
合约的管理者是A
,但创建合约C
的发起者是B
。 - 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_gas
和 sponsor_for_collateral
会被置为零地址,相应的燃料补贴余额也是零。 提供燃料补贴的账户和存储押金补贴的账户都可以通过与 SponsorControl 合约交互完成。合约当前的赞助账户可以直接追加补贴余额,也可以在满足一定条件下提高 sponsor_limit_for_gas_fee
。其他普通用户账户如果提供高于当前余额的资金,可以将原先的赞助者取而代之。更具体的细节如下。
赞助者替换
为了替换合约的 sponsor_for_gas
,新的赞助者需要调用函数 setSponsorForGas(address contractAddr, uint upperBound)
并向内置合约转移一笔资金。只有在满足下述条件时才能完成燃料费用赞助者的替换:
- 转移的资金应当比合约当前的
sponsor_balance_for_gas
高。 -
sponsor_limit_for_gas_fee
的新值(被指定为upperBound
参数)应当不小于原赞助者的限制,除非原本的sponsor_balance_for_gas
无法负担原赞助者的限制。 - 转移的资金应为新限额的1000倍以上,以便足以补贴至少
1000
次调用合约的交易。
如果上述条件满足,剩余的 sponsor_balance_for_gas
会返还给原赞助人 sponsor_for_gas
,随后转给内置合约的资金被注入 sponsor_balance_for_gas
。 sponsor_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)
将部分正常账户从白名单列表中移除。移除一个不存在的地址不会导致错误或异常。
需要注意的细节:
- 一个合约地址也可被加入到白名单列表中,但该操作无任何意义,因为只有交易的发送者可以被赞助。
合约的管理者可以使用 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)
即可。
之后你可以使用 addPrivilege
和 removePrivilege
为你的合约维护 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
取决于您何时阅读到该文章。
- 假设一个账户的
stakingBalance
中有10CFX,如果其调用voteLock(100 * 10^18, x)
,说明账户尝试锁定 100CFX. 但由于其缺少足够的stakingBalance
,交易执行失败。 - 然而,如果该账户调用
voteLock(8 * 10^18, x)
,则交易会成功。 - 随后,如果该账户调用
voteLock(6 * 10^18, x+y)
,交易同样会成功。这意味着交易执行后 2CFX 在今年结束时解锁,而另外 6CFX 会被锁定直到明年结束。 - 如果账户再调用
voteLock(0, x)
,没有任何事情会发生。在交易执行过程中交易不会触发错误。内置合约会将此调用视为无意义的承诺:该帐户在步骤 2、3 做出的旧承诺有效的前提下,再次承诺在今年结束前至少锁定 0 CFX. - 如果该账户调用
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地址间的互相转换
前提概念
- conflux有两条链,一条PoW链,主要处理事务;一条PoS链,对PoW的区块进行验证
- 质押并锁仓的CFX的个数最少为1000
- 1 PoS 投票权重 = 1000 CFX
- 增加,减少投票权重的单位是 vote power
- 锁定到提现有一个时间限制,在staking合约中调用
voteLock()
时需要指定解锁时间
前置条件
- 通过
staking
合约进行质押并锁仓 - 锁仓金额不小于1000CFX
方法阐述
register
概念
PoW账户与PoS账户进行绑定的通道, 当PoW账户的锁仓金额达到1000CFX时,可以调用该方法来为PoS节点与PoW建立连接,目前1000CFX可以换取1 vote power
前置条件
- 有一个正在运行的PoS本地节点
- 在本地调用
conflux rpc local pos register --power 1
, 并复制第一个数据以供调用register
使用
输入参数
- indentifier: PoS节点的标识符
- votePower: 注册时需要换取的投票权重
- blsPubKey: PoS节点的blsPubKey
- vrfPubKey: PoS节点的vrfPubKey
- blsPubKeyProof: PoS节点的blsPubKeyProof
输出参数
无
increaseStake
概念
在PoW账户与PoS账户建立连接, 若用户有提高自己投票权重的需要, 可以调用该方法将锁仓后的CFX置换为投票权重
前置条件
- PoW账户与PoS账户建立了连接
- 新锁仓的CFX大于1000CFX
输入参数
- votePower: 需要提高的投票权重
输出参数
无
retire
概念
在PoW账户与PoS账户建立连接, 若PoW账户有解除和PoS账户相绑定的需要,可调用该方法通过部分或全部解除锁仓来实现绑定的解除功能
前置条件
- PoW账户与PoS账户建立了连接
输入参数
- votePower: 需要减少的投票权重
输出参数
无
getVotes
概念
通过调用该方法来获取质押的token的votes和解除锁仓后的token的votes
前置条件
- PoW账户与PoS账户建立了连接
输入参数
- identifier: PoS节点的标识
输出参数
- totalStakedVotes: 质押的token的votes
- totalUnlockedVotes: 解除锁仓后的token的votes
identifierToAddress
概念
根据PoS账户地址获取对应的PoW地址
前置条件
- PoW账户与PoS账户建立了连接
输入参数
- identifier: PoS账户的地址
输出参数
- address:PoW地址
addressToIdentifier
概念
根据PoW账户地址获取对应的PoS地址
前置条件
- PoW账户与PoS账户建立了连接
输入参数
- address: PoW账户的地址
输出参数
- identifier: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
前提概念
- 在conlux中有CoreSpace与ESpace两个域,对应着conflux与以太坊两条链;
- CoreSpace中的账户与ESpace中的账户有一一对应关系;
前置条件
- Conflux稳定运行
方法阐述
createEVM
概念
通过调用该方法,在espace中创建合约(合约地址可通过事件获得,也可通过解析call方法调用合约返回的数据来获得相应的合约地址)
注: 由于call方法不改变状态,且是将当前账户的nonce+1来作为生成合约地址的数据,因此需要先调用call方法获得合约地址,再去调用createEVM
来获得相应的合约地址
输入参数
- init 创建合约的字节码
输出参数
- 合约地址
transferEVM
概念
向PoW账户映射的Espace账户中转帐
前置条件
- conflux链稳定运行
输入参数
- to: PoW账户映射的Espace账户的地址
输出参数
- 转账信息
callEVM
概念
通过该方法调用ESpace下的合约(合约地址可通过事件获得,也可通过解析call方法调用合约返回的数据来获得相应的合约地址)
输入参数
- to: PoW账户映射的Espace账户的地址
- data: 调用合约所需要的参数encode后的数据
输出参数
- 调用合约成功信息
staticCallEVM
概念
通过该方法静态调用ESpace下的合约
输入参数
- to: CroeSpace账户映射的Espace账户的地址
- data: 调用合约所需要的参数encode后的数据
输出参数
- 调用合约成功信息
withdrawFromMapped
概念
从ESpace账户中收回相应的代币至其映射的CoreSpace账户
输入参数
- value: 回收的金额
输出参数
无
mappedBalance
概念
查询CoreSpace账户映射的CoreSpace账户的余额
输入参数
- address: CoreSpace账户的地址
输出参数
- 对应的余额
mappedNonce
概念
查询CoreSpace账户映射的CoreSpace账户的nonce
输入参数
- address: 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);
}