Craft Whiskey Crypto Payments With Chainlink Oracles

For their Chainlink bounty-winning submission at the 2021 ETHGlobal MarketMake Hackathon, Jasper and Tillman Degens created Whiskey MarketMaker, a DeFi platform that leverages Chainlink oracles to enable small-batch whiskey distilleries to tokenize and list maturing bottles of whiskey for investors, while also allowing distilleries to earn interest on ETH earned from sales via an Aave interest-bearing pool. In this post, Jasper and Tillman walk through how they built their prize-winning submission with Chainlink’s ETH/USD Price Feed, as well as how they overcame the challenge of accurately representing floating point numbers in Solidity smart contracts.


By Jasper Degens and Tillman Degens

For the 2021 MarketMake Hackathon, we worked to create a prototype called Whiskey MarketMaker, a platform that connects whiskey enthusiasts to craft distilleries. Our family runs Stone Barn Brandyworks, a craft distillery in Portland, Oregon, and as we were learning more about the DeFi space, we realized that aging whiskey in barrels and interest-bearing accounts through a platform like Aave share a lot in common: in both cases, you deposit an asset and its value grows over time—each cask of whiskey is essentially an interest-bearing barrel. We came up with the idea of allowing investors to buy whiskey early in the maturation process, while at the same time providing immediate, accessible funds to small batch distilleries. In short, the platform swaps financial liquidity for potable liquidity.

How Whiskey MarketMaker integrates smart contracts into product design
How Whiskey Market Maker connects whiskey enthusiasts and craft distilleries for on-chain yield-bearing whiskey bottles.

An essential quality we wanted our prototype to have was an easy onboarding process for distilleries. To us, this meant that distilleries should focus on what they do best (making incredible whiskey) and have as little friction as possible when selling their products. Distilleries determine material, product, and bottle costs in fiat currency and need to comply with strict government taxation and regulation. Therefore, keeping pricing consistent and in a fiat currency on the platform was a necessity, with Chainlink Price Feeds providing conversion to ETH when required.

Converting Prices in DeFi Platforms

Since we are based in the United States, we decided to set and list prices in USD. To implement an on-chain USD-based platform, there are two methods we considered: the first would be to accept stablecoins such as USDC or DAI. A drawback here is that a stablecoin model would require a buyer to own multiple assets: ETH to pay for the gas fees and a stablecoin to purchase the tokenized whiskey. Each transaction would require ERC-20 approval and transfer steps to buy bottles. For us, it made more sense to handle all payments in ETH and convert between ETH and USD with the Chainlink ETH/USD Price Feed. This way, distilleries could set their prices in USD like usual and choose to receive off-ramped payments in USD or, if interested, in ETH.

To convert between USD and ETH, we needed access to a trustworthy, accurate, and tamper-proof price feed. Inaccurate or manipulable data feeds could lead to serious imbalances when exchanging funds for whiskey. If our exchange rate undervalued USD, then distilleries would not receive the true value of their product, and if ETH was priced too low, customers would be overpaying for their whiskey. We needed reliable exchange rates that reflected broad market coverage to guarantee an honest trade. This is where Chainlink came to the rescue with decentralized price data.

The first stop on any integration journey was to head to the Chainlink documentation. Our use case was extremely simple, as we just wanted to get the latest price from the ETH/USD price feed. For our platform, we needed to handle conversions for both our smart contract and our frontend: checking that enough ETH was sent when purchasing bottles of whiskey in our smart contract and making sure the user also sends the correct amount of ETH when purchasing through our website. The Chainlink docs kindly provide examples for both Solidity and JavaScript (also Python for you Pythoners).

Building the Backend

We designed our network of smart contracts as a small duet: we have the WhiskeyPlatformV1 contract that stores pricing data and handles purchasing and verification logic, and our BarrelHouse contract stores and manages the ownership of ERC1155 tokens that represent bottles of whiskey. We chose to separate the two components so that if and when we release a new platform, we could keep the data layer with the ownership of existing tokens unaffected.

We integrated the Chainlink ETH/USD Price feed with our WhiskeyPlatformV1 contract. When a distillery creates a new barrel listing, they specify the following details in USD: start price, matured price, and fees (which cover barrel storage costs) for each bottle. For each purchase, we calculate the current bottle price based on a linear interpolation between the start and matured price using the purchase date, and then add in the storage fee costs. So our price for the customer is as follows:

