A Brief Tour of the Fair Auction Process

Delphi
10 min readJul 8, 2017

--

Any software engineer who has spent time working on non-trivial programming projects can tell you that it is easy for the code (and logic) to get very complex as the project grows. In many cases, the algorithms involved can end up so complex that they are virtually unintelligible. The KISS principle is generally a wise doctrine to follow. When simplicity is abandoned, usually code quality is, too.

Consensus algorithms, in particular, often suffer from high degrees of complexity. Protocols like Raft distinguish themselves almost entirely on their “understandability” and dedicate entire whitepapers to describing the merits of an easy-to-understand framework for engineers to build on.

At Delphi, we want to make sure that everything we do is as transparent and understandable as possible, and when it comes to the token launch, we want to make sure that this holds especially true. We feel that the Ethereum community deserves a simpler, more intuitive crowdsale than the one recently offered by Gnosis. While the company did publish explanations of their Dutch-auction setup and tried to explain why they adopted this structure for their crowdsale, the entire process seemed to greatly confuse and frustrate the community in general. In fact, due to the widespread confusion in the community, Gnosis even felt obligated to attempt to justify their high degree of complexity in a blog post titled “Why So Complicated?”. Unfortunately this didn’t seem to help; ultimately the auction process resulted in a hyper-concentrated token distribution, which has alarming repercussions for their model.

To reduce the risk of Delphi suffering the same fate, we have made sure that our FairAuction contract (which will automatically distribute the ∇ (DEL) auctioned in our crowdsale) is actually understandable, and we have tried to ensure that there is no room for misinterpretation or confusion regarding the contract’s operation. We have made it as simple and intuitive as possible. Even so, we think it would be helpful to provide the community with a brief walkthrough of how the contract works, to help answer any questions that you might have. The code in question is available on our GitHub, so please feel free to load it up as you follow along in this article.

Basic token functions our contract needs to be aware of:

contract token {
function transfer(address, uint256){ }
function balanceOf(address) constant returns (uint256) { }
}

This section just lets the FairAuction contract know which two functions of our DelphiToken we will need to call. As you can see, we are not actually implementing these functions here in FairAuction.sol; we’re simply telling our contract what to expect when we refer to these functions later on.

General internal state variables our contract will use:

address public beneficiary;
uint public amountRaised; uint public startTime; uint public deadline; uint public memberCount; uint public crowdsaleCap;
uint256 public tokenSupply;
token public tokenReward;
mapping(address => uint256) public balanceOf;
mapping (uint => address) accountIndex;
bool public finalized;

Here we declare a list of variables that will be used in our contract. Most of these variables should hopefully be relatively self-explanatory from a quick glance at their respective names, but there is one variable here which may require a brief explanation: accountIndex. Why do we want to maintain a mapping of unsigned integer indexes to addresses if we are already maintaining a mapping of addresses to balances?

The answer is that later on, if we want the contract to automatically distribute the auctioned tokens, it would have to iterate through our list of contributing addresses, and reward each one the token balance that it is due. However, Ethereum and Solidity aren’t set up to allow direct iteration over such address mappings:

Mappings can be seen as hash tables which are virtually initialized such that every possible key exists and is mapped to a value whose byte-representation is all zeros. This analogy does not go too far, though, as it is neither possible to obtain a list of all keys of a mapping, nor a list of all values.

In other words, our accountIndex variable will serve as an “overlay index” that helps us keep track of the relevant address keys in our balanceOf mapping. That way, the auction participants (and their respective contributions) can be accurately tracked and used later during the distribution phase, in a way that doesn’t require each participant to claim their tokens manually.

Event declarations:

event TokenAllocation(address recipient, uint amount);
event Finalized(address beneficiary, uint amountRaised);
event FundTransfer(address backer, uint amount);
event FundClaim(address claimant, uint amount);

These are simply declarations of Events, used for callback purposes. You can think of these as basically “notifier functions” to other applications that “listen to” the contract.

Contract initialization function:

function FairAuction(
address fundedAddress,
uint epochStartTime,
uint durationInMinutes,
uint256 capOnCrowdsale,
token contractAddressOfRewardToken
) {
beneficiary = fundedAddress;
startTime = epochStartTime;
deadline = startTime + (durationInMinutes * 1 minutes);
tokenReward = token(contractAddressOfRewardToken);
crowdsaleCap = capOnCrowdsale * 1 ether;
finalized = false;
}

