Thursday, April 25, 2024
No menu items!
HomeDatabase ManagementSubsidize Ethereum blockchain transaction costs for your users

Subsidize Ethereum blockchain transaction costs for your users

Ownership is one of the core tenets of non-fungible tokens (NFTs)—you can own an NFT and prove that you own it. On blockchain, such a proof of ownership means that the NFT is in a wallet that the owner controls. Only the owner has the private key to that wallet, and only the owner can transfer tokens from the wallet. However, if a new user gets their first wallet with their freshly minted tokens, they run into a UX problem. They must own the native cryptocurrency of that blockchain to pay for transaction fees, limiting the set of potential end-users to those knowledgeable about cryptocurrency.

On example of NFTs in the music industry is how Global Rockstar offers digital shares in music production. An investor can invest in the production of a song and receive shares of that song. These shares bear the right of earning royalties. The holder receives revenues if the song is streamed or played on the radio. Global Rockstar distributes these revenues to the shareholders of the song each month.

In this post, we share how Global Rockstar updated their system to manage shareholdings and subsidize blockchain transaction costs. The token management solution uses Amazon API Gateway, AWS Lambda, and Amazon Managed Blockchain.

Legacy system and challenges

Until recently, Global Rockstar was using a proprietary backend to manage the shareholdings, which had two main pain points:

They couldn’t easily transfer tokens from one user to another.
Owners couldn’t trade existing shares, making them a very long-term investment. Because the copyright for a song holds for 70 years, each token accrues revenues for that timeframe.

A blockchain-based enhancement solves these problems. Shares, now called S-Music NFTs, are created as tokens on blockchain, which track current ownership. There are 1,000 tokens for each song, each representing 0.1% of the ownership. Tokens qualify the owners to royalties proportional to their token holdings.

With the records of current and historical ownership on blockchain, the existing system calculates the royalties for each investor. Transferring ownership also reduces to a simple blockchain transaction. The blockchain tracks who owned which tokens at what time. Furthermore, the token-based backend paves the way to a secondary market. With the new system, only legal hurdles for regulatory compliance remain—the technology is already in place. For an overview of Global Rockstar’s blockchain solution, watch the AWS re:Invent 2021 presentation from Christoph Straub, Global Rockstar CEO.

Global Rockstar is appealing to music lovers and wants to keep that audience. Their existing user base is not necessarily experienced with blockchain technology. Therefore, the barrier of entry should remain low. An investor should be able to buy shares without providing their own wallet. In particular, users shouldn’t need to buy Ether first to pay for later transactions.

Within the Global Rockstar ecosystem, end-users will be able to trade existing tokens on a marketplace. After each sale, the system transfers the tokens from seller to buyer in exchange for payment. One problem remains: each transaction on the public blockchain carries a transaction fee, payable in its native cryptocurrency. To simplify operations for their users, Global Rockstar decided to subsidize some transactions. Therefore, users don’t need to buy cryptocurrency before they can do anything.

Solution overview

The main architecture on AWS is a token management service consisting of an API Gateway backed by Lambda, which is responsible for sending transactions into the blockchain, and Managed Blockchain to connect to the Ethereum mainnet. The following diagram illustrates this architecture.

End-users are using the smart contract in an indirect way. First, users sign a message and hand it to Global Rockstar. Global Rockstar then wraps the message in a transaction and posts it to blockchain. With this approach, Global Rockstar calls the smart contract and pays the transaction fees.

The pattern is useful for the secondary market. Initially, a seller puts a listing on the marketplace. A buyer signals interest. The smart contract matches seller and buyer and facilitates the trade. It transfers tokens from seller to buyer in exchange for payment. Normally, this transfer could only be initiated by the owner. On the secondary market however, the market itself has to transfer them. Such a transfer by a third party (the marketplace) on the owner’s behalf needs explicit permissions. The marketplace smart contract must be approved.

Approval functions

The default approval function is setApprovalForAll(<operator_account>, true). It’s part of the ERC-1155 standard. A token owner can call it to approve an operator to transfer tokens on the owner’s behalf. (The owner can revoke the permission by setting the second parameter to false.) The token owner has to pay for the transaction, because they’re sending it to blockchain.

On the secondary market, the owner wants to approve the marketplace itself to transfer the tokens. Sellers have to approve it for token transfers. They can call setApprovalForAll(<marketplace>, true) on the token contract and pay the transaction fees.