// clamp date to avoid overflow
uint256 startTimestamp = barrel.startTimestamp;
uint256 endTimestamp = barrel.endTimestamp;
uint256 clampedDate = Math.max(startTimestamp, Math.min(endTimestamp, targetDate));
uint256 totalAgingDuration = endTimestamp - startTimestamp;
uint256 elapsedDuration = clampedDate - startTimestamp;
 
uint32 startPrice = barrel.startPriceUsd;
uint32 endPrice = barrel.endPriceUsd;
uint32 priceRange = endPrice - startPrice;
uint32 additionalPrice = uint32(uint256(priceRange).mul(elapsedDuration).div(totalAgingDuration));
 
uint32 currBottlePrice = barrel.startPriceUsd + additionalPrice;
uint256 totalPriceUSD = currBottlePrice * numberOfBottlesToPurchase;

That is the USD value we need to convert to ETH with the Price Feed. Before we query the Price Feed contract, we needed to get the right address for our currency pair. The docs list contract addresses for an extensive collection of price pairs on many different networks. We used the Kovan testnet and the ETH/USD Aggregator, so our target address was 0x9326BFA02ADD2366b30bacB125260Af641031331. Thus, our next step was to grab the price and convert using this Aggregator contract:

// Use ChainLink Oracle for price feed for production
AggregatorV3Interface priceFeed = AggregatorV3Interface(0x9326BFA02ADD2366b30bacB125260Af641031331);
(, int256 usdToEthRate, , , ) = priceFeed.latestRoundData();
uint256 totalPriceEth = totalPriceUSD / usdToEthRate;

However, if we try that calculation with, let’s say, totalPriceUSD as $75 and exchangeRate as 147139000000, then our total price would be 0! This is definitely not correct. Here is where we learned our first lesson when working with Solidity: you need to be very careful how decimals are represented, and you need to be aware of the order of multiplications and divisions.

Solidity does not natively support floating point arithmetic, so the most common solution to get around this limitation is to use unsigned or signed integers for amounts and then track how many decimal places are represented for the currency. Often, currencies use many decimal places, for example ETH and the stablecoin DAI use 18 decimals. As an example, a balance of 120000000000000000 on the DAI contract would have a value of 120000000000000000 / 10^18, which is around $0.12.

When working exclusively with signed and unsigned integers, you also need to consider the best way to store values that represent real-world amounts in your contracts. For us, because most distilleries refer to their prices in USD, it made the most sense to represent dollar amounts up to 2 decimal places and not fractionalized pennies. On our platform the uint “3999” would represent the USD value of $39.99.

Our next missing piece of info was to find out how many decimal places are represented in the USD/ETH Price Feed contract. If we look at the AggregatorV3Interface, there was the handy function decimals(), the same as ERC-20 currencies. So if we query that, we could correctly convert our price! The new logic was as follows.

// Use ChainLink Oracle for price feed for production
AggregatorV3Interface priceFeed = AggregatorV3Interface(0x9326BFA02ADD2366b30bacB125260Af641031331);
(, int256 usdToEthRate, , , ) = priceFeed.latestRoundData();
uint8 priceFeedDecimals = priceFeed.decimals();
uint256 totalPriceEth = totalPriceUSD / (usdToEthRate / 10^priceFeedDecimals);

If I type this calculation into my calculator, we get a price of 0.02 ETH. But remember, we are in an environment that does not have floating point numbers, and because integers round down, this would also actually equal 0—wrong again! We needed to go back to the drawing board.

For humans, it’s great to think about prices in terms of ETH, but for smart contracts it’s more useful to work in terms of wei (1 ETH = 10^18 wei). By working in wei, we can avoid the rounding issues and the resulting loss of precision. Also, values of payments on Ethereum are all in wei, so if we need to validate payments, it will be extremely useful if we are already working in that denomination. Instead of going from USD to ETH, we went from USD directly to wei. Our new formula looked like this:

uint256 usdToWei = ( 10^exchangeRateDecimals * 10^18 ) / usdToEthExchangeRate;

