Blog · · 4 min read
How on-chain chat works
Most chat applications are a database with a web page in front of it. Flashy Chat is a smart contract with a web page in front of it, and the difference shows up everywhere.
A message is an event, not a row
Solidity contracts can emit events. An event is not stored in the contract's state — it is written into the transaction's log, which becomes part of the block, which becomes part of the chain. Every node keeps it. Nobody can rewrite it.
Sending a message calls a function that emits:
MessageSent(address sender, uint8 msgType, string content)
msgType is 0 for text and 1 for an IPFS image. That is the entire storage
layer. There is no messages table, no primary key, no delete endpoint. The
contract owner has more privileges than you in several respects — they can
change fees and add tokens — but removing a message is not among them, because
the contract has no code that could do it and the log is not the contract's to
edit anyway.
The frontend reads history by asking a block explorer for the contract's past logs, with a direct RPC call as a fallback, and subscribes to new ones so the chat updates without a refresh.
Images go somewhere else
Putting an image on-chain would be absurdly expensive — storage is priced per 32-byte word, and a photograph is millions of them.
So images take a different path. The file is uploaded to IPFS, which
addresses content by its hash rather than by location: the identifier of a file
is a fingerprint of its bytes. What gets stored on-chain is that identifier,
a short ipfs://… string, in the same content field a text message would
use.
This gives you the property that matters. The chain does not hold the image, but it holds a hash that only that exact image can produce. If someone serves you a different picture under the same identifier, it does not match, and you can tell. The chat renders images through an IPFS gateway, and the message stays verifiable whether or not any particular gateway is up.
Accepted formats are JPEG, PNG, GIF and WebP, up to 5 MB.
The flash loans
Everything above would work without borrowing anything. The flash loans are the point of the project rather than a requirement of the chat.
When you send a message, the contract calls Morpho's flash loan function once
per supported token, requesting Morpho's entire balance of it. Morpho sends the
tokens and then calls back into the Flashy Chat contract, which is where the
interesting part happens: the MessageSent event is emitted inside that
callback, at the moment every borrowed token is held. Then the tokens are
returned and Morpho's check passes.
Two safety properties are worth naming, because a callback that anyone could invoke would be a serious hole:
- The callback verifies that the caller is Morpho. Nothing else can trigger it.
- The whole path is protected by OpenZeppelin's
ReentrancyGuard, so the callback cannot be used to re-enter the contract mid-flight.
Token transfers use SafeERC20, which matters more than it sounds: several
widely used tokens do not return a boolean from transfer the way the ERC-20
specification says they should, and naive code either reverts on them or, worse,
ignores a failure.
Why sending costs money
There is no moderator, no account, and nothing to ban. A username is an Ethereum address, and anyone can make a new one for free.
That removes the usual lever against spam, so the remaining one is price. Text messages cost 0.02 ETH, image messages 0.04 ETH, and a custom ad 0.01 ETH. A plain self-ad — one that promotes flashy.chat rather than your own link — is free apart from gas.
If you send more ETH than the fee, the excess is refunded inside the same transaction. You are never charged more than the stated price, and the contract does not quietly keep the difference.
Fees are adjustable by the owner through setMessageFee, setMediaFee and
setAdFee, and every change emits its own event, so the fee history is as
public as the message history.
Reading the state without a server
The contract exposes two view functions that the site leans on heavily:
getStats()returns the total number of messages, the total number of ads, and the cumulative amount ever flash-loaned per token — in one call.getFlashableAmounts()returns what the next transaction would borrow.
The live numbers on the homepage are those two calls and nothing else. There is no analytics pipeline behind them, and no figure on the site that you cannot reproduce by querying the contract yourself.
What you are trusting
Being honest about the trust model is more useful than claiming there is none.
You are trusting that the deployed bytecode matches the published source — which you can check, because the contracts are verified on Etherscan, Basescan, Blockscout and Sourcify. You are trusting the owner not to raise fees between you reading this and you sending a message, though a change would be visible on-chain. And you are trusting Morpho, whose pool the loans come from.
What you are not trusting is that anyone will keep your messages around. That part is not a promise. It is a property of where they are stored.
Sources
Last checked: