Having Users Pay RAM

A very common question I see is something along the following lines.

"I want to have users send NFTs to my contract, and store the asset IDs in a table row, but I don't want to pay the RAM. If I try to charge the user, I get an error!"

This is because you can not increase the RAM usage of another user unless they directly sign the action where the RAM is being increased. In other words, if a user signs an action on your contract, you can charge them RAM.

However, if you receive a transfer from a user, then they signed the transfer action on another contract. Not your contract. So you can not increase their RAM during that transfer.

There are way's around this. Let me show you a little trick that I use. Let's say that I want to do exactly what I said above - accept NFT deposits and store the asset IDs in a table.

That table might look something like this.

struct [[eosio::table, eosio::contract(CONTRACT_NAME)]] deposits {
  eosio::name user;
  vector<uint64_t> deposited_nfts;

  uint64_t primary_key() const { return user.value; }
};
using deposits_table = eosio::multi_index< "deposits"_n, deposits >;

If I try to add an asset ID into the deposited_nfts vector, then I will get an error along the lines of can not increase the RAM usage of another wallet. So, what I can do is add another field in the table struct, like this.

struct [[eosio::table, eosio::contract(CONTRACT_NAME)]] deposits {
  eosio::name user;
  vector<uint64_t> deposited_nfts;
  vector<uint64_t> announced_nfts;

  uint64_t primary_key() const { return user.value; }
};
using deposits_table = eosio::multi_index< "deposits"_n, deposits >;

Now I have 2 vectors. What I can do is add an action to my contract like this.

ACTION mycontract::announcenfts(const name& user, const vector<uint64_t>& asset_ids){
    require_auth( user );
    deposits_table deposits_t = deposits_table( _self, _self.value );
    auto itr = deposits_t.find( user.value );
    
    if( itr == deposits_t.end() ){
        deposits_t.emplace(user, [&](auto &row){
            row.user = user;
            row.announced_nfts = asset_ids;
        });
    } else {
        // modify the row, you get the point
    }
}

Now I have charged the user RAM for the asset IDs they are going to deposit. Then, my notification handler might look like this.

void receive_nft_transfer(name owner, name receiver, vector<uint64_t>& ids, std::string memo){
    // run your safety checks to validate the transfer, then proceed
    
    deposits_table deposits_t = deposits_table( _self, _self.value );
    auto itr = deposits_t.require_find( from.value, "user not found" );
    vector<uint64_t> existing_nfts = itr->deposited_nfts;

    for( uint64_t& nft : asset_ids ){
        check( std::find( itr->announced_nfts.begin(), itr->announced_nfts.end(), nft ) != itr->announced_nfts.end(),
            "you did not announce one of the transferred NFTs" );
        existing_nfts.push_back( nft );
    }
    
    deposits_t.modify( itr, same_payer, [&](auto &row){
        row.deposited_nfts = existing_nfts;
        row.announced_nfts = {};
    });
}

Now, I am not increasing the users RAM. That RAM was already being used to store the asset IDs that were in the announced_nfts vector. All I did was remove them from there, and move them into the deposited_nfts vector.

This circumvents any errors related to charging RAM to an unauthorized account, and avoids your contract needing to pay any of the RAM costs.

Last updated