Using Chainlink External Adapters to connect various components of decentralized infrastructure is one of the ways the Chainlink Network is helping to make it easier for smart contract developers to build new use cases and fully decentralized applications. Chainlink Prize winners from the ETHOnline Hackaton Toshiake Takase and Tsukasa Noguchi used Chainlink's oracle infrastructure and IPFS to enable artists on Audius's blockchain-based music streaming platform to distribute token rewards to their fans without the costly gas fees associated with hundreds or thousands of individual transactions.

In this tutorial, the Iroiro team shows how to use Chainlink to connect to IPFS for cost-efficient token distribution and a variety of other Ethereum infrastructure use cases.


by Toshiake Takase and Tsukasa Noguchi

Introduction

In addition to being useful as a currency, the ERC20 token standard on Ethereum can also act as a utility token.

In this context, we focused on the usefulness of tokens as a means of communication between creators and fans and developed "Iroiro" (GitHub repository is here) as a platform to enable artistic creators to generate their own ERC20 tokens and distribute them to fans.

There are many potential use cases for this type of creator token, and the purpose of distributing these tokens to fans will vary depending on the creator. Sometimes it's simply to express gratitude, and other times it's to allow access to exclusive content or experiences such as private chat channels, livestreams, or live performance perks. The range of potential exclusive rewards is another creative space for artists to explore.

However, the current costs of transacting on Ethereum create a barrier for many transactions. The cost of sending tokens can easily outweigh any benefit that an artist may receive from added fan loyalty. In order to make a fan rewards program a scalable use case for ERC20 tokens, we need to implement some supporting infrastructure to make this a reasonable use case for creators.

In this article, we will explain the technical details of the Chainlink and IPFS integration we used to build Iroiro. The implementation can be a model for other Chainlink and IPFS use cases to make more efficient token distribution models.

The Audius API

For the ETHOnline Hackathon, we decided to use Audius to enable token distribution. Audius is a decentralized music streaming protocol.

Since Audius has user accounts and facilitates a relationship between artists and followers, we decided that, as an extension of this relationship, we could build a flow where artists create tokens and distribute them to their fans according to user accounts.

Audius provides an API, through which you can get the wallet address of the account stored in the Hedgehog wallet developed by Audius, as well as follower addresses.

Therefore, we decided to implement the following flow:

  1. The user who generates the creator token gets a list of addresses following their Audius account
  2. The creator token generator receives and stores follower address information as a snapshot on the contract and sets addresses as the token distribution targets
  3. Each follower receives a notification that there is a distribution campaign outside of the dApp, and if they are eligible for distribution, they can execute a claim function and receive a token

Smart Contracts and Large Amounts of Data

There is a problem with the implementation in this flow, however, as sometimes the number of followers is huge, especially for famous artists such as RAC. The number of followers can climb to the tens or hundreds of thousands, and as creator platforms like Audius gain adoption, this number will likely increase.

If we try to record all the addresses of the followers in an on-chain contract, it will cost a lot of ETH in gas fees, especially during times of network congestion, which would burden creators with transaction costs.

Therefore, instead of writing follower information in the contract, our implementation saves it as a file off-chain using IPFS. We use Chainlink to check whether or not the address in IPFS exists as an Audius wallet, thereby linking the contract to a large amount of data without consuming a large amount of gas when creating the campaign.

The IPFS External Adapter

About IPFS

IPFS is a distributed system for storing media files. Uploaded files are stored on a distributed network.

In Iroiro, the list of follower addresses obtained by the Audius API is stored in IPFS as an array of strings in a JSON file, so that it can be retrieved by the Chainlink IPFS external adapter.

If we simply use Chainlink here, we will encounter a problem when linking it with IPFS. This is because Chainlink's built-in HTTP GET Adapter can retrieve a value at a specific path in a JSON string but doesn't retrieve and return an entire array of values.

The JSON file to be saved and stored on IPFS contains a list of addresses in the form of an array, as per the example below. This means that for a follower to check if their own address is stored in the JSON file, a Chainlink External Adapter must be built to handle searching through the array of addresses.

{ 
  "addresses": [
    "address1", 
    "address2", 
    ...
  ]
}

The IPFS External Adapter

We developed a Chainlink External Adapter to achieve the required functionality in the issue described above.

