跳到主要内容

SponsorWhitelistControl

Conflux实现了赞助机制,来补贴智能合约的使用。 This allows a new account with a zero balance to call smart contracts, provided the execution is sponsored (usually by the operator of Dapps). The internal SponsorWhitelistControl contract records the sponsorship information for smart contracts.

When a message call occurs, Conflux does not recheck sponsorship. For instance, if normal address A calls contract B and contract B calls contract C, Conflux only checks whether address A is sponsored by contract B. 如果A被赞助,B将承担交易执行过程中的所有gas和/或存储抵押,包括从BC的消息调用。 换句话说,只有一个交易发送者才能被赞助。

SponsorWhitelistControl Interfaces

pragma solidity >=0.4.15;

contract SponsorWhitelistControl {
/*** Query Functions ***/
/**
* @dev get gas sponsor address of specific contract
* @param contractAddr The address of the sponsored contract
*/
function getSponsorForGas(address contractAddr) public view returns (address) {}

/**
* @dev get current Sponsored Balance for gas
* @param contractAddr The address of the sponsored contract
*/
function getSponsoredBalanceForGas(address contractAddr) public view returns (uint256) {}

/**
* @dev get current Sponsored Gas fee upper bound
* @param contractAddr The address of the sponsored contract
*/
function getSponsoredGasFeeUpperBound(address contractAddr) public view returns (uint256) {}

/**
* @dev get collateral sponsor address
* @param contractAddr The address of the sponsored contract
*/
function getSponsorForCollateral(address contractAddr) public view returns (address) {}

/**
* @dev get current Sponsored Balance for collateral
* @param contractAddr The address of the sponsored contract
*/
function getSponsoredBalanceForCollateral(address contractAddr) public view returns (uint256) {}

/**
* @dev check if a user is in a contract's whitelist
* @param contractAddr The address of the sponsored contract
* @param user The address of contract user
*/
function isWhitelisted(address contractAddr, address user) public view returns (bool) {}

/**
* @dev check if all users are in a contract's whitelist
* @param contractAddr The address of the sponsored contract
*/
function isAllWhitelisted(address contractAddr) public view returns (bool) {}

/*** for contract admin only **/
/**
* @dev contract admin add user to whitelist
* @param contractAddr The address of the sponsored contract
* @param addresses The user address array
*/
function addPrivilegeByAdmin(address contractAddr, address[] memory addresses) public {}

/**
* @dev contract admin remove user from whitelist
* @param contractAddr The address of the sponsored contract
* @param addresses The user address array
*/
function removePrivilegeByAdmin(address contractAddr, address[] memory addresses) public {}

// ------------------------------------------------------------------------
// Someone will sponsor the gas cost for contract `contractAddr` with an
// `upper_bound` for a single transaction.
// ------------------------------------------------------------------------
function setSponsorForGas(address contractAddr, uint upperBound) public payable {}

// ------------------------------------------------------------------------
// Someone will sponsor the storage collateral for contract `contractAddr`.
// ------------------------------------------------------------------------
function setSponsorForCollateral(address contractAddr) public payable {}

// ------------------------------------------------------------------------
// Add commission privilege for address `user` to some contract.
// ------------------------------------------------------------------------
function addPrivilege(address[] memory) public {}

// ------------------------------------------------------------------------
// Remove commission privilege for address `user` from some contract.
// ------------------------------------------------------------------------
function removePrivilege(address[] memory) public {}

/**
* @dev get current available storage points for collateral (activated after CIP-118)
* @param contractAddr The address of the sponsored contract
*/
function getAvailableStoragePoints(address contractAddr) public view returns (uint256) {}
}

How to Sponsor a Contract

SponsorWhitelistControl maintains a whitelist for each user-established contract containing accounts eligible for the subsidy. First and foremost, eligible accounts should be added into the whitelist using addPrivilege(address[] memory) or addPrivilegeByAdmin(address contractAddr, address[] memory addresses). Specially, if a zero address is added to the whitelist, any account will become eligible for subsidy.

有两种资源可以被赞助:gas消耗和存储抵押。 The two resources can be sponsored separately through payable interfaces setSponsorForGas(address contractAddr, uint upperBound) and setSponsorForCollateral(address contractAddr).The paid CFX will be used for future gas or storage collateral sponsorship.

备注

The upperBound (unit: Drip) sets the sponsor upper bound of each transaction. And the value sent by transaction should be no less than 1000 * upperBound.

示例