Note that here we first calculated the full numerator before dividing by the exchange rate in order to not lose any precision due to rounding.

For our contract, because we represented USD with 2 decimals, to convert from amounts stored on our contract, we took one additional step:

uint8 constant INTERNAL_DECIMALS = 2;
uint256 usdToWei = usdToWei / 10^INTERNAL_DECIMALS;

And now we took this exchange rate and multiplied it with our dollar amount to determine its equivalent value in wei! Price validation became super simple:

uint256 totalPriceUsd = (bottlePrice + feesPerBottle) * numBottlesToPurchase;
uint256 requiredWei = totalPriceUsd * usdToWei;
// revert transaction if not enough payment was sent
require(msg.value >= requiredWei, “Payment does not cover the price of bottles”);

Voila, we were done with smart contract payment validation!

As a note, if we use this method of converting a usdToWei> rate, we can reuse this rate if we need to perform multiple currency conversion calculations. We do lose a touch of precision (around 2e-14%) by precomputing this amount versus if we multiplied everything out first and then divided, but we could save more in gas costs by cutting down on math calculations.

Building the Frontend

When a consumer wants to buy bottles, we need to create a transaction with a value of the price in USD converted into wei. We queried the Chainlink Price Feed again via JavaScript to get the current rate. The documentation uses the web3.js library to interact with the on-chain contracts, but for our project we used ethers.js:

const priceFeed = new ethers.Contract(addr, aggregatorV3InterfaceABI, library);
const roundData = await priceFeed.latestRoundData();
const currRate = roundData.answer.toNumber();

Since we knew the transaction amount in USD, we went through the same procedure as we did on our smart contract and translated the USD/ETH exchange rate to USD/wei. Two important notes here: first, make sure you are using a big number library when doing these calculations, because JavaScript’s built-in number type hits its max value at 2^53 – 1 (and in Solidity we can represent values up to 2^256-1 in a uint256). Also, remember that you cannot use decimals in numbers, so if your price is in the format of $55.25, your big number library will error. Depending on the decimal places needed for your dApp (as I described before, we use 2 decimal places), you need to expand out by that order of magnitude and at the end divide by that same order. The order of magnitude should be consistent with your smart contracts, otherwise you could have a difference in precision which could lead to reverted transactions.

const chainlinkDecimals = await priceFeed.decimals();
// used to expand out decimal places
const internalDecimals = 2;
const usdToWei = ethers.constants.WeiPerEther.mul(chainlinkDecimals).div(ethToUsdRate);
const priceInWei = usdToWei.mul(Math.floor(props.totalPriceUsd * 10**internalDecimals)).div(10**internalDecimals);

After this, we used the priceInWei as the value of our transaction using ethers.js:

const trx = await whiskeyPlatform.purchaseBottles(props.tokenId, props.numBottles, {value: priceInWei.toString()});

Just like that, we sent the right wei value from the frontend and verified the amount on the backend! This process ensured that platform users do not overpay and that distilleries received the true value of their product. All parties could rest easy and pour themselves a glass.

Summary

We used the Chainlink ETH/USD Price Feed to fetch the real-world exchange rate between USD and ETH, then used that rate to calculate the wei required for payment on the frontend of our DeFi platform, and ultimately verified the payment in our smart contract. The process itself is fairly simple but can be extremely powerful when bridging real-world and digital assets, easing the burden of transactions for both the buyer and the producer. As more marketplaces transition to accepting cryptocurrencies, we believe the conversion and payment technique we developed for Whiskey MarketMaker will become more and more useful for a wide range of tokenized assets. We’re eager to see which physical goods will be tokenized next and how smart contracts and Chainlink oracles will be leveraged to help businesses both big and small.

Photo of the author Tilman Degens and a cask and bottle of Hoppin’ Eights whiskey from Stone Barn Brandy Work
Author Tillman Degens with his own cask of Hoppin’ Eights whiskey.

Learn More

If you’re a developer and want to connect your smart contract to existing data and infrastructure outside the underlying blockchain, reach out here or visit the developer documentation.

Docs | Discord | Reddit | YouTube | Telegram | Events | GitHub | Price Feeds | DeFi | VRF

Need Integration Support?
Talk to an expert
Faucets
Get testnet tokens
Read the Docs
Technical documentation