Cryptography in Fluence Developer Rewards

And how to claim the Fluence Airdrop

Over 110,000 web3 developers have been allocated 5,000 FLT tokens each, with allocations halving every three months. The Fluence team has released a comprehensive guide on how to claim your allocation of the ERC20 token FLT-DROP. For more details, visit:

Fluence Network - Developer Rewards

I happened to be one of the lucky devs who received an allocation, so I was curious how the distribution process works.

This article delves deeper into the cryptography underlying the distribution of rewards and outlines the conversion process from FLT-DROP to FLT.

TL;DR

Once your lockup period concludes, you can transfer 5,000 FLT (or your specific allocation) to any address. The FLT-DROP contract will then transfer an equivalent amount of FLT to your address. This process is outlined in the transfer(address to, uint256 value) function within the DevRewardDistributor contract, which can be viewed here:

Etherscan - DevRewardDistributor Contract

function transfer(address to, uint256 value) external returns (bool) {
        require(value > 0, "Value is 0");
        require(lockedBalances[msg.sender].amount == value, "Invalid amount");
        require(block.timestamp > lockedBalances[msg.sender].unlockTime, "Tokens are locked");

        // your FLT-DROP is set to 0
        lockedBalances[msg.sender].amount = 0;
        _totalSupply -= value;
        // FLT is transferred to you from the **DevRewardDistributor contract**
        IERC20(token).safeTransfer(msg.sender, value);
        emit Transfer(msg.sender, address(0x00), value);

        return true;
}

At the time of writing, the DevRewardDistributor (FLT-DROP) holds 48,740,000 FLT.

DevRewardDistributor's FLT balance

You can verify this yourself in the "Read as Proxy" section:

Etherscan - FLT

Note that the balance is displayed in wei, which is 1/(10^18) of an Ether.

What is Fluence?

Fluence is a distributed computing project which aims to liberate computation from centralized infrastructures, providing low-cost, decentralized, and verifiable computing solutions.

Learn more about their initiatives at Fluence Network.

Rewards Architecture

The rewards program consists of two main phases:

  • Generation Phase: Conducted prior to the contract deployment by the Fluence team.

  • Claim Phase: Executed by developers once the contracts are live.

Generation Phaze

During the generation phase, the Fluence team:

The source code for the generation script is accessible here:

Fluence Labs - Developer Rewards Script

Claim Phaze

To claim their allocated tokens, a developer must:

  • Decrypt the Ethereum account's private key using their GitHub private key.

  • Generate a Merkle proof of inclusion for their Ethereum account in the tree.

  • Sign an Ethereum message with the temporary account as proof of possession.

It is as easy as that!

We can check out the DevRewardDistributor contract to see the two cryptographic mechanisms in action:

/**
 * @notice Claim reward token
 * @param userId - user id in merkle tree
 * @param merkleProof - merkle proof for leaf
 * @param temporaryAddress - temporary Ethereum address that's used only for signing
 * @param signature - signature of temporary Ethereum address
 *
 */
function claimTokens(
    uint32 userId,
    bytes32[] calldata merkleProof,
    address temporaryAddress,
    bytes calldata signature
) external whenClaimingIs(true) {
    ...
    // the value, which existance in the merkle tree you prove
    bytes32 leaf = keccak256(abi.encodePacked(userId, temporaryAddress));
        // MerkleProof.verify reverts if the merkle proof is invalid,
        // e.g. you are trying to prove a non-existant temporary account
        // or an invalid tree root
    require(MerkleProof.verify(merkleProof, merkleRoot, leaf), "Valid proof required");

        // the message signed by your temporary address
    bytes32 msgHash = keccak256(abi.encodePacked("\\x19Ethereum Signed Message:\\n20", msg.sender));
    // the recovered signer of the provided message
    address signer = ECDSA.recover(msgHash, signature);
    // if the recovered signer is valid, you indeed posess this temporary account
    require(signer == temporaryAddress, "Invalid signature");

      ...
}

Conclusion

The setup here is pretty straightforward and well designed.

At first I was a bit surprised by the fact that the team decided to create random accounts and encrypt the private keys.

This implementation involves some trust — we have to trust that the team didn’t keep copies of the keys before they got encrypted.

I believe this has been done for the sake of simplicity of the cryptographic scheme.

A simple cryptosystem:

  • Is cheaper to develop (in developer hours).

  • Is faster to compute.

  • Is more straightforward to explain and understand.

Could it be done trustlessly though? Sure, that’s totally possible. But diving into how that might work is for another day and another article.