How We Scale Currency Splits — Fragments

At Fragments, we’re developing a low volatility coin that splits on inflation. This is a novel approach and we get a lot of questions about how splits work in practice — so in this post we’ll talk about how we implement them efficiently.

Fragments are price targeted to $1 USD like a stablecoin. However unlike stablecoins, when demand increases the new supply is automatically split to coin-holders. To better understand what we mean by splitting, join us along the ideal path of a single Fragment across three moments in time — t1,t2,t3

At t1 Alice holds one token worth $1. At t2 the exchange rate doubles and Alice is momentarily holding one token worth $2. To maintain price stability the currency splits, so that at t3 Alice holds 2 tokens each worth $1.

Instead of inflating supply entirely from a central authority — to devalue the currency Alice is holding — we can split the new supply with Alice such that her net holdings aren’t devalued. This has the benefit of rewarding early adopters for joining the network, while maintaining unit price stability … pretty neat!

But how do we distribute the supply without congesting the Ethereum network?

Approach 1 — Airdropping

To start let’s imagine a simple airdrop. When the system needs to increase supply by X tokens, it can just divide up the X tokens pro-rata, and airdrop the new supply by sending a transaction to each wallet with the computed amount.

Unfortunately, the airdrop approach is really slow and expensive on the Ethereum network, scaling linearly with the number of wallets to update, O(N) wallets.

Cost Estimate: Airdrop
Say there are 1M wallets. Assuming a $0.05 transaction fee, the cost of 1M transactions would be ~$50,000 and it would take ~20 hours to complete a single rebase operation.

At scale the airdrop approach is cost prohibitive both in time and money. A better approach would be sub-linear or constant with the number of wallets.

Approach 2 — Updating Scalar Contract Variables

Alternatively, we can hold scalar variables and use these to compute balances on the fly when they’re exposed through the usual ERC20 interface functions. The benefit of this approach is we can update all wallet balances with a single transaction simply by updating the splitRatio, instead of executing a transaction for every wallet.

If we want to increase the balances of all wallets by X% we send a transaction to update the splitRatio by X% thus affecting all wallets in a single TX and bringing the cost down to constant time, O(1) wallets.

First we declare some global scalars:

// global scalar variables we use 
private externalSupply;
private internalSupply;
private splitRatio;

Then we overwrite all external interfaces so that they are denominated in externalSupply units (in our case Fragments), while all internal operations execute on hidden internalSupply units.

// public interfaces we overwrite
// retrieves wallet balance
function balanceOf (walletAddress) {}
// transfers from address to address
function transferFrom (from, to, externalValue) {}
// transfer from sender to address
function transfer (to, externalValue) {}
// approve the passed address to spend a specific 
// amount of tokens on behalf of msg.sender
function approve (spender, externalValue) {}

Let’s dig into the use case of retrieving balance. At a high level, to retrieve a user’s wallet balance, we multiply the user’s privateSupply by a globally maintained splitRatio to return their external (Fragments) balance.

function internalToExternal(interalValue) {       
return splitRatio.mul(interalValue);
}
function externalToInternal(externalValue) {
return externalValue.div(splitRatio);
}
function balanceOf(walletAddress){
return internalToExternal(internalBalances[walletAddress]);
}

Similarly we overwrite the transfer() method to update changes in privateBalances before emitting a Transfer.

function transfer(to, publicValue) {
privateValue = publicToPrivate(publicValue);
privateBalances[msg.sender] = \
privateBalances[msg.sender].sub(privateValue);
privateBalances[to] = privateBalances[to].add(privateValue);
emit Transfer(msg.sender, to, publicValue);
return true;
}

And so on and so forth for approve() and transferFrom(). This approach leverages the structure of smart contracts for a substantial gain in performance.

Cost Estimate: Scalar Update
Say there are 1M wallets. Assuming a $0.05 transaction fee, the cost of 1M transactions would be ~$0.05 and complete in ~15s.

Conclusion

Going from an O(N) to O(1) operation is a huge win, which is why we’ve adopted the Scalar Contract Variable approach, but the decision is not without complication.

You may have noticed that we left out type declarations in the pseudocode above. Actually we’ve left out quite a bit more — and this has to do with the fact that Solidity doesn’t persist floating point numbers.

Division is a tricky business in accounting software, which is probably why Solidity doesn’t support floats. Rounding and numerical stability is something that we, as developers, should be forced to think carefully about. In practice, instead of holding a decimal splitRatio we persist cumulativeSupplyAdditions and infer a splitRatio in memory at the time of calculation. Converting to and from hiddenSupply with consistent precision calls for careful testing, there’s no easy way around it — and we’ll need as many eyes on this as possible.

Stay tuned for the open source contract, we look forward to your feedback!

www.fragments.org

Join us on Telegram

If you enjoyed this article, please hit the clap icon!