Skip to main content

Explore assets and transfers

Transferring assets and tokens is crucial in dapp development. This example demonstrates building a simple English Auction in both Solidity and Rell, Chromia's language.

Solidity

Our initial example is presented in Solidity, featuring a streamlined version of an auction. It's important to note that this example primarily focuses on the core logic of the auction process. In a comprehensive, fully-functional auction smart contract, additional functions and checks are typically required. However, the purpose of this guide is to emphasize the essential operations and demonstrate how they can be effectively translated into Chromia's Rell language.

We start by defining a struct to hold our auctions within the contract. Additionally, our model uses event emission to persist the history of bids and claims. Using events instead of storing the entire history within the contract helps minimize gas fees. This approach is particularly beneficial for managing the costs associated with executing smart contracts on the blockchain. As we will discover later, this is not the case for Chromia.

Solidity
struct Auction {
uint256 index; // Auction Index
address addressNFTCollection; // Address of the ERC721 NFT Collection contract
address addressPaymentToken; // Address of the ERC20 Payment Token contract
uint256 nftId; // NFT Id
address creator; // Creator of the Auction
address payable currentBidOwner; // Address of the highest bidder
uint256 currentBidPrice; // Current highest bid for the auction
uint256 endAuction; // Timestamp for the end day & time of the auction
uint256 bidCount; // Number of bids placed on the auction
}

Auction[] private allAuctions;

Below is the full implementation of the Auction in Solidity. We will delve into the details of each part in the upcoming sections.

Full example in Solidity

Solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.9;

import "./ERC20.sol";
import "./NFTCollection.sol";
import "@openzeppelin/contracts/token/ERC721/IERC721Receiver.sol";

contract Marketplace is IERC721Receiver {
string public name;
uint256 public index = 0;

struct Auction {
uint256 index; // Auction Index
address addressNFTCollection; // Address of the ERC721 NFT Collection contract
address addressPaymentToken; // Address of the ERC20 Payment Token contract
uint256 nftId; // NFT Id
address creator; // Creator of the Auction
address payable currentBidOwner; // Address of the highest bidder
uint256 currentBidPrice; // Current highest bid for the auction
uint256 endAuction; // Timestamp for the end date & time of the auction
uint256 bidCount; // Number of bids placed on the auction
}

Auction[] private allAuctions;

event NewBidOnAuction(uint256 auctionIndex, uint256 newBid);
event NFTClaimed(uint256 auctionIndex, uint256 nftId, address claimedBy);
event TokensClaimed(uint256 auctionIndex, uint256 nftId, address claimedBy);
event NFTRefunded(uint256 auctionIndex, uint256 nftId, address claimedBy);

constructor(string memory _name) {
name = _name;
}

function isContract(address _addr) private view returns (bool) {
uint256 size;
assembly {
size := extcodesize(_addr)
}
return size > 0;
}

function createAuction(
address _addressNFTCollection,
address _addressPaymentToken,
uint256 _nftId,
uint256 _initialBid,
uint256 _endAuction
) external returns (uint256) {

require(isContract(_addressNFTCollection), "Invalid NFT Collection contract address");
require(isContract(_addressPaymentToken), "Invalid Payment Token contract address");
require(_endAuction > block.timestamp, "Invalid end date for auction");
require(_initialBid > 0, "Invalid initial bid price");

NFTCollection nftCollection = NFTCollection(_addressNFTCollection);
require(nftCollection.ownerOf(_nftId) == msg.sender, "Caller is not the owner of the NFT");
require(nftCollection.getApproved(_nftId) == address(this), "Require NFT ownership transfer approval");
require(nftCollection.transferNFTFrom(msg.sender, address(this), _nftId), "NFT transfer failed");

address payable currentBidOwner = payable(address(0));

Auction memory newAuction = Auction({
index: index,
addressNFTCollection: _addressNFTCollection,
addressPaymentToken: _addressPaymentToken,
nftId: _nftId,
creator: msg.sender,
currentBidOwner: currentBidOwner,
currentBidPrice: _initialBid,
endAuction: _endAuction,
bidCount: 0
});

allAuctions.push(newAuction);
index++;

return index;
}


function isOpen(uint256 _auctionIndex) public view returns (bool) {
Auction storage auction = allAuctions[_auctionIndex];
return block.timestamp < auction.endAuction;
}

function bid(uint256 _auctionIndex, uint256 _newBid) external returns (bool) {
require(_auctionIndex < allAuctions.length, "Invalid auction index");
Auction storage auction = allAuctions[_auctionIndex];

require(isOpen(_auctionIndex), "Auction is not open");
require(_newBid > auction.currentBidPrice, "New bid price must be higher than the current bid");
require(msg.sender != auction.creator, "Creator of the auction cannot place new bid");

ERC20 paymentToken = ERC20(auction.addressPaymentToken);

require(paymentToken.transferFrom(msg.sender, address(this), _newBid), "Transfer of token failed");

if (auction.bidCount > 0) {
paymentToken.transfer(auction.currentBidOwner, auction.currentBidPrice);
}

auction.currentBidOwner = payable(msg.sender);
auction.currentBidPrice = _newBid;
auction.bidCount++;

emit NewBidOnAuction(_auctionIndex, _newBid);

return true;
}

function endAuction(uint256 _auctionIndex) external {
require(_auctionIndex < allAuctions.length, "Invalid auction index");
require(!isOpen(_auctionIndex), "Auction is still open");

Auction storage auction = allAuctions[_auctionIndex];

require(
msg.sender == auction.creator || msg.sender == auction.currentBidOwner,
"Only creator or winner can end the auction"
);

NFTCollection nftCollection = NFTCollection(auction.addressNFTCollection);

require(
nftCollection.transferNFTFrom(address(this), auction.currentBidOwner, auction.nftId),
"NFT transfer failed"
);

ERC20 paymentToken = ERC20(auction.addressPaymentToken);

require(
paymentToken.transfer(auction.creator, auction.currentBidPrice),
"Token transfer failed"
);

emit NFTClaimed(_auctionIndex, auction.nftId, auction.currentBidOwner);
}

function onERC721Received(
address,
address,
uint256,
bytes memory
) public virtual override returns (bytes4) {
return this.onERC721Received.selector;
}
}