Global Rockstar doesn’t want to require users to fund their wallet with cryptocurrencies first. This presents a problem. A user can’t send an approval transaction without funds in their wallet. Without the approval transaction, Global Rockstar’s marketplace can’t transfer tokens on the user’s behalf.

Because of the missing funds in users’ wallets, we can’t approve the marketplace directly. Instead, we use an indirect approach. With a new setPermitForAll(<owner>, <operator>, true, <signature>) function, we realize free approvals for the users. The function takes additional parameters:

owner – An intermediary sends the transaction to the blockchain and not the original owner. Therefore, we need to set the owner explicitly.
signature – With direct approvals, the signed transaction proves that the owner is the actual owner of the tokens. Only the tokens’ owner has access to the private key, and only they can sign the transaction. With the indirect approach, an intermediary sends the transaction, so we can’t use the transaction signature to verify the token owner. Instead, the transaction carries a signed message from the owner as payload. The signature on the message serves as a proof of ownership. In contrast to direct approvals, it’s not the transaction signature itself.

With these parameters, we can use setPermitForAll. The smart contract has to verify the signature and then set the correct approvals. The indirect approach follows three steps:

Message signature – The user creates an approval message. It effectively says, “The marketplace may transfer tokens on my behalf.” The user signs the message with their private key and hands it to Global Rockstar’s backend. Signing doesn’t cost any transaction fees yet. The message is not yet included in the blockchain.
Message submission – Global Rockstar takes the message and calls setPermitForAll with it. They pay transaction fees as they send it to the blockchain.
Signature verification – The smart contract receives the message and processes the signature. It verifies the signature belonging to the account that is allegedly approving token transfers. The smart contract requires a valid signature. Then it approves the operator (marketplace). The operator can now transfer tokens on the owner’s (user) behalf.

This setup achieves our goal. A user can approve the marketplace to transfer tokens on their behalf. This is a prerequisite to trading on the secondary market. To simplify things for the user, Global Rockstar pays for these transactions (and only these). If a user wants to trade on the secondary market, they can do so with minimal effort.

Implementation

The main idea for implementing these so-called gasless approvals has been pioneered by the Open Gasstation Network. In a parallel attempt, MakerDAO has implemented permit for their DAI token. Our smart contract uses OpenZeppelin’s smart contract suite. They provide default contracts for the major token standards. In our case, we use an ERC-1155 token; the procedure is similar for ERC-20 or ERC-721. For ERC-20, OpenZeppelin has already provided the extension ERC20Permit. It enables a similar functionality for ERC-20 tokens.

We start with the OpenZeppelin ERC-1155 smart contract. For gasless approvals, we add a new function to the contract. It verifies a signature and then calls an internal function to update the mapping of approvals. Off-chain, a user signs a message and hands it to another party (Global Rockstar’s backend). They transmit it to blockchain. We start with signature generation, follow up with message submission, and conclude with its verification.

Message signature

In our three-step process, we start out with signing a message. This happens entirely off-chain. The smart contract only verifies that the signature is, in fact, correct. To create a signature, we use Ethereum’s eth_signTypedData_v4 call. In contrast to earlier signing methods, it can sign a message and display some useful information about it (instead of a random hex string). With it, a user has more control over what they’re actually signing. For our use case, it’s the perfect match because our message is highly structured. It follows the message format as specified in EIP712, which specifies a domain, types, and the actual values. The following is the code (in JavaScript, using ethers.js as Web3 library) for creating the message:

const { chainId } = await ethers.provider.getNetwork()
const domain = {
name: await token.uri(1),
version: ‘1’,
chainId,
verifyingContract: token.address
};

const EIP712Domain = [
{ name: “name”, type: “string” },
{ name: “version”, type: “string” },
{ name: “chainId”, type: “uint256” },
{ name: “verifyingContract”, type: “address” },
];

const types = {
EIP712Domain,
Permit: [
{ name: ‘owner’, type: ‘address’ },
{ name: ‘operator’, type: ‘address’ },
{ name: ‘approved’, type: ‘bool’ },
{ name: ‘nonce’, type: ‘uint256’ }
]
}

const nonce = await token.nonces(tokenOwner)
const values = {
owner: tokenOwner,
operator: operator,
approved: true,
nonce: nonce.toString(),
};

const typedMessage = {
types: types,
domain: domain,
primaryType: ‘Permit’,
message: values,
}

There are a few things to note here:

The message is bound to the specific smart contract on a particular chain (specified by domain.name, domain.chainId, and domain.verifyingContract).
The values bind the message to the combination of tokenOwner, operator, and approved. Additionally, there is a nonce, which the smart contract tracks for each account. The nonce provides a replay protection: the same message can’t be used twice. (This nonce is different from the nonce that each transaction has. This one is only used for the message and not for the transaction itself.)

After all this setup, typedMessage holds the clear text message. The next step is the actual signing. Ethereum’s eth_signTypedMessage_v4 helps with that:

// get the default provider
const provider = hre.ethers.provider

// sign by calling the provider
const rawSignature = await provider.send(
‘eth_signTypedData_v4’,
[tokenOwner, typedMessage]
)

var sig = ethers.utils.splitSignature(rawSignature);

After this step, sig contains the values v, r, and s, which are used in the verification function call.

Message submission

The second step is sending the transaction to the smart contract. Any account can do this; it doesn’t need to be the account that signed the message. In our case, the Global Rockstar account sends the transaction to the network. They subsidize these transactions for their users:

// setPermitForAll
await token.setPermitForAll(tokenOwner, operator, true,
sig.v, sig.r, sig.s)

The smart contract verifies the signature and updates the approval mapping if the signature is correct. The operator (Global Rockstar) has paid the transaction fees.

Signature verification

The final step is the signature verification. OpenZeppelin’s contracts manage approvals in mapping(address => mapping(address => bool)) private _operatorApprovals. The mapping takes an address as key and stores address/boolean pairs as value. For each token owner (address 1), we can query if an operator (address 2) can transfer tokens on the owner’s behalf (boolean). The mapping is private; only the contract itself can modify it. Derived contracts can’t directly change the mapping. They can use the internal function _setApprovalForAll(address owner, address operator, bool approved). It updates the mapping, but it doesn’t verify any signatures.

To add verification, we create a contract, ERC1155Permit, which extends ERC1155 itself. Among the normal smart contract it adds the new function setPermitForAll:

function setPermitForAll(
address owner,
address operator,
bool approved,
uint8 v,
bytes32 r,
bytes32 s
) public virtual override {
bytes32 structHash = keccak256(
abi.encode(
_PERMIT_TYPEHASH,
owner,
operator,
approved,
_useNonce(owner)
)
);

bytes32 hash = _hashTypedDataV4(structHash);

address signer = ECDSA.recover(hash, v, r, s);
require(signer == owner,
“ERC1155Permit: invalid signature”);
require(owner != operator,
“ERC1155: setting approval status for self”);

_setApprovalForAll(owner, operator, approved);
}

The function takes the usual parameters owner, operator, and approved. Additionally, it takes a signature in the form of three values: v, r, and s. This is the default format for signatures so that the smart contract can recover the public address that signed the message. The function does two things:

It recreates the message from the parameters and an account-specific nonce. One thing worth noting it the _useNonce(owner) call. It retrieves the current account specific nonce and increases it by one. That way, the next time a different nonce value is returned. Finally, the function recovers the public address that signed the message with ECDSA.revover(hash, v, r, s).
It checks if the signature originates from the alleged owner with require(signer == owner, …). This is the main check; it verifies that the owner has in fact signed a message approving the operator. After another check, the function finally approves the operator. It modifies the approval mapping with the new values.

When the function has succeeded, the operator is approved for token transfers on the owner’s behalf. A user can always revoke the approval by calling the same function with approved set to false.

Conclusion

The pattern of using signed messages for approvals provides flexibility and lowers the barrier of entry for end-users. If you want to onboard new users without prior blockchain knowledge, you can make it easier for them by paying for some of their transaction fees. Not only is this a direct monetary value for your users, it also makes onboarding much simpler. Your users don’t need to top up their wallet with cryptocurrency before they can interact with your application. Instead, they can just start using the app.

As a developer, you have full control over which transactions you want to subsidize. For example, you could pay for approval transactions for your users, but not for transfer transactions. A user can still transfer tokens to a different wallet, but they need to pay for the transaction. The OpenZeppelin smart contracts already implement the pattern for ERC-20. In this post, we extended it to ERC-1155, which Global Rockstar is using.

Leave a comment on how you would simplify the onboarding workflow with this technique.

About the author

Christoph Niemann is a Senior Blockchain Architect with AWS Professional Services. He likes Blockchains and helps customers designing and building blockchain based solutions. If he’s not building with blockchains, he’s probably drinking coffee.

Read MoreAWS Database Blog

RELATED ARTICLES

LEAVE A REPLY

Please enter your comment!
Please enter your name here

Most Popular

Recent Comments