Wallet V5 Extension
Wallet V5 extension mechanism: whitelisted helper contracts allowed to send messages on behalf of the wallet. The foundation for gasless transfers and on-chain subscriptions on TON.
Aliases: w5-extension, w5 plugin, wallet v5 extension, w5 helper
Wallet V5 Extension is a Wallet V5 feature introduced in 2024. The wallet stores a whitelist of extension addresses; any of them can send internal messages “as if from the wallet” without the owner’s signature.
Use cases
- Gasless USDT: the relayer is added as an extension, pays gas, keeps a small fee.
- Subscriptions: automatic recurring debit without a per-payment signature from the user.
- DApp helpers: an approved DApp can batch-send transactions on the user’s behalf.
Security
Extension == full balance access. The whitelist is managed only by the owner (signed action). Any added extension can withdraw everything — only add services you trust. Before approving in the wallet, look carefully at which address is requesting permissions.
Difference from Wallet V4 plugins
V4 plugins only supported subscription-cron; V5 extensions allow arbitrary internal messages. V4 became effectively legacy after V5 shipped; new wallets default to W5.
Storage layout: where extensions live
W5 keeps the whitelist in extensions — a HashmapE 256 int1 inside the wallet’s data cell. The key is a 256-bit hash of the address (workchain + address hash), the value is a flag of 1 (true marker for presence). Reads look like:
(int found, slice _) = extensions~udict_get?(256, ext_hash);
throw_unless(146, found); ;; "Extension not whitelisted"
One entry is ~265 bits → about $0.0001/year in forward-fees per extension. There’s no hard cap on extension count, but in practice more than 20 is unusual.
Adding and removing — the API
In the @ton/ton TypeScript client (W5-aware) the operations look like:
import { WalletContractV5R1 } from '@ton/ton';
const wallet = client.open(WalletContractV5R1.create({ publicKey, walletId }));
// Add an extension
await wallet.sendAddExtension({
seqno: await wallet.getSeqno(),
secretKey,
extensionAddress: Address.parse('EQrelayer...'),
});
// Remove
await wallet.sendRemoveExtension({
seqno: await wallet.getSeqno(),
secretKey,
extensionAddress: Address.parse('EQrelayer...'),
});
Both calls require a signed action from the owner. Internally they are encoded as opcodes add_extension (0x02) / remove_extension (0x03) in the W5 action-list format.
What an extension can and cannot do
| Operation | Allowed for extension | Allowed for signed action |
|---|---|---|
| Send internal messages with send_mode 0/1/2/3 | ✅ | ✅ |
| Add other extensions | ❌ (default) | ✅ |
| Remove the signing key | ❌ | ✅ |
Change is_signature_allowed (signed-mode lock) | ❌ | ✅ |
Perform set_data directly | ❌ | ❌ |
This is “second-class citizenship” — an extension can’t become a new owner, but it can drain the entire balance in a single message. The distinction matters for risk reviews.
Real-world deployments 2024-2026
- Tonkeeper Battery — a Tonkeeper-built extension that pays gas for jetton transfers from a pre-funded “battery” token balance.
- TONX Sponsor — gasless USDT provider, attached as an extension on the user’s first transfer.
- Subscription protocols — TonsClub, Subby (in alpha as of 2026) — extension with a rate-limited debit cap.
- Hot-wallet routing for exchanges — an exchange can add a cold-storage extension and route batch withdrawals without signing every operation.
Removal best-practice
If you stop using a dApp that holds an extension — remove it explicitly. Adapter-level bugs in extension logic can lead to drain attempts even a year after the last legitimate use. Tonkeeper shows active extensions in Settings → Wallet → Extensions; a 5-minute audit once per quarter is cheap insurance.
Related
- Wallet V5 — overall W5 architecture.
- Gasless transfer — the primary extension use-case.
- Internal message — what an extension physically sends.