Rell

Implementing the same example in Rell involves a more model-driven approach. We create a data model consisting of the entities needed, setting up a model that simplifies working with the auction logic and future extensions or feature additions.

Our model consists of:

  • auction representing our auction with all properties needed to manage the entire auction life cycle.
  • bid representing a bid from a participant in the auction.
  • artwork_nft our NFT model.
Rell
entity auction {
key id: byte_array = op_context.transaction.tx_rid;
key nft: artwork_nft;
seller: account;
mutable started: boolean;
mutable end_at: timestamp;
mutable ended: boolean;
mutable highest_bid: integer = 0;
mutable highest_bid_owner: account;
payment_token: asset;
}

entity bid {
auction: auction;
bid: integer;
bidder: account;
index auction, bid;
}

entity artwork_nft {
key id: byte_array = op_context.transaction.tx_rid;
artwork_url: text;
mutable owner: account;
index owner;
}

In this example, we have already set up an NFT in our model. Like in the Solidity example, we assume the NFT has been minted, and there are minted tokens used as payment tokens for the auction. Setting up a token to use as currency is simple using Chromia FT4, and you can read more about this process here. Registering a new NFT can be done using a mint operation, as shown below:

Rell
operation mint(artwork_url: text) {
val account = auth.authenticate();
create artwork_nft( .artwork_url = artwork_url, .owner = account );
}

Now we move on to the logic of the dapp. Below, we present the full solution and then examine the central parts step by step.

Full example in Chromia Rell

Rell
operation create_auction(nft_id: byte_array, starting_bid: integer, payment_token_id: byte_array, end_at: integer) {
val account = auth.authenticate();

val nft = require(artwork_nft @? { .id == nft_id }, "NFT missing");
val payment_token = require(asset @? { .id == payment_token_id }, "Payment token asset missing");

require(nft.owner == account, "You are not the owner of this NFT");
require(starting_bid > 0, "Invalid initial bid price");
require(end_at > op_context.transaction.tx_time, "Invalid end date for auction");

// Transfer NFT to auction account
nft.owner = account @ { .id == auction_meta.auction_account };

create auction (
.nft = nft,
.seller = account,
.started = true,
.ended = false,
.end_at = end_at,
.highest_bid = starting_bid,
.highest_bid_owner = account,
.payment_token = payment_token
);
}