This is the function used to actually initialize or deploy a FairAuction contract instance. The deployer of the contract is able to decide which address is set up as the recipient address for the crowdsale funds, the exact time that the auction will start accepting funds, how long the auction will run for (assuming the cap is not hit during the crowdsale), a cap on the amount of ETH the contract is willing to accept, and the specific Token ID (i.e. contract address) of the token that is being distributed by the FairAuction process. These parameters being generalized like this (rather than set up specifically for the Delphi crowdsale only) means that other token-launches are free to make use of our code, if they want to distribute their token via a Fair Auction in a similar manner to ours.

The default function:

The unnamed function (commonly known as the “default function” or “fallback function”) is called whenever someone attempts to send ether to a deployed FairAuction contract. This function contains the logic responsible for accepting (or rejecting) participation in the crowdsale:

function () payable {

The first thing our default function will do is check to make sure that the auction is live:

/* Ensure that auction is ongoing */
if (now < startTime) throw;
if (now >= deadline) throw;

If someone tried to send ether to the FairAuction before the startTime was reached, or if the auction had already terminated or expired, this code will reject the transaction (and the ether that it tries to send). This way, no one is at risk of losing their funds by accidentally sending at the wrong time, and the auction only allows participation during the designated time interval assigned during deployment.

After that, the function will make sure that the sent ether doesn’t exceed the cap on our crowdsale:

uint amount = msg.value;
/* Ensure that we do not pass the cap */
if (amountRaised + amount > crowdsaleCap) throw;

If someone were trying to send ETH that would exceed the FairAuction’s cap or limit, this code would automatically prevent the transaction from completing.

If the contract has made it this far, it means that the bid being sent will be accepted, because it adheres to the rules of the FairAuction (by being sent at the right time, and by not overfunding the contract).

Now, the function will check how much the sender has already sent to the contract (and if they are a new participant, we will add their account to our overlay index for later iteration):

uint256 existingBalance = balanceOf[msg.sender];
/* Tally new members (helps iteration later) */
if (existingBalance == 0) {
accountIndex[memberCount] = msg.sender;
memberCount += 1;
}

This helps us to transparently keep track of how many people have participated so far in the auction.

The function will then need to credit the sender for the amount that they’ve sent into the contract, as well as adding that same amount to the running total received in the FairAuction:

/* Track contribution amount */
balanceOf[msg.sender] = existingBalance + amount;
amountRaised += amount;

Both of these values will be used later, during the distribution phase.

Finally, the function will fire an Event, for any listening applications, to notify them:

/* Fire FundTransfer event */
FundTransfer(msg.sender, amount);

This completes the “crowdsale participation” phase of the contract; the only thing remaining is the “token distribution” phase.

Finalization:

Our FairAuction makes use of a finalize() function, which will basically take a snapshot of how many reward tokens the contract has to disburse and mark the auction as done. This function can be called by anyone, at any time, although it will not have an effect unless the auction has been completed.

function finalize() {

The first thing that our finalize() function will check is whether the FairAuction even raised anything at all during the crowdsale. If it did not, then there is no point in trying to finalize anything, because there is nothing to finalize in the first place:

/* Nothing to finalize */
if (amountRaised == 0) throw;

This will also prevent us from accidentally trying to divide by zero.

Next, we will stipulate the conditions under which a FairAuction can be successfully finalized:

/* Auction still ongoing */
if (now < deadline) {
/* Don’t terminate auction before cap is reached */
if (amountRaised < crowdsaleCap) throw;
}

Translated to English, this becomes: “If the FairAuction deadline has been reached, or the cap on the crowdsale has already been hit, finalization is allowed. Otherwise, it is not.” We don’t want to allow someone to call this function and prematurely end the auction before the deadline has been reached, unless the cap has already been hit, in which case the auction wouldn’t accept any further bids anyway. This way, if the cap is hit before the deadline is reached, users do not have to needlessly wait to receive their tokens.

As the contract is finalized, we save the value of the total supply of reward tokens available to it, to ensure that the full balance is distributed fairly:

tokenSupply = tokenReward.balanceOf(this);

Finally, we mark our contract as finalized and send out one last Event (notification), to let listeners know that the contract and auction has been successfully finalized:

finalized = true;
/* Fire Finalized event */
Finalized(beneficiary, amountRaised);

Post-Finalization

After the contract has been finalized, the individualClaim(), beneficiarySend(), and automaticWithdrawLoop() functions can be called. To make sure that the finalization has already occurred successfully, each of these functions starts with a quick check:

if (!finalized) throw;

The beneficiarySend() function simply sends the raised ETH to the beneficiary address of the contract, and fires a FundClaim event as a notification that this occurred:

/* Send proceeds to beneficiary */
if (beneficiary.send(amountRaised)) {
/* Fire FundClaim event */
FundClaim(beneficiary, amountRaised);
}

Claims

Finally, we reach the interesting portion of the contract, where the token rewards are distributed fairly among the auction participants, according to their respective contributions. The rewards are able to be claimed in two different ways: individually, or automatically via a loop.

An individualClaim() call can be initiated by any auction participant, to transfer the tokens they are owed by the contract to their contributing address on demand:

tokenReward.transfer(msg.sender, (balanceOf[msg.sender] * tokenSupply / amountRaised));

This might seem complicated at first glance, but once you break it down, it’s not so bad. In a nutshell, what this does is divide the “total contribution balance of this particular contributor” by the “total contribution balance of all contributors”, multiplies this value by the “total (DEL) token supply being awarded by the contract”, and sends the final result to the relevant contributing address.

After this is done, a TokenAllocation event fires to notify listening applications, and we set the balance-due of the claimant address to zero, so that users cannot try to “double claim” their tokens:

/* Fire TokenAllocation event */
TokenAllocation(msg.sender, (balanceOf[msg.sender] * tokenSupply / amountRaised));
/* Prevent repeat-withdrawals */
balanceOf[msg.sender] = 0;

To save users from having to initiate their own withdrawals manually, we have also included an automatic distribution option:

function automaticWithdrawLoop(uint startIndex, uint endIndex) {

This function follows the same logic as the individualClaim() function described above, but uses a loop which can be called by anyone to disburse arbitrarily-large batches of token rewards at once. It does this by iterating over the memberCount overlay index we have set up to keep track of crowdsale contributors:

/* Distribute auctioned tokens among participants fairly */
for (uint i=startIndex; i<=endIndex && i<memberCount; i++) {
/* Should not occur */
if (accountIndex[i] == 0)
continue;
/* Grant tokens due */
tokenReward.transfer(accountIndex[i], (balanceOf[accountIndex[i]] * tokenSupply / amountRaised));
/* Fire TokenAllocation event */
TokenAllocation(accountIndex[i], (balanceOf[accountIndex[i]] * tokenSupply / amountRaised));
/* Prevent repeat-withdrawals */
balanceOf[accountIndex[i]] = 0;
}

The bottom few lines of this should look very familiar, because they are almost identical to those in the individualClaim() function. After a quick “sanity check” to ensure that nothing unexpected happened with the index/account association, these token transfers occur, one-by-one, allocating to those accounts which have not yet claimed their tokens individually.

Simple Example

To make sure the bigger picture here isn’t confusing, let’s examine an (extremely simple) example of how the contract could play out:

  1. The FairAuction contract is provided 1000 DEL as the reward token.
  2. Alice sends 10 ETH to the FairAuction contract.
  3. Bob sends 7.5 ETH to the FairAuction contract.
  4. Carol sends 2.5 ETH to the FairAuction contract.
  5. The FairAuction contract’s deadline is reached.
  6. The FairAuction is finalized.
  7. The automaticWithdrawLoop() is initiated.

In this example, the tokenSupply is 1000 DEL, and the amountRaised is 20 ETH. Therefore, when the finalize() function is called, Alice will receive (10 * 1000 / 20) = 500∇, Bob will receive (7.5 * 1000 / 20) = 375∇, and Carol would receive (2.5 * 1000 / 20) = 125∇.

As you can see, Alice sent 4x as much ETH into the FairAuction as Carol did, and as a result receives 4x as much ∇ as Carol. Similarly, Bob contributed 3x as much ETH as Carol did, and likewise received 3x the ∇ that she did. In other words, everyone is compensated fairly (and simply!) according to the ratio of their contribution to the total contributions received.

Conclusion

We set out to make sure that our crowdsale mechanism was as easily-understandable (and reviewable) as possible. We wanted to make sure that there was no confusion in the community on the logic or algorithm involved, and we hope that this code walkthrough has helped to achieve this goal. We wanted to set ourselves apart by keeping things simple and straightforward. This way, the market is able to properly assess what it needs to and decide upon a fair valuation for the auctioned tokens as a result.

Edit: Since publication, we have added individual withdrawal functionality to the FairAuction (and updated this walkthrough accordingly).

--

--