Repository navigation
Conversation
Adds two pages under the TRON "Guides" section: - guide-usdt-vault: a TRC-4626 vault over USDT-TRON with entry/exit fees, tested on a local TRON node (TRE) and deployed to Nile. Teaches the TVM divergences at the point they bite: USDT's transfer returning false on success, 6-decimal amounts, the three address formats, and energy instead of gas. - guide-agentic-development: connecting the OpenZeppelin MCP server and coding agents to TRON contract work. Install and review sections are complete; the walkthrough is a skeleton pending tools that do not exist. The vault example is verified: it compiles with tron-solc 0.8.26 and passes 11/11 tests against the stock tronbox/tre:dev image. A project built from only this page's code blocks compiles and passes, so the guide is self-contained with nothing to clone. Not ready to merge. Six TODO(verify) markers remain, all Nile items that need a funded testnet account. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
✅ Deploy Preview for openzeppelin-docs-v2 ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
- Interact section contradicted itself: it said the USDT balance ends lower by the fees, then said the fees come straight back because the treasury is the reader's own address. The second is correct; the balance returns roughly unchanged and what is actually spent is TRX for energy. - Inflation-attack description said shares "round down to zero", which describes a naive vault. TRC4626 carries virtual shares, so they do not; the guide's own test shows the victim recovering 666.666667 of 1,000 at offset 0. Reworded to match. - Removed an unmeasured TRX cost estimate that sat in the same sentence as the TODO asking for it to be measured. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
luiz-lvj
left a comment
There was a problem hiding this comment.
Looks good, left some minor comments and a few corrections.
| }); | ||
| ``` | ||
|
|
||
| TODO(verify): confirm `TXYZopYRdj2D9XRtbG411XZZ3kM5VkAeBf` is the test USDT that the Nile faucet and TronFAQBot hand out, and that its `transfer` returns `false` like mainnet USDT. |
There was a problem hiding this comment.
Great, I've removed the comment and confirmed that it also returns false on success.
| V->>Tr: safeTransferChecked(entry fee) | ||
| V-->>U: mint vUSDT shares | ||
| U->>V: redeem(shares, receiver, owner) | ||
| V->>Tr: safeTransferChecked(exit fee) | ||
| V->>T: safeTransferChecked(assets to user) |
There was a problem hiding this comment.
The order doesn't match TRC4626Fees: super._deposit pulls and mints before the entry fee, and super._withdraw pays the receiver before the exit fee.
| V->>Tr: safeTransferChecked(entry fee) | |
| V-->>U: mint vUSDT shares | |
| U->>V: redeem(shares, receiver, owner) | |
| V->>Tr: safeTransferChecked(exit fee) | |
| V->>T: safeTransferChecked(assets to user) | |
| V-->>U: mint vUSDT shares | |
| V->>Tr: safeTransferChecked(entry fee) | |
| U->>V: redeem(shares, receiver, owner) | |
| V->>T: safeTransferChecked(assets to user) | |
| V->>Tr: safeTransferChecked(exit fee) |
There was a problem hiding this comment.
Confirmed and applied.
| evmVersion: 'cancun', | ||
| viaIR: true, | ||
| // Embed the source as literal text in the metadata so Tronscan can | ||
| // reconstruct it deterministically when you verify. | ||
| metadata: { bytecodeHash: 'ipfs', useLiteralContent: true }, |
There was a problem hiding this comment.
The project compiles without viaIR (all 46 files), and Tronscan's verification form has no viaIR field, so keeping it makes the contract unverifiable there. The metadata block doesn't help either: Tronscan recompiles from the uploaded source and has no useLiteralContent setting, so this only makes the bytecode differ from what Tronscan produces.
| evmVersion: 'cancun', | |
| viaIR: true, | |
| // Embed the source as literal text in the metadata so Tronscan can | |
| // reconstruct it deterministically when you verify. | |
| metadata: { bytecodeHash: 'ipfs', useLiteralContent: true }, | |
| evmVersion: 'cancun', |
| - `tron: true` on a network routes `hre.ethers` calls through TronWeb, so ethers-style tests and scripts work against the TVM. | ||
|
|
||
| <Callout> | ||
| The compiler is pinned to Solidity 0.8.26 with `evmVersion: 'cancun'` because that is what `tron-solc` and the current TVM support. Don't raise it to match your Ethereum projects. |
There was a problem hiding this comment.
tron-solc ships up to 0.8.31, so that isn't the reason for the pin.
| The compiler is pinned to Solidity 0.8.26 with `evmVersion: 'cancun'` because that is what `tron-solc` and the current TVM support. Don't raise it to match your Ethereum projects. | |
| The compiler is pinned to Solidity 0.8.26 with `evmVersion: 'cancun'` because that is what OpenZeppelin Contracts for TRON is compiled and tested with. Don't raise it to match your Ethereum projects. |
|
|
||
| ## Add an entry and exit fee | ||
|
|
||
| A common customization is a fee that goes to a treasury. The fee logic below is the example from the [TRC-4626 page](/tron-contracts/trc4626#custom-behavior-adding-fees-to-the-vault), unchanged. It lives in the library's `contracts/mocks/docs/` folder, which the npm package doesn't ship, so you can't import it. Add it to your project as `contracts/TRC4626Fees.sol`: |
There was a problem hiding this comment.
| A common customization is a fee that goes to a treasury. The fee logic below is the example from the [TRC-4626 page](/tron-contracts/trc4626#custom-behavior-adding-fees-to-the-vault), unchanged. It lives in the library's `contracts/mocks/docs/` folder, which the npm package doesn't ship, so you can't import it. Add it to your project as `contracts/TRC4626Fees.sol`: | |
| A common customization is a fee that goes to a treasury. The fee logic below is the example from the [TRC-4626 page](/tron-contracts/trc4626#custom-behavior-adding-fees-to-the-vault), with only the comments and import paths changed. It lives in the library's `contracts/mocks/docs/` folder, which the npm package doesn't ship, so you can't import it. Add it to your project as `contracts/TRC4626Fees.sol`: |
| Custom-error assertions are not always safe on the TVM. The library documents three paths where the payload doesn't survive: | ||
|
|
||
| - An **out-of-gas bubble**. The inner call burns the whole energy budget, and the custom error is dropped on the way out. | ||
| - A **`CREATE2` collision**. | ||
| - A **panic code**, whose selector doesn't reliably survive TVM's revert-data field. |
There was a problem hiding this comment.
The CREATE2 collision is the out-of-gas case again (the collision burns almost all energy), and the panic bullet should name the Panic(uint256) payload, not a selector.
| Custom-error assertions are not always safe on the TVM. The library documents three paths where the payload doesn't survive: | |
| - An **out-of-gas bubble**. The inner call burns the whole energy budget, and the custom error is dropped on the way out. | |
| - A **`CREATE2` collision**. | |
| - A **panic code**, whose selector doesn't reliably survive TVM's revert-data field. | |
| Custom-error assertions are not always safe on the TVM. The library documents two paths where the payload doesn't survive: | |
| - An **out-of-gas bubble**. The inner call burns the whole energy budget, and the custom error is dropped on the way out. A `CREATE2` collision is the same case, since it burns almost all energy. | |
| - A **`Panic(uint256)` revert**, whose payload doesn't reliably survive TVM's revert-data field. |
There was a problem hiding this comment.
Sounds good, applied!
| <Callout> | ||
| To keep the node running between runs and skip the startup wait, set `tre.keepRunning: true`. | ||
|
|
||
| Starting the container yourself with `tre.autoStart: false` is a sharper edge than it looks. The plugin mines one priming block when it spawns the container, and skips that step for a node it merely found already running. Without the priming block, the first deploy fails inside TronWeb with `Unable to get params: Cannot read properties of undefined (reading 'toString')`, because java-tron omits the zero-valued block number at height 0. Prefer `keepRunning` over managing the container by hand. |
There was a problem hiding this comment.
The plugin also primes a node it finds already running. The step is skipped because with autoStart: false the plugin's lifecycle never runs at all.
| Starting the container yourself with `tre.autoStart: false` is a sharper edge than it looks. The plugin mines one priming block when it spawns the container, and skips that step for a node it merely found already running. Without the priming block, the first deploy fails inside TronWeb with `Unable to get params: Cannot read properties of undefined (reading 'toString')`, because java-tron omits the zero-valued block number at height 0. Prefer `keepRunning` over managing the container by hand. | |
| Starting the container yourself with `tre.autoStart: false` is a sharper edge than it looks. The plugin mines one priming block before your tests, but only when it manages the node; with `autoStart: false` that step never runs. Without the priming block, the first deploy fails inside TronWeb with `Unable to get params: Cannot read properties of undefined (reading 'toString')`, because java-tron omits the zero-valued block number at height 0. Prefer `keepRunning` over managing the container by hand. |
There was a problem hiding this comment.
Applied. This makes sense thanks!
| TODO(verify): paste real output from a Nile deploy. | ||
|
|
||
| <Callout> | ||
| TRON has no gas. Transactions use **bandwidth** (for transaction size) and **energy** (for contract execution). Your account gets some free bandwidth each day. Energy you haven't staked TRX for is paid by burning TRX. The plugin sets a fee limit of 1,000 TRX (`1_000_000_000` SUN) per transaction. That is a cap, not a charge: you pay only for the energy used. TODO(verify): measure the actual energy and TRX cost of this deploy. To raise or lower the cap for a call, pass `{ gasLimit }`; the plugin converts it into a fee limit. |
There was a problem hiding this comment.
A few corrections: free bandwidth is 600 points a day and this deploy is about 6.7 KB, so most of it burns TRX; the fee limit caps only the energy spend; a transaction that exhausts the limit fails with OUT_OF_ENERGY and is still charged up to it; and { gasLimit } can only lower the cap, since the plugin clamps at 1,000 TRX. The energy figure is from simulating this exact bytecode on Nile (1,065,275 energy at 100 sun per energy).
| TRON has no gas. Transactions use **bandwidth** (for transaction size) and **energy** (for contract execution). Your account gets some free bandwidth each day. Energy you haven't staked TRX for is paid by burning TRX. The plugin sets a fee limit of 1,000 TRX (`1_000_000_000` SUN) per transaction. That is a cap, not a charge: you pay only for the energy used. TODO(verify): measure the actual energy and TRX cost of this deploy. To raise or lower the cap for a call, pass `{ gasLimit }`; the plugin converts it into a fee limit. | |
| TRON has no gas. Transactions use **bandwidth** (for transaction size) and **energy** (for contract execution). Your account gets 600 free bandwidth points a day; beyond that, and for any energy you haven't staked TRX for, TRX is burned. The plugin sets a fee limit of 1,000 TRX (`1_000_000_000` SUN) per transaction. That is a cap on the energy you burn, not a charge: a transaction that succeeds or reverts pays only for the energy it used, while one that exhausts the cap fails and is charged up to it. Deploying this vault takes about 1.07M energy, roughly 107 TRX if none of it is staked. To lower the cap for a call, pass `{ gasLimit }`; the plugin converts it into a fee limit and never goes above 1,000 TRX. |
There was a problem hiding this comment.
Great, thanks for the corrections and providing the simulated figure.
| VAULT_ADDRESS=<T... address from deploy.js> npx hardhat run scripts/interact.js --network nile | ||
| ``` | ||
|
|
||
| Your USDT balance comes back close to where it started. The vault charged both fees, but the treasury is your own address in this tutorial, so they came straight back. What you actually spent is TRX for energy. Point the treasury at a different address and you would end up lower by the two fees. The redeem worked even though Nile's USDT returns `false` from `transfer`, because `TRC4626` paid it out with `safeTransferChecked`. |
There was a problem hiding this comment.
Bandwidth is paid in TRX too once the free daily quota is used up.
| Your USDT balance comes back close to where it started. The vault charged both fees, but the treasury is your own address in this tutorial, so they came straight back. What you actually spent is TRX for energy. Point the treasury at a different address and you would end up lower by the two fees. The redeem worked even though Nile's USDT returns `false` from `transfer`, because `TRC4626` paid it out with `safeTransferChecked`. | |
| Your USDT balance comes back close to where it started. The vault charged both fees, but the treasury is your own address in this tutorial, so they came straight back. What you actually spent is TRX for energy and bandwidth. Point the treasury at a different address and you would end up lower by the two fees. The redeem worked even though Nile's USDT returns `false` from `transfer`, because `TRC4626` paid it out with `safeTransferChecked`. |
| - Each transaction's **Resources consumed**, showing the energy and bandwidth it used. | ||
|
|
||
| <Callout> | ||
| Tronscan source verification isn't automated by `hardhat-tron` yet. To verify the contract, upload the source on Tronscan's contract verification page with compiler `0.8.26`, optimizer on with 200 runs, and `viaIR` enabled. TODO(verify): Tronscan's current flow for `viaIR` / standard-JSON input. |
There was a problem hiding this comment.
Tronscan recompiles from the upload and doesn't accept subdirectory layouts, so the project has to be flattened first. Its form has no viaIR setting, which is why dropping viaIR from the config above matters.
| Tronscan source verification isn't automated by `hardhat-tron` yet. To verify the contract, upload the source on Tronscan's contract verification page with compiler `0.8.26`, optimizer on with 200 runs, and `viaIR` enabled. TODO(verify): Tronscan's current flow for `viaIR` / standard-JSON input. | |
| Tronscan source verification isn't automated by `hardhat-tron` yet. To verify the contract, flatten it into one file (`npx hardhat flatten contracts/UsdtVault.sol > UsdtVault.flat.sol`) and upload that on Tronscan's contract verification page with compiler `0.8.26` and optimizer on with 200 runs. Tronscan doesn't accept subdirectory layouts or a `viaIR` setting. |
Fifteen corrections from the library engineer's review, all verified
locally before applying:
- Mermaid order now matches TRC4626Fees: super._deposit/_withdraw run
before the fee transfer, so shares mint before the entry fee and the
receiver is paid before the exit fee.
- Drop viaIR and the metadata block. The project compiles (46 files) and
passes 11/11 without them, and Tronscan has no viaIR field, so keeping
them only made the bytecode unverifiable there.
- Pin rationale corrected: tron-solc ships up to 0.8.31, so 0.8.26 is what
the library is tested with, not a toolchain ceiling.
- safeTransferChecked: the balance check, not USDT's own revert, is what
catches a token that returns false without moving anything.
- Fee rounding favours the treasury, not the vault; only rounding dust
stays behind. Test comments corrected to match.
- Custom-error callout: a CREATE2 collision is the out-of-gas case, and
the panic path is a Panic(uint256) payload, not a selector.
- autoStart: the plugin does prime a node it finds running. The lifecycle
is skipped entirely when autoStart is false, which is why it breaks.
- Energy: 600 free bandwidth points a day, the fee limit caps energy only
and is charged in full on OUT_OF_ENERGY, and { gasLimit } can only lower
it since the plugin clamps at 1,000 TRX. Deploy measured at ~1.07M
energy, roughly 107 TRX.
- Tronscan verification needs a flattened file; it rejects subdirectories.
Resolves two TODO(verify) markers (energy cost, Tronscan flow) and
confirms the Nile test USDT address. Four remain, all Nile.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Answers the open question on review comment #1. Probed both tokens over TronGrid with a constant call to transfer(addr, 0) from a neutral caller: both return 0x00...00, i.e. false, on a call that succeeds. Both report the contract name TetherToken with an ABI declaring returns (bool). Their deployed bytecode is byte-for-byte identical (15,575 bytes, sha256 2866816060782939f36b0f42...), so the Nile token is the same contract, not a lookalike. Removes the TODO(verify) and states the fact, since this is the reason the Nile run exercises the payout path at all. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The page needs further internal review before it goes in front of the client, so it leaves this PR and comes back as its own draft later. Sixteen of its sections are TODO(tool) placeholders for tooling that does not exist yet (TRC-4626 generator, scaffold, test, deploy, verify, and a TRON agent skill). Published as-is that list reads as a roadmap, which is the wrong signal while the TRON library is in maintenance mode. Removes the page, its nav entry, and the cross-link from the vault guide's Next steps. The draft is kept outside the repo so it can be picked up on its own. This PR is now the vault tutorial and its nav entry. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The {/* DRAFT ... */} comment at the top was a note to reviewers, not
content. It never rendered, and the page now matches its siblings:
frontmatter, then prose.
Three TODO(verify) markers are still in the body and are unaffected by
this. They are visible text and clear with one Nile deploy session.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Deployed and exercised the vault on Nile, then read the numbers back off chain rather than estimating them. - Faucet: one request returned 2,000 TRX and 2,000 test USDT. - Deploy output pasted verbatim. Vault at TGmwKo2Tz46uPGsy75eAPNfTRtkZkeFNFg, tx faed6c72..., block 72028163. - Cost measured: 1,145,938 energy for 114.59 TRX, plus 9.62 TRX of bandwidth once the free 600 points ran out, 124.22 TRX in total. The earlier 107 TRX figure came from an energy-only simulation and was about 16% low, which is the bandwidth the review flagged at L799. - Interact output pasted verbatim. tx.hash is the TVM transaction ID Tronscan indexes and .wait() resolves on inclusion, both as on Ethereum. - The run ended on exactly 2,000.000000 USDT, confirming the fee accounting: every fee returned because the treasury is the deployer. Also splits the VAULT_ADDRESS command onto two lines. The single-line form is correct shell but wraps badly when copied, and the resulting error says VAULT_ADDRESS is unset right after the reader set it. No TODO(verify) markers remain. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The nileex.io web faucet does not load. DNS resolves and port 443 accepts, but no HTTP response comes back, while api.nileex.io answers in under 300ms, so the chain is healthy and only the site is down. Leading a tutorial with a link that hangs is worse than not listing it, so the web faucet is gone and the bot is the primary route. Discord is first because it is the one confirmed working here. Telegram is named as an alternative. Both links checked, both return 200. Records the real limit: 2,000 of each token per address per 24 hours, which matches both TRON's own docs and the grant this guide's Nile run used. Also states what the tutorial actually costs, 124 TRX and 10 USDT, so a reader can see one grant is ample. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Verified the deployed vault on Nile by hand and hit three fields the guide never mentioned: Main Contract, ViaIR and VM version. A reader reaches that form at the very end of the tutorial, having already deployed, so a missing field there is an expensive place to get stuck. Lists all six settings in a table and explains the two that actually decide the outcome. Main Contract is a selector, not a label: the flattened file holds every inherited contract and Tronscan uses the name to choose which one to compile. ViaIR must be off to match the bytecode the config above produces. The flatten command moves into its own code block rather than sitting inline in a sentence. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Adds the TRON vault tutorial under the Guides section, plus its nav entry.
Build a USDT vault on TRON with Hardhat
A TRC-4626 vault over USDT-TRON with entry and exit fees, tested on a local TRON node and deployed to Nile. TVM divergences are taught at the point they bite rather than front-loaded: USDT's
transferreturningfalseon success, 6-decimal amounts, the three address formats, and energy instead of gas.tron-solc0.8.26 (46 files, cancun) and passes 11/11 tests against the stocktronbox/tre:devimage, no patched node required.TGmwKo2Tz46uPGsy75eAPNfTRtkZkeFNFg, creation txfaed6c72…, block 72028163. Deploy and interact output are pasted verbatim.Running it yourself needs Docker up and nothing else. The plugin spawns a real java-tron container at test start and tears it down at the end. Set
tre.keepRunning: trueif you want it to persist between runs.Review feedback
@luiz-lvj's fifteen inline comments are all applied, each checked against source before being taken: the
TRC4626Feesordering, theviaIRandmetadataremoval (verified: 46 files compile and 11/11 pass without them), the compiler-pin rationale, thesafeTransferCheckedfailure path, the fee-rounding and test comments, the custom-error paths, theautoStartpriming explanation, the energy and bandwidth figures, and the Tronscan flattening step.Open items
engines.node), while the "Tested with" line says 24.13.0. Both true, but a reader could take 24 as the floor.Still worth a look
TRC4626Feesis vendored into the example because the npm package excludescontracts/mocks/**. The code matches the copy on/tron-contracts/trc4626. Shout if it should link out instead.Notes
api.nileex.ioanswers in under 300ms. The bot's documented limit of 2,000 per token per 24 hours matches what this run received.@hh2tags on the install. No Hardhat 3 references, per the current plan.main.