The External Adapter is an independent application that accepts requests from Chainlink nodes and performs the necessary processing, returning results in a format that the Chainlink nodes can process.

By building an External Adapter, we can leverage Chainlink with flexible functions that cannot be achieved with a built-in Adapter alone.

The External Adapter will perform the following processes:

  • Obtain an IPFS file by using the IPFS cid (a unique key that indicates the file) received at the time of the Chainlink request from the contract.
  • Verify if the user address is stored in the field of the IPFS file to confirm token distribution targets.
  • If the target address is stored, Chainlink will fulfill the request by returning a hash of the user address, campaign address, and address storage information (Boolean) as the return value.

We will now use the code to explain the sequence of events.

Creators' Side

First, let's take a look at the flow on the creator side.

Log in to Audius on the front end. The Hedgehog Wallet allows you to log in with your email address and password.

const audiusSignIn = useCallback(async () => {
    setIsSigningIn(true)
    const email = emailRef.current.value
    const password = passwordRef.current.value
    const { user } = await libs.Account.login(email, password)
    const followers = await libs.User.getFollowersForUser(100, 0, user.user_id)

    setIsSigningIn(false)
    setAudiusAccount(user)
    setAudiusFollowers(followers)
  }, [libs])

Get the list of followers by using the Audius API
Once the login is complete, use the Audius API to get the list of followers, and then extract the wallet address list information from the user information.

const signer = await provider.getSigner()
const walletAddress = await signer.getAddress()
const factory = new Contract(addresses.TokenFactory, abis.tokenFactory, provider)
const tokenAmount = await factory.tokenAmountOf(walletAddress)

let tokenId
for(let i = 0; i < tokenAmount.toNumber(); i++) {
  const address = await factory.creatorTokenOf(walletAddress, i+1)
  const lowerAddress = address.toLowerCase()
  const lowerTokenAddress = tokenAddress.toLowerCase()
  if( lowerTokenAddress === lowerAddress ) {
    tokenId = i+1
  }
}

let walletAddresses = []
for(let i = 0; i < audiusFollowers.length; i++) {
  walletAddresses.push(audiusFollowers[i].wallet)
}

Save the extracted address list as a JSON file to IPFS.

const json = {addresses: walletAddresses}
const { path } = await ipfs.add(JSON.stringify(json))

Run a token distribution campaign using the IPFS cid
Create a token distribution campaign contract with the cid of the JSON file you uploaded to IPFS as an argument. The cid to be uploaded will be the key to the file that Chainlink will retrieve and use.

const audius = new Contract(addresses.Audius, abis.audius, signer)
const _followersNum = audiusFollowers.length

audius.addAudiusList(tokenId, path, _followersNum)

The Graph fetches events that are emitted when the TokenFactory contract creates a new token and when tokens are transferred. The front end then fetches this information using GraphQL provided by the subgraph.

Fan Side

Next, let's take a look at the flow on the fan side where Chainlink is used.

A fan sends a withdrawal request to a contract to confirm the availability of withdrawals.

function requestCheckingAddress(
    address _oracle,
    bytes32 _jobId,
    string memory userAddress,
    string memory tokenAddress,
    address token,
    uint256 fee
) public override returns (bytes32 requestId) {
    uint64 tokenId = tokenIdList[token];
    require(tokenId > 0, "Token is not registered");

    uint64 userId;
    if (userIdList[msg.sender] == 0) {
        userId = nextUserId;
        userIdList[msg.sender] = userId;
        nextUserId = nextUserId.add(1);
    } else {
        userId = userIdList[msg.sender];
    }
    string memory followersHash = followersHash[token];

    Chainlink.Request memory request = buildChainlinkRequest(_jobId, address(this), this.fulfill.selector);
    request.add("cid", followersHash);
    request.add("tokenAddress", tokenAddress);
    request.add("userAddress", userAddress);

    return sendChainlinkRequestTo(_oracle, request, fee);
}

The Chainlink node that receives the request will then use the External Adapter to obtain the IPFS information.

In this case, the job is to check whether the address of the user who submitted the request is included in the list of addresses to be distributed in the campaign and to fulfill the result to the contract.

Use the External Adapter to fulfill the result of whether the IPFS has specific information or not. The IPFS External Adapter executes the following processes and fulfills the results to the campaign contract.

  • Receive the user ID, user address, campaign ID, and cid of the target file stored in the event.
  • Obtain the IPFS file using the received cid. Check if the target user address is in the address array of the file.
  • Return a "hashed value" (explained below) of the user address, campaign address, and address storage information (Boolean) as a result.
