Reward Distribution

Every app receives the B3TR from weekly allocation rounds in a contract called X2EarnRewardsPool.

Instead of withdrawing and transferring B3TR tokens to your users, you should distribute the rewards directly from that contract. By doing such you will:

  • Allow VeBetterDAO and the community to track your reward distribution

  • Attach proofs and impacts to each reward distribution

  • Increase the identity score of your users allowing them to participate in governance voting

There are 3 functions available in the X2EarnRewardsPool contract that will allow you to distribute the rewards:

distributeReward(bytes32 appId, uint256 amount, address receiver, string)

1) appId is the ID of your app that you can find on your VeBetterDAO App page; 2) amount is the quantity of B3TR to transfer 3) The receiver is the address to who you want to transfer B3TR 4) The last parameter should be an empty string

How to implement the distribution method

If you are currently sending tokens with a normal transfer then the following guides should help you to start using the X2EarnRewardsPool contract.

Upgrade Smart Contract

Import the IX2EarnRewardsPool interface in your smart contract, and call the X2EarnRewardsPool contract to distribute the rewards instead of the B3TR token contract.

In solidity, a normal token transfer looks like this:

IERC20 token = IERC20(rewardToken);
require(
    token.transfer(receiver, rewardAmount),
    "Reward transfer failed"
);

To use the VeBetterDAO contract you should replace that with the following:

IX2EarnRewardsPool x2EarnRewardsPool = IX2EarnRewardsPool(rewardsPoolAddress);
// If distributeReward fails, it will revert
x2EarnRewardsPool.distributeReward(
    APP_ID, // get it from the VeBetterDAO dApp
    rewardAmount,
    receiver,
    "",
);

Look at the integration guide to see what your final contract should look like.

Upgrade Backend

If you were distributing rewards through a backend script by calling the transfer method on the B3TR contract, it should look like this:

const contractAddress = process.env.CONTRACT_ADDRESS;
const fromAddress = process.env.FROM_ADDRESS_PUB;
const defaultPrivateKey = process.env.FROM_ADDRESS_PRIV;

const client = new ThorClient(new HttpClient("https://mainnet.vechain.org/"));
const transferAbi = {
  name: "transfer",
  inputs: [
    {
      name: "_to",
      type: "address",
    },
    {
      name: "_value",
      type: "uint256",
    },
  ],
  outputs: [],
};

const transferFragment = new fragment.Function(
  ethers.FunctionFragment.from(transferAbi),
);

const buildClauses = (records) => {
  const clauses = [];

  for (let i = 0; i < records.length; i++) {
    const { address, amount } = records[i];

    const _amount = unitsUtils.parseUnits(amount, 18);

    const data = transferFragment.encodeInput([address, _amount]);

    clauses.push({
      to: contractAddress,
      data,
      value: "0x0",
    });
  }

  if (clauses.length === 0) {
    console.log("No clauses to build");
    throw new Error("No clauses to build");
  }

  return clauses;
};

const airdropBatch = async (batch, privateKey) => {
  const clauses = buildClauses(batch);
  const from = addressUtils.fromPrivateKey(Buffer.from(privateKey, "hex"));

  // Options to use gasPadding
  const options = {
    gasPadding: 0.2 // 50%
  };

  const estimation = await client.gas.estimateGas(
    clauses,
    fromAddress
  );

  if (estimation.reverted) {
    console.log("Estimation failed", estimation);
    throw new Error("Estimation failed");
  }

  const txBody = await client.transactions.buildTransactionBody(
    clauses,
    estimation.totalGas,
  );

  const signed = await client.transactions.signTransaction(txBody, privateKey);

  const tx = await client.transactions.sendTransaction(signed);

  //console.log(`Transaction ID: ${tx.id}`);
  console.log(`https://vechainstats.com/transaction/${tx.id}`);
};

Update your code to call the X2EarnRewardsPool contract instead of the B3TR contract, like this:

const contractAddress = process.env.X2EARN_REWARDS_POOL_CONTRACT_ADDRESS;
const fromAddress = process.env.FROM_ADDRESS_PUB;
const defaultPrivateKey = process.env.FROM_ADDRESS_PRIV;

const client = new ThorClient(new HttpClient("https://mainnet.vechain.org/"));
const distributeRewardAbi = {
  "inputs": [
    {
      "internalType": "bytes32",
      "name": "appId",
      "type": "bytes32"
    },
    {
      "internalType": "uint256",
      "name": "amount",
      "type": "uint256"
    },
    {
      "internalType": "address",
      "name": "receiver",
      "type": "address"
    },
    {
      "internalType": "string",
      "name": "proof",
      "type": "string"
    }
  ],
  "name": "distributeReward",
  "outputs": [],
  "stateMutability": "nonpayable",
  "type": "function"
};

const distributeRewardFragment = new fragment.Function(
  ethers.FunctionFragment.from(distributeRewardAbi),
);

const buildClauses = (records) => {
  const clauses = [];

  for (let i = 0; i < records.length; i++) {
    const { address, amount } = records[i];

    const _amount = unitsUtils.parseUnits(amount, 18);

    const data = distributeRewardFragment.encodeInput([APP_ID, _amount, address, ""]);

    clauses.push({
      to: contractAddress,
      data,
      value: "0x0",
    });
  }

  if (clauses.length === 0) {
    console.log("No clauses to build");
    throw new Error("No clauses to build");
  }

  return clauses;
};

const airdropBatch = async (batch, privateKey) => {
  const clauses = buildClauses(batch);
  const from = addressUtils.fromPrivateKey(Buffer.from(privateKey, "hex"));

  // Options to use gasPadding
  const options = {
    gasPadding: 0.2 // 50%
  };

  const estimation = await client.gas.estimateGas(
    clauses,
    fromAddress
  );

  if (estimation.reverted) {
    console.log("Estimation failed", estimation);
    throw new Error("Estimation failed");
  }

  const txBody = await client.transactions.buildTransactionBody(
    clauses,
    estimation.totalGas,
  );

  const signed = await client.transactions.signTransaction(txBody, privateKey);

  const tx = await client.transactions.sendTransaction(signed);

  //console.log(`Transaction ID: ${tx.id}`);
  console.log(`https://vechainstats.com/transaction/${tx.id}`);
};

Provide Sustainability Proof

You are now required to provide proof of the sustainable action the user performed to receive the reward. You can still set the proof to "" and not provide it but this will have a bad impact on your reputation and could lead to the expulsion from VeBetterDAO.

Read more about the proof standard and how we expect you to provide it in the Sustainability Proofs section.

Last updated