[feat] Configure Lista OFT v2 bridge - #110
Conversation
Add the official LayerZero V2 upgradeable contracts needed for the upgradeable Lista OFT bridge: - @layerzerolabs/oft-evm-upgradeable - @layerzerolabs/oapp-evm-upgradeable - @layerzerolabs/oapp-evm - @layerzerolabs/oft-evm These provide OFTUpgradeable / OFTAdapterUpgradeable and the OApp upgradeable bases (ERC-7201 namespaced storage), compatible with the repo's OpenZeppelin 4.9.3 (contracts accept ^4.8.1 || ^5.0.0).
Port the v1 TransferLimiter into an upgradeable abstract base for the v2 OFT contracts. Logic (daily/global + per-address transfer limits) is unchanged; differences: - no external setters (the inheriting contract gates them by role) - a storage gap reserved for future appended variables
Upgradeable LayerZero V2 OFT Adapter for the canonical LISTA token on BNB Chain. Locks/unlocks the native token (single lockbox for the mesh). - UUPS upgradeable; only DEFAULT_ADMIN_ROLE may authorize upgrades - PausableUpgradeable: PAUSER_ROLE pause(), MANAGER_ROLE unpause() - MANAGER_ROLE gates the transfer limiter and the LayerZero OApp config (setPeer / setEnforcedOptions / setMsgInspector / setPreCrime) and is the LZ endpoint delegate - _debit/_credit enforce the transfer limiter and pause guard
Upgradeable LayerZero V2 OFT representing LISTA on remote chains (ETH). Mints on inbound bridge messages, burns on outbound; the canonical supply stays escrowed in ListaOFTAdapterV2 on BNB Chain. - same role model as the adapter (DEFAULT_ADMIN / MANAGER / PAUSER) - UUPS upgradeable, DEFAULT_ADMIN_ROLE gated - EIP-2612 permit via ERC20PermitUpgradeable - _debit/_credit enforce the transfer limiter and pause guard
Split mainnet/testnet deploy entrypoints plus LayerZero wiring scripts
for the upgradeable Lista OFT bridge.
- deploy.mainnet.ts / deploy.testnet.ts: UUPS proxy deploy via
hardhat-upgrades (constructorArgs for the immutable token/endpoint,
unsafeAllow constructor + state-variable-immutable), writes the proxy
address back to the live config
- setPeer.ts / setTransferLimit.ts / setDVNConfig.ts: MANAGER-run wiring
- setDVNConfig encodes the ULN (DVN) + executor config: required DVNs
LayerZero Labs + Nethermind + Google, optional USDT0; configurable
- oftChainsV2.{mainnet,testnet}.example.json: real endpoint / send /
receive / executor / DVN addresses for BSC + ETH (and bscTestnet +
sepolia); live configs are git-ignored
- local.t.sol: end-to-end bridge over two mock LayerZero endpoints (lock -> mint -> burn -> unlock), plus transfer limiter, pause/role gating, MANAGER-only OApp config, UUPS upgrade and EIP-2612 permit - fork.t.sol: forks BNB Chain and exercises the adapter against the REAL canonical LISTA token and the REAL LayerZero endpoint — initialize, lock/unlock custody, transfer limiter, pause guard and admin-only upgrade (skips automatically when no BSC fork is available) All 14 tests pass (forge test, solc 0.8.24).
Replace the hardhat deploy flow with native foundry (forge script) for the upgradeable Lista OFT bridge. Network is selected by --rpc-url and resolved from block.chainid. - OFTConfig.sol: per-chain endpoint / send-lib / receive-lib / executor / token / eid / DVN set / transfer limits (BSC 56, ETH 1, BSC testnet 97, Sepolia 11155111) - OFTScriptBase.sol: role resolution, transfer-limit build, DVN sort helpers - DeployListaOFTAdapterV2.s.sol / DeployListaOFTv2.s.sol: UUPS proxy deploy - SetPeer.s.sol / SetTransferLimit.s.sol / SetDVNConfig.s.sol: LZ wiring - SetDVNConfig encodes the ULN (required: LayerZero Labs + Nethermind + Google; optional: USDT0) + executor config and sets the send/receive libs Verified by forking BNB Chain in anvil: deploy + setPeer + setTransferLimit + setDVNConfig all broadcast on-chain successfully against the real LayerZero endpoint.
The v2 OFT deploy/config flow now runs entirely on foundry (scripts/foundry/oft/v2). Remove the superseded hardhat TypeScript scripts and revert the related .gitignore entries.
…GER/PAUSER
Rename the access-control role identifiers and their keccak256 string seeds
in the v2 OFT contracts (ListaOFTv2, ListaOFTAdapterV2), so the on-chain role
ids become keccak256("MANAGER") / keccak256("PAUSER"). Update the v2 tests and
foundry scripts/docs to match.
Pull Request ReviewThis PR adds a full LayerZero OFT v2 bridge implementation for LISTA using upgradeable Solidity contracts, including a lock/unlock adapter on BSC ( Sensitive ContentBlockchain Address:
Security IssuesNo serious security issues detected. Generated by Hashdit Bot. This tool can absolutely NOT replace manual audits. |
TransferLimiterV2 rejected a zero amount (commonly a sub-1e12 input rounded down to 0 by _removeDust) with TransferLimitExceeded, which misleads operators into thinking a configured limit was breached. Add a dedicated ZeroAmount() error and use it for the _amount == 0 case, leaving TransferLimitExceeded for genuine limit breaches. Adds a unit test that drives the internal limiter through an exposed harness.
The single-transfer upper/lower bound check ran after the daily-reset SLOADs and conditional SSTOREs, so an out-of-range amount wasted those reads/writes before reverting. The check depends only on the in-memory limit and _amount, so hoist it above the reset blocks; out-of-range amounts now revert without touching lastUpdatedTime / counter storage. Behavior is otherwise unchanged.
_checkAndUpdateTransferLimit rewrote lastUpdatedTime / lastUserUpdatedTime on every successful transfer and reset the daily counters only when the gap since the last transfer exceeded 24h. On a continuously active route that gap never elapses, so the counters never reset and the global cap behaved as a cumulative ceiling consumed across many days instead of a daily allowance (M01). Reset on a fixed 00:00 UTC boundary instead: compare the UTC day index (timestamp / 1 days) of the stored last-transfer time against the current block. This also removes isMoreThanACalendarDay, whose name implied a calendar reset it never did and whose subtraction could underflow if called with a > b (I08); the new _dayIndex helper does no subtraction. lastUpdatedTime / lastUserUpdatedTime keep storing block.timestamp, so the storage layout is unchanged. Adds a regression test proving two transfers ~20s apart but on different UTC days now reset the counters.
TransferLimiterV2 declares 6 storage mappings plus a uint256[45] gap, totalling 51 reserved slots and breaking the OpenZeppelin 4.9.3 used + gap = 50 convention that the sibling contracts (ListaOFTv2, ListaOFTAdapterV2, each 0 used + gap[50]) follow. Shrink the gap to uint256[44] so used(6) + gap(44) = 50. Storage-layout-safe: this only moves the boundary between TransferLimiterV2's trailing gap and the derived contract's gap, both of which are empty reserved space. No used slot changes position, so the existing testnet proxies can take this via an implementation upgrade; mainnet is not yet deployed.
setPeer defines the trusted LayerZero peer for an endpoint id and is the core cross-chain trust anchor: a forged or malicious peer lets an attacker deliver messages the contract accepts as authentic, minting unbacked tokens (ListaOFTv2) or unlocking escrow (ListaOFTAdapterV2). It was gated by MANAGER, a hot operational role that also holds pause, DVN/options config, and (per OFTScriptBase._roles) can default to the deployer EOA. Move it to DEFAULT_ADMIN_ROLE so the trust anchor sits behind the same authority as the upgrade path (_authorizeUpgrade); the admin should be a timelock/multisig. Emergency severing is unaffected because it goes through pause() (PAUSER/MANAGER), and peer changes are rare, non-emergency setup operations. Also updates SetPeer.s.sol (broadcaster must now hold DEFAULT_ADMIN_ROLE) and the setPeer test to assert a MANAGER-only account is rejected.
The default per-address daily cap was 200,000 against a 1,000,000 global cap (20%), so as few as 5 addresses could fill the shared daily bucket and brick the bridge for everyone until the counters reset. The 100 per-address attempt cap gave little extra protection since an address reached its amount cap in ~2 transfers. Rebalance the defaults so the per-address cap is 2% of the global cap (~50 distinct addresses to exhaust it) while keeping a usable single transfer size: maxDailyTransferAmount 1,000,000 -> 2,000,000 singleTransferUpperLimit 100,000 -> 30,000 dailyTransferAmountPerAddress 200,000 -> 40,000 dailyTransferAttemptPerAddress 100 -> 20 singleTransferLowerLimit 0.1 (unchanged) Constraints in _setTransferLimitConfig still hold (2M > 40k > 30k > 0.1). Limits are pushed on-chain by SetTransferLimit, so they stay tunable without a redeploy as confidence and volume grow.
Every network configured requiredDVNs = [] and relied entirely on an
all-optional threshold (mainnet 3-of-4, testnet 1-of-2). The LayerZero
ULN skips the required-DVN loop when it is empty, so any qualifying
optional subset could finalize a message (mint on the OFT / unlock on
the adapter) with no single operator that must sign every message.
Promote one operator into requiredDVNs on each network so a mandatory
DVN participates in every verification and cannot be routed around:
- BSC / ETH mainnet: required = Google; optional = LayerZero Labs,
Nethermind, USDT0 at 2-of-3.
- BSC testnet / Sepolia: required = LayerZero Labs; optional = Google
at 1-of-1.
required and optional sets stay disjoint and each is sorted by _sorted,
so the existing SetDVNConfig encoding (_dvnCount returns the real length
once non-empty) needs no change. Doc comments updated.
_ethMainnet() and _sepolia() configured the OFT token name as "Lista DAO Token", while the canonical BSC LISTA that the adapter wraps is named "Lista DAO". This produced inconsistent mesh metadata and a different EIP-712 permit domain on the ETH side. Align the name to "Lista DAO" on both the Ethereum mainnet and Sepolia OFT configs. The name is baked in at __ERC20Permit_init on deploy, so this must land before the ETH OFT is deployed (mainnet is not yet deployed); the BSC side is an adapter and sets no name.
OFTScriptBase._roles resolves ADMIN/MANAGER/PAUSER via vm.envOr(name, deployer), so an unset env var silently grants that role to the deployer EOA. With all three unset, one hot key holds every sensitive role and no script flags it. Emit a loud console warning when any role resolves to the deployer. Kept as a warning rather than a revert so single-key test/staging deploys still work; operators should set distinct keys and rotate roles to multisigs after deploy.
setEnforcedOptions exists and is MANAGER-gated on both OFTs, but no deploy/config script ever called it, so enforcedOptions stayed empty and there was no on-chain destination gas floor. A user who calls send() with empty or insufficient extraOptions then produces a message with too little lzReceive gas; _credit runs out of gas on the destination and the funds stall in a stuck / retryable state mid-bridge. Add SetEnforcedOptions.s.sol, which sets a type-3 enforced option with an executor lzReceive gas floor (default 300000, matching the value the bridge test script already uses) for the SEND message type per dstEid, and wire it into the README bring-up sequence before the test transfer.
The bring-up sequence wired peers before configuring libraries/DVNs, so a route could go live while the endpoint still resolved to LayerZero's default library and DVN set, verifying messages under security settings the protocol did not choose. Cross-chain wiring is also non-atomic. Reorder the documented steps so SetDVNConfig (plus transfer limits and enforced options) run before SetPeer, and make SetPeer the last config step that brings a route live. Add a launch checklist: deploy paused; verify libraries, DVNs, enforced options, limits, and peers on both chains and both directions; only then unpause. Also refresh the DVN policy section for the new required-DVN setup and correct the SetPeer role note to DEFAULT_ADMIN_ROLE.
The testnet DVN policy (required LayerZero Labs + optional Google 1-of-1) forced both DVNs to attest, but the Google testnet DVN does not reliably attest, so cross-chain messages stalled at verification (committer never completed). Drop the optional Google requirement on testnet and require only the always-on LayerZero Labs DVN (optionalDVNs empty, threshold 0). Mainnet is unchanged (required Google + 2-of-3 of LayerZero Labs / Nethermind / USDT0), where every DVN is production-grade.
Deploys a fresh implementation (lock/unlock adapter or mint/burn OFT per chain, immutables read from OFTConfig) and calls upgradeTo on the existing UUPS proxy. Used to roll the audit-remediation implementations onto the already-deployed testnet proxies; storage layout is unchanged so no reinitializer is needed.
Pull Request ReviewThis PR introduces a full LayerZero OFT v2 bridging stack for Lista, including two new upgradeable Solidity contracts ( Sensitive ContentBlockchain Address:
Security IssuesNo serious security issues detected. Generated by Hashdit Bot. This tool can absolutely NOT replace manual audits. |
Pull Request ReviewThis PR introduces a full OFT v2 bridge stack for Lista across BSC and Ethereum (including testnets), adding new upgradeable contracts ( Sensitive ContentBlockchain Address:
Security IssuesNo serious security issues detected. Generated by Hashdit Bot. This tool can absolutely NOT replace manual audits. |
Size _defaultLimits against LISTA supply (totalSupply 1B, circulating ~415M, price ~$0.049): - maxDailyTransferAmount 2M -> 8M (<2% circulating, ~$392k) - singleTransferUpperLimit 30k -> 200k - dailyTransferAmountPerAddress 40k -> 400k (5% of global; 20 addrs to drain) Per-address 400k/day lets operations bridge ~500k/week from one wallet. DVN set remains the primary control; limiter is the backstop. Baked at deploy; already-live proxies update via SetTransferLimit.
The DVN "Config error: Outbound confirmations must be >= inbound confirmations" blocked ETH->BSC because SetDVNConfig set the receive-side ULN confirmations to the local chain's value. Inbound confirmations must mirror the REMOTE source chain's send confirmations. - OFTConfig: add inboundConfirmations (BSC 15 from ETH, ETH 20 from BSC, testnets 5). Asymmetric mainnet values (BSC 20 / ETH 15) are why this only surfaced on mainnet; symmetric testnet values (5/5) masked it. - SetDVNConfig: encode separate send (confirmations) and receive (inboundConfirmations) ULN configs. Verified on mainnet: after re-running SetDVNConfig on both chains, BSC->ETH (20>=20) and ETH->BSC (15>=15) both pass and a stuck in-flight message unlocked.
…ipts - SetDVNConfig: log send (outbound) and inbound (receive) confirmations so a confirmations mismatch is visible in the run output. - SetTransferLimit: log all five limit values being pushed.
Two-step, verifiable handoff off the deployer EOA: - GrantRolesAndTransferOwnership: grants ADMIN+owner to the Timelock, MANAGER+delegate to the Safe, PAUSER to the pauser, and transfers ownership, WITHOUT stripping the deployer. Requires the Timelock/Safe to have code on the current chain (guards a mistyped/cross-chain address) and rejects the deployer as any target (a role granted to the deployer would be stripped in step 2, leaving it holderless). - RevokeDeployerRole: revoke-only. Preconditions assert the Timelock already holds ADMIN + owner before the deployer revokes MANAGER/PAUSER and renounces DEFAULT_ADMIN_ROLE last, so the handoff cannot lock the contract out of an admin. Splitting grant from the irreversible renounce leaves a window to verify the new holders are controllable before stripping the deployer.
Pull Request ReviewThis PR introduces a full OFT v2 bridge stack for Lista across BSC and Ethereum, including two new upgradeable contracts ( Sensitive ContentBlockchain Address:
Security IssuesNo serious security issues detected. Generated by Hashdit Bot. This tool can absolutely NOT replace manual audits. |
|
@audit-agent |
Cloud Auditor — automated review of PR #110Scope: New Lista OFT v2 bridge ( Severity summary
HashDit 2026-06-30 remediation verificationAll 12 "fixed" items independently verified against this PR's code: Key non-Info findings
Verified cleanReentrancy (13 checks — CEI order intact, LISTA is plain ERC-20 no ERC-777 hooks, Full report PDF delivered separately (bridge audit channel). Prior HashDit audit already covers most of this diff; this pass focused on regression verification and delta from the HashDit-endorsed resolution config. |
Summary
This PR configures the Lista OFT v2 bridge flow for BSC and ETH, and updates the LayerZero DVN policy used by the Foundry scripts.
Changes
MANAGERandPAUSERconstants consistently.SetDVNConfigidempotent by skipping send/receive library updates when the same explicit library is already configured.