app.post('/api', async (req, res) => {
    console.debug("request body: ", req.body)
    const cid = req.body.data.cid
    const userAddress = req.body.data.userAddress
    const tokenAddress = req.body.data.tokenAddress
    const userId = new BN(await Audius.methods.userIdList(userAddress).call())
        .mul(new BN(10).pow(new BN(21)))
    const tokenId = new BN(await Audius.methods.tokenIdList(tokenAddress).call())
        .mul(new BN(10))

    const content = await getFile(cid)

    const isClaimable = content.addresses.includes(userAddress)
    const claimKeyHash = getClaimKeyHash(userId, tokenId, isClaimable)
    console.debug("Claim key hash: ", claimKeyHash)

    return res.send({
        id: req.body.id,
        data: claimKeyHash
    })
})

const getClaimKeyHash = (userId, tokenId, isClimable) => {
    const truthyValue = isClimable ? 1 : 0;
    const truthyBN = new BN(truthyValue)
    const claimKey = userId.add(tokenId).add(truthyBN)
    console.debug("Claim key: ", claimKey.toString())

    return soliditySha3(claimKey)
}

About Hashing

In the last process, the hashed value of the combination of id and Boolean is returned as the return value of the fulfill function because at present, Chainlink request fulfillments can only accept a single value.

Even if only a Boolean value is returned in the fulfill function, it is not possible to determine which user or campaign can distribute the token.

For this reason, the user id, campaign id, and "is contained" information are combined into an unsigned integer value that is converted into a hash and stored in mapping as a single value. Then the contract side generates the hash using the same process and checks if the target hash exists in mapping.

In this way, we can confirm that the withdrawal is possible.

After fulfillment, the fan side confirms whether the user can withdraw.

Using the information from results returned and stored by the above Chainlink job, the user calls the contract to confirm whether he/she can withdraw from the contract.

If the result is found to be withdrawable, the user can actually send a claim() to execute the withdrawal and receive the creator's token.

function claim(address token) external override {
    require(isClaimable(token), "Account is not able to claim");

    followerClaimedTokens[token][msg.sender] = true;
    FanToken fanToken = FanToken(token);
    fanToken.transfer(msg.sender, distributedAmount(token));
}

The overall picture of this application is as follows.

Iroiro Architecture
Iroiro Architecture

We have achieved the above flow for distributing creator tokens, as well as the following:

Dramatic reduction in gas costs and more efficient transactions

Where a large amount of data would have required a large amount of gas, we were able to drastically reduce the cost of gas by using IPFS to store the data off-chain and retrieve it via Chainlink.

This directly leads to lower gas prices for users in Iroiro and contributes to lowering the barrier to entry for users.

Connecting to off-chain data

It was not possible for the blockchain to get the file contents on IPFS and execute the required logic, but we were able to do this using Chainlink.

This makes it possible to develop highly scalable smart contracts that use off-chain data.

Realization of a flexible token distribution method

Iroiro only used Audius during the hackathon, but it is possible to work with various platforms as long as the data to be distributed on other platforms can be obtained via an API.

Using Chainlink means that flexible distribution is now possible, not just limited to the on-chain distribution method.

Current Iroiro Development Status

Since winning the ETHOnline hackathon prize, Iroiro has launched on Ethereum mainnet, and has support to distribute tokens on xDai and Polygon.

The platform developed in the hackathon has been enhanced to allow for more flexible distribution methods, and more will be added as needed.

IPFS External Adapter

We are also working on expanding the IPFS External Adapter in parallel with Iroiro.

In the hackathon, we developed the External Adapter specifically for Iroiro, but in the future we will develop a generalized IPFS External Adapter that can be used by various contracts without limitations.

If you need a Chainlink External Adapter for a function you want to achieve, please contact us.

More on this Topic

If you’re a developer interested in using Chainlink to connect smart contracts to data from a new blockchain or any other data source, reach out here or visit the developer documentation. You can also subscribe to the Chainlink Newsletter to stay up to date with everything in the Chainlink stack.

Website | Twitter | Discord | Reddit | YouTube | Telegram | Events | GitHub | Price Feeds | DeFi