Unfortunately for TON smart contract developers, the system does not protect against spamming unbounded data structures in contract state. The TON gas fee model dictates that writes are not constant in cost, the cost is normally proportional to how much data exists in the data structure. This behavior stems from TON's reliance on the "Bag of Cells" architecture - contract state is divided to 1023 bit chunks called "cells" that the developer is required to maintain. Maps are implemented as a tree of cells, and writing to a leaf in the tree requires writing new hashes along its entire height. If an attacker were to spam keys in the map, some user balances would be pushed so low in the tree that updating them would be pass the gas limit.
The best practice in TON is therefore to avoid unbounded data structures in state. This will protect the contract from crafty DoS vulnerabilities. This topic probably deserves its own independent blog post, but the solution in short is to rely on contract sharding. If we have a potentially infinite number of user balances in our USDC contract, we should break the single contract to multiple child contracts - each child holding the balance for a single user.
This should explain why NFT
collection contracts on TON place every item in its own separate contract (the number of items can be unbounded); and why fungible token
contracts on TON place every user's balance in its own separate contract.
We normally provide a bit of the reasoning as to why TON is designed this way. The Ethereum gas fee model where map writes are fixed and independent of map size is over simplified. In reality, as maps grow in size, miners require more effort to change their contents. This extra effort is negligible as long as the maps are small, but when maps can grow to billions of entries, this is no longer the case.