operation bid(auction_id: byte_array, bid: integer) {
val account = auth.authenticate();
val auction = require(auction @? { .id == auction_id }, "Auction not found");
val auction_account = account @ { .id == auction_meta.auction_account };

require(auction.started, "Auction has not started yet");
require(not auction.ended, "Auction has already ended");
require(bid > auction.highest_bid, "Bid must be higher than the current highest bid");
require(account != auction.seller, "The auction creator cannot place a bid");

transfer(account, auction_account, auction.payment_token, bid);
transfer(auction_account, auction.highest_bid_owner, auction.payment_token, auction.highest_bid);

auction.highest_bid = bid;
auction.highest_bid_owner = account;

create bid ( .auction = auction, .bid = bid, .bidder = account );
}

operation __begin_block(height: integer) {
val auctions = auction @* { .started and not .ended and .end_at <= op_context.last_block_time };
for (auction in auctions) {
end_auction(auction.id);
}
}

function end_auction(auction_id: byte_array) {
val auction = require(auction @? { .id == auction_id }, "Auction not found");
val auction_account = account @ { .id == auction_meta.auction_account };

require(auction.started, "Auction has not started yet");
require(not auction.ended, "Auction has already ended");
require(op_context.last_block_time >= auction.end_at, "Auction has not ended yet");

auction.ended = true;
auction.nft.owner = auction.highest_bid_owner;
transfer(auction_account, auction.seller, auction.payment_token, auction.highest_bid);
}

Create auction

To create and initialize a new auction, we must store key information such as the NFT up for auction, the highest bidder, and the highest bid.

Rell
operation create_auction(nft_id: byte_array, starting_bid: integer, payment_token_id: byte_array, end_at: integer) {
val account = auth.authenticate();

val nft = require(artwork_nft @? { .id == nft_id }, "NFT missing");
val payment_token = require(asset @? { .id == payment_token_id }, "Payment token asset missing");

require(nft.owner == account, "You are not the owner of this NFT");
require(starting_bid > 0, "Invalid initial bid price");
require(end_at > op_context.transaction.tx_time, "Invalid end date for auction");

// Transfer NFT to auction account
nft.owner = account @ { .id == auction_meta.auction_account };

create auction (
.nft = nft,
.seller = account,
.started = true,
.ended = false,
.end_at = end_at,
.highest_bid = starting_bid,
.highest_bid_owner = account,
.payment_token = payment_token
);
}

In the Rell code, we start by authenticating the user who is creating the auction and retrieving their account information:

Rell
val account = auth.authenticate();

Next, we fetch the NFT that will be auctioned:

Rell
val nft = require(artwork_nft @? { .id == nft_id }, "NFT missing");

We verify that the user creating the auction is the actual owner of the NFT:

Rell
require(nft.owner == account, "You are not the owner of this NFT");

To handle bids and final payments, we require a payment token for the auction:

Rell
val payment_token = require(asset @? { .id == payment_token_id }, "Payment token asset missing");

We add relevant checks:

Rell
require(starting_bid > 0, "Invalid initial bid price");
require(end_at > op_context.transaction.tx_time, "Invalid end date for auction");

auction_meta.auction_account represents a special account used to handle the assets (NFTs and payment tokens) involved in the auction process. This account is set up during the initialization of the app using Chromia FT4. Detailed information can be found here:

Rell
nft.owner = account @ { .id == auction_meta.auction_account };

Finally, we create the auction entity and supply all necessary data:

Rell
create auction (
.nft = nft,
.seller = account,
.started = true,
.ended = false,
.end_at = end_at,
.highest_bid = starting_bid,
.highest_bid_owner = account,
.payment_token = payment_token
);

To read more about account creation you can follow this link.

Add bids

Once the auction is created and started, participants can begin placing their bids. The Rell example is structured and typed, making it straightforward to follow and understand the logic.

Rell
operation bid(auction_id: byte_array, bid: integer) {
val account = auth.authenticate();
val auction = require(auction @? { .id == auction_id }, "Auction not found");
val auction_account = account @ { .id == auction_meta.auction_account };

require(auction.started, "Auction has not started yet");
require(not auction.ended, "Auction has already ended");
require(bid > auction.highest_bid, "Bid must be higher than the current highest bid");
require(account != auction.seller, "The auction creator cannot place a bid");

transfer(account, auction_account, auction.payment_token, bid);
transfer(auction_account, auction.highest_bid_owner, auction.payment_token, auction.highest_bid);

auction.highest_bid = bid;
auction.highest_bid_owner = account;

create bid ( .auction = auction, .bid = bid, .bidder = account );
}

In the Rell code, we fetch the necessary entities to place a bid:

  • account: The bidder's account
  • auction: The auction where the bid is being placed
  • auction_account: Used to transfer payment tokens and NFTs to and from the auction

Next, we perform relevant checks:

Rell
require(auction.started, "Auction has not started yet");
require(not auction.ended, "Auction has already ended");
require(bid > auction.highest_bid, "Bid must be higher than the current highest bid");
require(account != auction.seller, "The auction creator cannot place a bid");

Once all checks pass, we can create the bid and transfer the payment tokens.

First, we transfer the new bid to the auction_account to lock the bid to the auction:

Rell
transfer(account, auction_account, auction.payment_token, bid); 

Then, we transfer the previous highest bid back to the highest bidder:

Rell
transfer(auction_account, auction.highest_bid_owner, auction.payment_token, auction.highest_bid);

Finally, we update the auction with the current highest bidder and their bid, and create the bid in the dapp state:

Rell
auction.highest_bid = bid;
auction.highest_bid_owner = account;

create bid ( .auction = auction, .bid = bid, .bidder = account );

End auction

The final step of the auction process occurs when the auction reaches its end time, and the account holding the highest bid wins the auction. Ending the auction in Solidity requires a manual trigger, which means sending a transaction to the endAuction function. This action incurs a fee.

Solidity
function endAuction(uint256 _auctionIndex) external {
require(_auctionIndex < allAuctions.length, "Invalid auction index");
require(!isOpen(_auctionIndex), "Auction is still open");

Auction storage auction = allAuctions[_auctionIndex];

require(
msg.sender == auction.creator || msg.sender == auction.currentBidOwner,
"Only creator or winner can end the auction"
);

NFTCollection nftCollection = NFTCollection(auction.addressNFTCollection);

require(
nftCollection.transferNFTFrom(address(this), auction.currentBidOwner, auction.nftId),
"NFT transfer failed"
);

ERC20 paymentToken = ERC20(auction.addressPaymentToken);

require(
paymentToken.transfer(auction.creator, auction.currentBidPrice),
"Token transfer failed"
);

emit NFTClaimed(_auctionIndex, auction.nftId, auction.currentBidOwner);
}

In Rell, we can automate the process using the __begin_block() function, which is called every time a new block is created. This automates the process of ending the auction as soon as the auction time has expired, automatically paying the seller the highest bid and transferring the NFT to the highest bidder.

Rell
operation __begin_block(height: integer) {
val auctions = auction @* { .started and not .ended and .end_at <= op_context.last_block_time };
for (auction in auctions) {
end(auction.id);
}
}

function end_auction(auction_id: byte_array) {
val auction = require(auction @? { .id == auction_id }, "Auction not found");
val auction_account = account @ { .id == auction_meta.auction_account };

require(auction.started, "Auction has not started yet");
require(not auction.ended, "Auction has already ended");
require(op_context.last_block_time >= auction.end_at, "Auction has not ended yet");

auction.ended = true;
auction.nft.owner = auction.highest_bid_owner;
transfer(auction_account, auction.seller, auction.payment_token, auction.highest_bid);
}

This concludes our example of an English Auction on both Solidity and Rell. Both code bases are very similar making it easy to start using Rell if you have previously used Solidity. A couple of key takeaways and advantages when you make this transition are:

  1. On-chain Storage: Using Chromia and Rell, there is no need to rely on emitting events to keep the bidding history, as all data is stored on-chain.
  2. Automation: __begin_block() helps us automate the auction and eliminates the need to actively user triggering functions.
  3. Modern Syntax: Writing code in Rell is simpler due to its modern, structured, and lightweight syntax, making it easier to transition from Solidity.