Suppose you have the provided test contract needs sponsorship:

pragma solidity >=0.8.0;

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 {
SponsorWhitelistControl cpc = SponsorWhitelistControl(0x0888000000000000000000000000000000000001);
address[] memory a = new address[](1);
a[0] = account;
cpc.addPrivilege(a);
}

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

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

The following javascript code shows how to deploy and sponsor the provided test contract.

"use strict"
const { Conflux, Drip } = require("js-conflux-sdk")
// you need to change the path to the compiled contract
const { abi, bytecode } = require("path/to/CommissionPrivilegeTest.json");

async function main() {
// your secret key to deploy contract
// testnet token can be claimed at https://faucet.confluxnetwork.org/
const PRIVATE_KEY = '0x......';
const cfx = new Conflux({
url: 'https://test.confluxrpc.com',
// logger: console,
networkId: 1,
});
const account = cfx.wallet.addPrivateKey(PRIVATE_KEY); // create account instance
const randomAccount = cfx.wallet.addRandom() // a random account with no cfx
const testContract = cfx.Contract({
abi,
bytecode
})

const contract_addr = (await testContract.constructor().sendTransaction({
from: account.address
}).executed()).contractCreated
console.log(`contract deployed at ${contract_addr}`)

testContract.address = contract_addr
await testContract.add(randomAccount.address).sendTransaction({
from: account.address
}).executed()
console.log(`random address ${randomAccount.address} added to whitelist`)

const sponsor_contract = cfx.InternalContract('SponsorWhitelistControl');

const upperBound = 10n**15n
const upperBoundCfx = Drip(upperBound).toCFX()
const gasSponsorVal = 10n**18n
const storageSponsorVal = 10n ** 18n
if( gasSponsorVal < upperBound * 1000n ) {
throw new Error(`gas sponsor value should be greater than 1000 * upperBound`)
}
await sponsor_contract.setSponsorForGas(contract_addr, upperBound).sendTransaction({
from: account,
value: gasSponsorVal
}).executed();
console.log(`Gas is sponsored with upper bound ${upperBound} Drip (${upperBoundCfx} CFX)`)
await sponsor_contract.setSponsorForCollateral(contract_addr).sendTransaction({
from: account,
value: storageSponsorVal
}).executed();
console.log("Storage collateral is sponsored")

const receipt = await testContract.par_add(1, 3).sendTransaction({
from: randomAccount.address
}).executed()
console.log(`${receipt.transactionHash} is sent`)
console.log(`gas and storage covered by sponsor: ${receipt.gasCoveredBySponsor && receipt.storageCoveredBySponsor}`)
}

main().catch(
console.error
)

The example provided illustrates how to deploy and sponsor a test contract. The code is divided into five main sections:

  • Setting Up Conflux Instance and Accounts
  • Deploying the Smart Contract
  • Interacting with the Deployed Contract
  • Sponsoring Gas and Storage
  • Sending a Transaction whose Gas and Storage are Sponsored
  1. Setting Up Conflux Instance and Accounts:

    const PRIVATE_KEY = '0x......';
    const cfx = new Conflux({
    url: 'https://test.confluxrpc.com',
    networkId: 1,
    });
    const account = cfx.wallet.addPrivateKey(PRIVATE_KEY);
    const randomAccount = cfx.wallet.addRandom();
    • PRIVATE_KEY: A placeholder for the private key of the user. This is essential for deploying contracts and sending transactions. You need to replace this value with your own private key with enough CFX
    • account: An account instance created using the provided private key. Will be used to deploy contract.
    • randomAccount: A new random account instance. This account doesn't have any CFX (Conflux's native currency) by default.
  2. Deploying the Smart Contract:

    const testContract = cfx.Contract({
    abi,
    bytecode
    });
    const contract_addr = (await testContract.constructor().sendTransaction({
    from: account.address
    }).executed()).contractCreated;
    console.log(`contract deployed at ${contract_addr}`);
    • testContract: A new contract instance is created using the ABI and bytecode.
  3. Interacting with the Deployed Contract:

    testContract.address = contract_addr;
    await testContract.add(randomAccount.address).sendTransaction({
    from: account.address
    }).executed();
    console.log(`random address ${randomAccount.address} added to whitelist`);
    • The address of the deployed contract is set to the testContract instance.
    • A transaction is sent to the contract to add the random account's address to a whitelist.
  4. Sponsoring Gas and Storage:

    const sponsor_contract = cfx.InternalContract('SponsorWhitelistControl');
    const upperBound = 10n**15n;
    const upperBoundCfx = Drip(upperbound).toCFX();
    const gasSponsorVal = 10n**18n;
    const storageSponsorVal = 10n ** 18n;
    if( gasSponsorVal < upperBound * 1000n ) {
    throw new Error(`gas sponsor value should be greater than 1000 * upperBound`);
    }
    await sponsor_contract.setSponsorForGas(contract_addr, upperBound).sendTransaction({
    from: account,
    value: gasSponsorVal
    }).executed();
    console.log(`Gas is sponsored with upper bound ${upperBound} Drip (${upperBoundCfx} CFX)`);
    await sponsor_contract.setSponsorForCollateral(contract_addr).sendTransaction({
    from: account,
    value: storageSponsorVal
    }).executed();
    console.log("Storage collateral is sponsored");
    • The code sets an upper bound for gas sponsorship and calculates its equivalent in CFX. It makes sure if the gas sponsorship value is at least 1000 times the upper bound. If not, an error is thrown. (This is the requirement of the SponsorWhitelistControl interface)
    • The code then sponsors gas and storage for the deployed contract. This means users interacting with the contract won't have to pay for gas or storage, as it's covered by the sponsor.
  5. Sending a Transaction whose Gas and Storage are Sponsored:

    const receipt = await testContract.par_add(1, 3).sendTransaction({
    from: randomAccount.address
    }).executed();
    console.log(`${receipt.transactionHash} is sent`);
    console.log(`gas and storage covered by sponsor: ${receipt.gasCoveredBySponsor && receipt.storageCoveredBySponsor}`);
    • A transaction is sent to the contract, calling the par_add function with arguments 1 and 3.
    • Log the transaction's hash and whether its gas and storage were covered using the transaction receipt.

