# Explore assets and transfers · Chromia for EVM developers · Learn Chromia

> Moving assets and tokens between accounts is a core task in dapp development. This example builds a simple English auction twice: once in Solidity and once…

Source: https://chromia.com/learn/chromia-for-evm-developers/asset-transfer/

Chromia for EVM developersLesson 2 of 8

1. [01Chromia for EVM developers](https://chromia.com/learn/chromia-for-evm-developers/introduction/)
2. [02Explore assets and transfers](https://chromia.com/learn/chromia-for-evm-developers/asset-transfer/)
3. [03Chromia dapp overview](https://chromia.com/learn/chromia-for-evm-developers/chromia-stack/)
4. [04Authentication](https://chromia.com/learn/chromia-for-evm-developers/compare-authentication/)
5. [05Core concepts](https://chromia.com/learn/chromia-for-evm-developers/concepts/)
6. [06Create a news feed dapp](https://chromia.com/learn/chromia-for-evm-developers/news-feed/)
7. [07Extend news feed dapp](https://chromia.com/learn/chromia-for-evm-developers/news-feed-follower/)
8. [08Summary](https://chromia.com/learn/chromia-for-evm-developers/summary/)

[Learn](https://chromia.com/learn/)[Chromia for EVM developers](https://chromia.com/learn/chromia-for-evm-developers/)

02 / 08

# Explore assets and transfers

Moving assets and tokens between accounts is a core task in dapp development. This example builds a simple English auction twice: once in Solidity and once in Rell, Chromia's language.

## Solidity

The Solidity version is a reduced auction that covers only the core logic. A complete, production-ready auction contract would normally need more functions and checks. The goal here is to isolate the essential operations so you can see how each one maps to Rell.

The contract first defines a struct that stores the auctions. It also emits events to record the history of bids and claims. Keeping that history in events rather than in contract storage reduces gas fees, which helps control the cost of running smart contracts on the blockchain. As you will see later, Chromia does not have this constraint.

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;
```

The complete Solidity implementation is below. The following sections walk through each part.

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

The Rell version takes a model-driven approach. You define a data model made up of the required entities, which makes the auction logic easier to work with and easier to extend with new features later.

The model contains these entities:

* `auction`: holds every property needed to manage an auction through its whole life cycle.
* `bid`: a single bid placed by an auction participant.
* `artwork_nft`: the 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;
}
```

The model already includes an NFT. As in the Solidity example, it assumes the NFT is already minted and that minted tokens exist to serve as the auction's payment currency. Chromia FT4 makes it simple to set up a token as currency; see [registering assets in FT4](https://docs.chromia.com/build/ft4/backend/assets/register-assets/) for details. To register a new NFT, use a mint operation like the one below:

Rell

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

Next comes the dapp logic. The complete solution is shown first, followed by a step-by-step look at its main parts.

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

Creating and initializing an auction means storing its key data: the NFT being sold, 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
    );
}
```

Solidity

```
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;
}
```

The Rell operation first authenticates the user creating the auction and loads their account:

Rell

```nolines
val account = auth.authenticate();
```

It then loads the NFT to be auctioned:

Rell

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

It confirms that the user creating the auction owns the NFT:

Rell

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

The auction needs a payment token for bids and the final payment:

Rell

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

The relevant checks follow:

Rell

```nolines
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` is a dedicated account that holds the assets (NFTs and payment tokens) involved in an auction. Chromia FT4 creates it when the app initializes. See the [FT4 accounts overview](https://docs.chromia.com/build/ft4/backend/accounts/overview/) for details:

Rell

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

Last, the operation creates the auction entity with all required 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
);
```

For more on creating accounts, see the [FT4 accounts documentation](https://docs.chromia.com/build/ft4/backend/accounts/overview/).

## Add bids

After an auction is created and running, participants can place bids. Because the Rell code is structured and typed, the logic is easy to follow.

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 );
}
```

Solidity

```
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;
}
```

To place a bid, the Rell code loads these entities:

* `account`: the account of the bidder
* `auction`: the auction receiving the bid
* `auction_account`: moves payment tokens and NFTs into and out of the auction

It then runs the 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");
```

If every check passes, the code creates the bid and moves the payment tokens.

The new bid is transferred to the `auction_account`, which locks it to the auction:

Rell

```nolines
transfer(account, auction_account, auction.payment_token, bid);
```

The previous highest bid is returned to its bidder:

Rell

```nolines
transfer(auction_account, auction.highest_bid_owner, auction.payment_token, auction.highest_bid);
```

Finally, the auction is updated with the new highest bidder and bid, and the bid is written to the dapp state:

Rell

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

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

## End auction

An auction finishes when its end time arrives, and the account holding the highest bid wins. In Solidity, someone must trigger this manually by sending a transaction to the `endAuction` function, and that transaction costs 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, you can automate this with the `__begin_block()` function, which runs each time a new block is created. Once the auction time expires, it ends the auction automatically, pays the highest bid to the seller, and transfers 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);
}
```

That completes the English auction in both Solidity and Rell. The two code bases are close in structure, so Solidity experience carries over well to Rell. The main advantages you gain by switching are:

1. **On-chain Storage:** With Chromia and Rell, all data lives on-chain, so you do not need events to preserve the bidding history.
2. **Automation:** `__begin_block()` automates the auction, so users do not have to trigger functions themselves.
3. **Modern Syntax:** Rell's modern, structured, and lightweight syntax keeps code simple and eases the move from Solidity.

Written for this site from Chromia’s public course. Code samples are the course’s own.

[← Previous**Chromia for EVM developers**](https://chromia.com/learn/chromia-for-evm-developers/introduction/)[Next →**Chromia dapp overview**](https://chromia.com/learn/chromia-for-evm-developers/chromia-stack/)

On this page

* [Solidity](#solidity)
* [Rell](#rell)
* [Create auction](#create-auction)
* [Add bids](#add-bids)
* [End auction](#end-auction)