Specification

Conflux keeps the following information for each user-established contract:

  • sponsor_for_gas: this is the account that provides the subsidy for gas consumption, and can be accessed via SponsorWhitelistControl or getSponsorInfo RPC;
  • sponsor_for_collateral: this is the account that provides the subsidy for collateral for storage, and can be accessed via SponsorWhitelistControl or getSponsorInfo RPC;
  • sponsor_balance_for_gas: this is the balance of subsidy available for gas consumption, and can be accessed via SponsorWhitelistControl or getSponsorInfo RPC;
  • sponsor_balance_for_collateral: refundable balance of subsidy available for collateral for storage, and can be accessed via SponsorWhitelistControl or getSponsorInfo RPC;
  • availableStoragePoints: storage points available for storage collateral, and can be accessed via SponsorWhitelistControl or getSponsorInfo RPC;
  • usedStoragePoints: storage points available for storage collateral, can be accessed via getSponsorInfo RPC, can only be accessed via getSponsorInfo RPC;
  • sponsor_limit_for_gas_fee: this is the upper bound for the gas fee subsidy paid for every sponsored transaction, and can be accessed via SponsorWhitelistControl or getSponsorInfo RPC;
  • whitelist: 这是有资格获得补贴的普通账户列表,其中一个特殊的全零地址指代所有普通账户。 只有合约本身和管理员有权更改这个列表。 The elements of whitelist cannot be accessed directly, instead, isWhitelisted interface of SponsorWhitelistControl can tell if an address is whitelisted.

有两种资源可以被赞助:gas消耗和存储抵押。

  • For gas consumption: If a transaction calls a contract with non-empty sponsor_for_gas and the sender is in the whitelist of the contract and the gas fee specified by the transaction is within the sponsor_limit_for_gas_fee, the gas consumption of the transaction is paid from the sponsor_balance_for_gas of the contract (if it is sufficient) rather than from the sender’s balance. 否则,发送者应该支付gas消耗。
  • For storage collateral: If a transaction calls a contract with non-empty available_storage_points or sponsor_for_collateral and the sender is in the whitelist of the contract, the collateral for storage incurred in the execution of the transaction is deducted from sponsor_for_collateral(with priority) or available_storage_points of the contract, and the owner of those modified storage entries is set to the contract address accordingly. 否则,发送者应该支付交易执行过程中产生的存储抵押。

Storage Points

CIP-107 introduced the concept of storage points to improve Conflux tokenomics.

When a sponsor adds storage collateral for a contract, a proportion of the CFX tokens will be burned and corresponding amount of "storage point" will be mint. The "storage point" cannot be transferred and will not generate storage interest. But it can pay for storage collateral as CFX tokens.

The storage point system is designed to have a minimal impact on economics. Contract sponsors mainly care about the amount of tokens required to support their business operations, but aren't typically concerned about the refund of storage collaterals.

setSponsorForGas and setSponsorForCollateral behavior

When a contract is created, its sponsor_for_gas and sponsor_for_collateral will be initialized by zero address, and the sponsor balance and storage points will be initialized by 0. Both sponsorship for gas and for collateral can be updated by calling setSponsorForGas and setSponsorForCollateral of the SponsorWhitelistControl contract.

However, the behavior of the mentioned interfaces varies in different situation.

增加赞助余额

An accounts can provide sponsor balance if the sponsor is never set or the current sponsor is the account itself. In this case, the sponsor should interact with function setSponsorForGas(address contractAddr, uint upperBound) or setSponsorForCollateral(address contractAddr).

  • For setSponsorForGas(address contractAddr, uint upperBound), transferred value in drip will be added to sponsor_balance_for_gas if following requirements meet:
    • The new value for upperBound should be no less than the old upperBound unless the current sponsor_balance_for_gas cannot afford the old sponsor_limit_for_gas_fee. Noting, if the sponsor is never set, this rule will be ignored.
    • Besides, the transferred fund should be no less than 1000 times of the new limit, which means the sponsorship should at least support 1000 calls.
    • The transferred value in drip will be added to sponsor_balance_for_gas.
  • For setSponsorForCollateral(address contractAddr), there is no extra requirement. p proportion of the surplus CFX provided (whereas p * tx.value) will be burnt and converted into storage_points. The rest ((1-p) * tx.value) will be added to sponsor_balance_for_collateral.

赞助替换

Gas Sponsor Replacement

To replace the gas sponsor of a contract, the new sponsor must meet specific conditions:

  1. 转移的资金应该大于合约当前的sponsor_balance_for_gas
  2. 新的sponsor_limit_for_gas_fee(由upperBound参数指定)应该不低于旧赞助者的限制,除非旧的sponsor_balance_for_gas不能负担旧的sponsor_limit_for_gas_fee
  3. 转移的资金应该 >=新限制的1000倍,这样才能足以补贴至少1000次调用合约的交易。

如果满足以上条件,剩余的sponsor_balance_for_gas将退还给旧的sponsor_for_gas,而被转移到内部合约的资金将加到合约的sponsor_balance_for_gas中。 然后,根据新赞助者的指定,更新sponsor_for_gassponsor_limit_for_gas_fee。 否则,将触发一个异常。

Collateral Sponsor Replacement

The replacement of collateral sponsorship is similar but more complex due to storage points. As a proportion of CFX is burnt, the new sponsor should transfer a fund more than the refundable CFX provided by the current sponsor for collateral of the contract, whereas,

refundable storage collateral = sponsor_balance_for_collateral + (collateral_for_storage - use_storage_points / 1024)

The origin sponsor will be refunded with the above CFX immediately after the sponsor replacement. The collateral_for_storage refers to storage collateral already sponsored, accessible via cfx_getAccount RPC with contract's address as parameter.

p proportion of the surplus CFX provided will be burnt and converted into storage_points, whereas,

surplus storage points
= p * (surplus CFX provided) * 1024
= p * (tx.value - previous refundable collateral) * 1024
= p * (tx.value - (sponsor_balance_for_collateral + (collateral_for_storage - use_storage_points / 1024))) * 1024

白名单维护

只有合约本身或合约管理员可以更新合约的白名单。 Sponsors have no rights to change the whitelist.

一个合约可以调用函数addPrivilege(address[] memory)将任何地址添加到白名单中。 这意味着如果设置了sponsor_for_gas,合约将为白名单中的账户支付gas费用,如果设置了sponsor_for_collateral,合约将为白名单中的账户支付CFS(存储抵押)。 零地址是一个特殊的地址0x0000000000000000000000000000000000000000。 如果这个地址被添加到白名单中,所有调用这个合约的交易都将被赞助。 一个合约可以调用函数removePrivilege(address[] memory)从白名单中移除一些普通账户地址。 移除一个不存在的地址不会导致错误或异常。

边界情况:

  1. A contract address can also be added to the whitelist, but it is meaningless since only the transaction sender could be sponsored.

合约的管理员可以使用接口addPrivilegeByAdmin(address contractAddr, address[] memory addresses)removePrivilegeByAdmin(address contractAddr, address[] memory addresses)来维护白名单。