diff --git a/script/check_live_layout_prefix.sh b/script/check_live_layout_prefix.sh new file mode 100755 index 0000000..096cdb9 --- /dev/null +++ b/script/check_live_layout_prefix.sh @@ -0,0 +1,99 @@ +#!/usr/bin/env bash +# Live-storage anchor for post-genesis tail shards. +# +# The repo-side guard (check_storage_layout.sh) proves the diamond and every +# facet in the TREE agree on one layout. This script anchors that layout to +# the code that is ACTUALLY DEPLOYED, so a tail shard activated onto the +# live diamonds provably collides with nothing the live bytecode declares: +# +# 1. fetches the Etherscan-VERIFIED source of a live vault diamond +# (ETHERSCAN_KEY from .env), compiles it standalone, and extracts the +# deployed code's storage layout; +# 2. cross-checks that compile against the on-chain runtime bytecode +# (executable segment byte-equal; the CBOR metadata trailer may differ +# because verified bundles normalize source paths); +# 3. asserts the on-chain runtime bytecode is IDENTICAL across every live +# vault passed in EXTRA_VAULTS, so one verified source covers the fleet; +# 4. asserts the deployed layout is an exact entry-for-entry PREFIX of the +# tree's committed snapshot, and prints the appended tail entries. +# +# Usage: bash script/check_live_layout_prefix.sh +# VAULT/RPC/CHAIN override the eth-usdc default; EXTRA_VAULTS is a +# space-separated "name=rpc=address" list for the fleet check. +# Requires forge + cast + python3, ETHERSCAN_KEY in .env, network access. +set -euo pipefail +cd "$(dirname "$0")/.." + +set -a; source .env; set +a + +VAULT="${VAULT:-0x7e1EFF4301defc24936470B30bd1c686D2a295dc}" +CHAIN="${CHAIN:-1}" +RPC="${RPC:-mainnet}" +SNAPSHOT="test/diamond/storage_snapshot/vault_diamond_layout.json" +DIAMOND_PATH="src/diamond/vault/WiseTelecomNodesDiamond.sol:WiseTelecomNodesDiamond" + +tmp="$(mktemp -d)" +trap 'rm -rf "$tmp"' EXIT + +echo "== 1. fetch + compile the Etherscan-verified source of $VAULT (chain $CHAIN)" +cast source "$VAULT" --chain "$CHAIN" --etherscan-api-key "$ETHERSCAN_KEY" -d "$tmp/live" > /dev/null +live_root="$(dirname "$(find "$tmp/live" -path "*/src/diamond/vault/WiseTelecomNodesDiamond.sol" | head -1)")" +live_root="${live_root%/src/diamond/vault}" +printf '[profile.default]\nsrc = "src"\nlibs = ["node_modules"]\nsolc_version = "0.8.36"\noptimizer = true\noptimizer_runs = 600000\nevm_version = "cancun"\n' > "$live_root/foundry.toml" +(cd "$live_root" && forge build > /dev/null 2>&1 && forge inspect "$DIAMOND_PATH" storage-layout --json > "$tmp/live_layout.json" && forge inspect "$DIAMOND_PATH" deployedBytecode > "$tmp/compiled.hex") + +echo "== 2. on-chain runtime vs verified-source compile" +cast code "$VAULT" --rpc-url "$RPC" > "$tmp/onchain.hex" +python3 - "$tmp/onchain.hex" "$tmp/compiled.hex" <<'PY' +import sys +def load(p): + h = open(p).read().strip().strip('"') + return bytes.fromhex(h[2:] if h.startswith("0x") else h) +on, co = load(sys.argv[1]), load(sys.argv[2]) +def split_meta(b): + n = int.from_bytes(b[-2:], "big") + return b[:-(n + 2)], b[-(n + 2):] +on_x, on_m = split_meta(on) +co_x, co_m = split_meta(co) +if on_x != co_x: + print("MISMATCH: executable runtime differs from the verified-source compile") + sys.exit(1) +tail = "metadata trailer identical" if on_m == co_m else "metadata trailer differs (verified-bundle path normalization)" +print(f" executable segment byte-identical ({len(on_x)} bytes); {tail}") +PY + +echo "== 3. fleet bytecode identity" +ref_hash="$(cast keccak < "$tmp/onchain.hex")" +echo " $VAULT (reference) $ref_hash" +for spec in ${EXTRA_VAULTS:-}; do + name="${spec%%=*}"; rest="${spec#*=}"; rpc="${rest%%=*}"; addr="${rest#*=}" + h="$(cast code "$addr" --rpc-url "$rpc" | cast keccak)" + echo " $name $h" + if [ "$h" != "$ref_hash" ]; then + echo "MISMATCH: $name runs different bytecode than the reference vault" + exit 1 + fi +done + +echo "== 4. deployed layout must be an exact prefix of the committed snapshot" +python3 - "$tmp/live_layout.json" "$SNAPSHOT" <<'PY' +import json, re, sys +def norm(path): + data = json.load(open(path)) + return [{"label": e["label"], "slot": e["slot"], "offset": e["offset"], + "type": re.sub(r"\)\d+", ")", e["type"])} for e in data["storage"]] +live, tree = norm(sys.argv[1]), norm(sys.argv[2]) +if live != tree[:len(live)]: + print("MISMATCH: the deployed layout is NOT a prefix of the tree snapshot") + for i, (a, b) in enumerate(zip(live, tree)): + if a != b: + print(f" first divergence at entry {i}: deployed={a} tree={b}") + break + sys.exit(1) +print(f" deployed code declares {len(live)} entries (last: " + f"{live[-1]['label']} @ slot {live[-1]['slot']}); tree appends:") +for e in tree[len(live):]: + print(f" + {e['label']} @ slot {e['slot']} ({e['type']})") +PY + +echo "live layout anchor OK: deployed bytecode verified, fleet identical, tail shard append-only against the DEPLOYED code" diff --git a/script/check_storage_layout.sh b/script/check_storage_layout.sh index 64fc995..bbc4fb7 100755 --- a/script/check_storage_layout.sh +++ b/script/check_storage_layout.sh @@ -17,6 +17,7 @@ DIAMOND="src/diamond/vault/WiseTelecomNodesDiamond.sol:WiseTelecomNodesDiamond" SNAPSHOT="test/diamond/storage_snapshot/vault_diamond_layout.json" FACETS=( AdminFacet + AutoCompoundFacet BridgeFacet BurnWiseFacet CashedInterestFacet diff --git a/script/diamond/WiseTelecomNodesDiamondSelectors.sol b/script/diamond/WiseTelecomNodesDiamondSelectors.sol index 01ac5ab..aba2437 100644 --- a/script/diamond/WiseTelecomNodesDiamondSelectors.sol +++ b/script/diamond/WiseTelecomNodesDiamondSelectors.sol @@ -18,9 +18,38 @@ import {QueueFulfillFacet} from "../../src/diamond/vault/facets/QueueFulfillFace import {QueueForecastFacet} from "../../src/diamond/vault/facets/QueueForecastFacet.sol"; import {InterestAdminFacet} from "../../src/diamond/vault/facets/InterestAdminFacet.sol"; import {RescueFacet} from "../../src/diamond/vault/facets/RescueFacet.sol"; +import {AutoCompoundFacet} from "../../src/diamond/vault/facets/AutoCompoundFacet.sol"; import {WiseTelecomNodesQueueUIHelper} from "../../src/diamond/vault/helpers/WiseTelecomNodesQueueUIHelper.sol"; import {WiseTelecomNodesQueueHelper} from "../../src/diamond/vault/helpers/WiseTelecomNodesQueueHelper.sol"; +/** + * @dev Getter mirror of the auto-compound tail shard: Solidity + * exposes no `.selector` for public state variables, so the routed + * getter selectors come from here. Signature parity is asserted by + * the facet tests and the live-fork getter reads. + */ +interface IAutoCompoundGetters { + + function isAutoCompoundBot( + address _bot + ) + external + view + returns (bool); + + function autoCompoundAllowed( + address _user + ) + external + view + returns (bool); + + function autoCompoundFeeBps() + external + view + returns (uint256); +} + /** * @dev Single source of truth for WiseTelecomNodes facet selectors. * Deploy scripts and tests both import this library so the wiring @@ -31,7 +60,8 @@ import {WiseTelecomNodesQueueHelper} from "../../src/diamond/vault/helpers/WiseT * queueAdmin=2, queueJoinLeave=5, queueFulfill=4, queueView=10 — * total 90. Post-launch additions (registered via the timelocked * selector proposals, not part of the genesis 90): queueForecast=1, - * interestAdmin=1, rescue=1. + * interestAdmin=1, rescue=1, autoCompound=7 (4 functions + the 3 + * shard getters, which the frozen live dispatchers cannot serve). */ library WiseTelecomNodesDiamondSelectors { @@ -143,6 +173,21 @@ library WiseTelecomNodesDiamondSelectors { sels[0] = RescueFacet.rescueToken.selector; } + function autoCompoundSelectors() + internal + pure + returns (bytes4[] memory sels) + { + sels = new bytes4[](7); + sels[0] = AutoCompoundFacet.compoundInterestOnBehalf.selector; + sels[1] = AutoCompoundFacet.setAutoCompoundAllowed.selector; + sels[2] = AutoCompoundFacet.setAutoCompoundBot.selector; + sels[3] = AutoCompoundFacet.setAutoCompoundFeeBps.selector; + sels[4] = IAutoCompoundGetters.isAutoCompoundBot.selector; + sels[5] = IAutoCompoundGetters.autoCompoundAllowed.selector; + sels[6] = IAutoCompoundGetters.autoCompoundFeeBps.selector; + } + function burnWiseSelectors() internal pure diff --git a/src/diamond/STORAGE_LAYOUT.md b/src/diamond/STORAGE_LAYOUT.md index 70f7ef0..b1e7822 100644 --- a/src/diamond/STORAGE_LAYOUT.md +++ b/src/diamond/STORAGE_LAYOUT.md @@ -4,7 +4,7 @@ Documents the storage layout of `WiseTelecomNodesDiamond` and how it relates to ## WiseTelecomNodesDiamond -Inheritance order: `WiseTelecomNodesDiamondEvents` (no storage) → `WiseTelecomNodesDiamondErrors` (no storage) → `OwnableMaster` → `Pausable` → `ReentrancyGuard` → `ERC20` → `ConfigDeclaration` → `UserStateDeclaration` → `ProxyDeclaration` → `WorkerDeclaration` → `PeerVaultDeclaration` → `SelectorRoutingDeclaration` → `QueueStateDeclaration` → `QueueConfigDeclaration` → `CrossChainDeclaration` → `WiseDeclaration` → `BridgeReplayDeclaration` → `ThirdPartyDeclaration` → `TransferHookDeclaration` → `BurnWiseRotationDeclaration` → `ReferralDeclaration` → `DepositGateDeclaration` → `GracePeriodDeclaration` → `DepositHookDeclaration` → `DepositAccumDeclaration` → `CashedInterestTotalDeclaration` → `SweeperDeclaration` → `DepositAccumPrevDeclaration` → `HookGuardDeclaration` (transient only — no persistent slot) → `InterestRemainderDeclaration`. +Inheritance order: `WiseTelecomNodesDiamondEvents` (no storage) → `WiseTelecomNodesDiamondErrors` (no storage) → `OwnableMaster` → `Pausable` → `ReentrancyGuard` → `ERC20` → `ConfigDeclaration` → `UserStateDeclaration` → `ProxyDeclaration` → `WorkerDeclaration` → `PeerVaultDeclaration` → `SelectorRoutingDeclaration` → `QueueStateDeclaration` → `QueueConfigDeclaration` → `CrossChainDeclaration` → `WiseDeclaration` → `BridgeReplayDeclaration` → `ThirdPartyDeclaration` → `TransferHookDeclaration` → `BurnWiseRotationDeclaration` → `ReferralDeclaration` → `DepositGateDeclaration` → `GracePeriodDeclaration` → `DepositHookDeclaration` → `DepositAccumDeclaration` → `CashedInterestTotalDeclaration` → `SweeperDeclaration` → `DepositAccumPrevDeclaration` → `HookGuardDeclaration` (transient only, no persistent slot) → `InterestRemainderDeclaration` → `AutoCompoundDeclaration`. ### 2026-07 slot compaction @@ -83,6 +83,9 @@ Six dead fields that were previously "retained as storage gaps" were deleted and | 61 | `isSweeper` | mapping(address ⇒ bool) | SweeperDeclaration | | 62 | `depositWindowPrevTotal` | mapping(address ⇒ uint256) | DepositAccumPrevDeclaration | | 63 | `interestRemainder` | mapping(address ⇒ uint256) | InterestRemainderDeclaration | +| 64 | `isAutoCompoundBot` | mapping(address ⇒ bool) | AutoCompoundDeclaration | +| 65 | `autoCompoundAllowed` | mapping(address ⇒ bool) | AutoCompoundDeclaration | +| 66 | `autoCompoundFeeBps` | uint256 | AutoCompoundDeclaration | `CrossChainDeclaration` is appended before `WiseDeclaration`, so `ccipRouter` packs into the free upper bytes of slot 34 alongside `negativeIncentivesNotAllowed` and the remaining cross-chain maps take fresh tail slots — no pre-existing slot moves. `WiseDeclaration` is appended last of the original shards, so `WISE_TOKEN` takes fresh tail slot 39 and likewise moves no pre-existing slot. `BridgeReplayDeclaration` is appended after it as a bridge-hardening tail shard: `processedMessageId` is the `ccipReceive` replay/idempotency guard, and `proposedCrossChainPeer` / `proposedCrossChainPeerDecimals` stage a pending cross-chain peer so repointing an already-enabled lane is timelocked like a first-time enable. Its three maps take fresh tail slots 40–42 and move no pre-existing slot. `ThirdPartyDeclaration` follows it as another tail shard backing the `setThirdPartyAddress` propose/execute timelock: `proposedThirdPartyAddress` and `thirdPartyChangeQueuedAt` take fresh tail slots 43–44 and move no pre-existing slot. `TransferHookDeclaration` follows it as the final tail shard backing the optional swappable transfer hook: `transferHookFacet` is the facet `transfer` / `transferFrom` (and the queue internal move) DELEGATECALL for their interest-move policy when set (zero = built-in inline default), and `proposedTransferHookFacet` / `transferHookChangeQueuedAt` back its 3-day propose/execute timelock. Its three fields take fresh tail slots 45–47 and move no pre-existing slot. `BurnWiseRotationDeclaration` follows it as the final tail shard backing the rotating percentage burn: `burnWiseIndex` is the global sequence cursor advanced and wrapped on every successful `burnWise`, and `lastBurnWiseAt` is the per-caller 1-day cooldown stamp. Its two fields take fresh tail slots 48–49 and move no pre-existing slot. `ReferralDeclaration` follows it as the final tail shard for cross-chain forward-prep: `referralEnabled` gates whether an inbound bridge surfaces a non-empty `referralData` payload via `BridgeReferral`, and `bridgeGasLimit` overrides the destination `ccipReceive` callback budget when non-zero (`0` falls back to `BRIDGE_GAS_LIMIT`). Both pack into fresh tail slot 50 (`referralEnabled` at byte 0, `bridgeGasLimit` at bytes 1..8) and move no pre-existing slot. `DepositGateDeclaration` follows it as the final tail shard backing the dormant-chain deposit gate: `depositsDisabled` blocks direct deposits, Permit2 deposits and queue joins when set by master while withdrawals, queue leaves, transfers and both bridge directions keep working. It packs into the free byte 9 of slot 50 alongside `referralEnabled` and `bridgeGasLimit` and moves no pre-existing slot. `GracePeriodDeclaration` follows it as the final tail shard backing the large-deposit grace period: a single deposit/compound call that grows a user's balance by `graceThresholdAmount` or more stamps `lastLargeDepositAt[user]`, and until `gracePeriodDuration` has elapsed that user cannot claim, compound or reassign interest while deposits, withdrawals and every exit path stay open. `graceThresholdAmount == 0` disables the trigger, so an upgraded diamond with zeroed tail storage behaves exactly as before until master calls the setters. `graceFreezeEnabled` (slot 54, default `false`) is the master on/off switch for the swappable freeze hook (`GraceFreezeHookFacet`): the transfer / queue freeze on a grace-locked address enforces only while it is `true`, flipped instantly (no timelock) via `setGraceFreezeEnabled`, independent of the always-on interest-extraction gate. Its four fields take fresh tail slots 51–54 and move no pre-existing slot. `DepositHookDeclaration` follows it as a tail shard backing the optional swappable deposit hook: `depositHookFacet` is the facet `_registerLargeDeposit` DELEGATECALLs with the decorated call's positive balance delta for its large-deposit detection policy when set (zero = built-in inline single-call default), and `proposedDepositHookFacet` / `depositHookChangeQueuedAt` back its 3-day propose/execute timelock (instant pre-finalize, like the transfer hook). `depositHookFacet` packs into the free bytes 1..20 of slot 54 alongside `graceFreezeEnabled` and the other two fields take fresh tail slots 55–56 — no pre-existing slot moves. `DepositAccumDeclaration` follows it as a tail shard backing the two-bucket deposit accumulator (`GraceAccumHookFacet`): `depositAccumWindow` (slot 57, default `0` = accumulator off, never seeded by constructor or scripts) is the master-set bucket length over which the per-user `depositWindowStart` / `depositWindowTotal` records sum sub-threshold deposits toward `graceThresholdAmount`, all window records cleared on every stamp. Its three fields take fresh tail slots 57–59 and move no pre-existing slot. `CashedInterestTotalDeclaration` follows it as a tail shard backing the global cashed-interest ledger: `totalCashedInterest` mirrors `Σ cashedInterest[user]` in lockstep (accrual adds the banked pending, claims subtract the debited amount, the constructor migration seed delta-adds, `moveMyInterestTo` is net-zero and untouched) and feeds `_calculateNeededBuffer` so `sweepOverhang` reserves the settled liability; it is `internal` (no auto-getter) and read externally via `CashedInterestFacet.getTotalCashedInterest()`. It takes fresh tail slot 60 and moves no pre-existing slot. `SweeperDeclaration` follows it as the final tail shard backing the sweeper allowlist: `isSweeper` gates who may trigger `sweepOverhang`. The constructor seeds the worker, the third party and the deployer (`_seedSweepers`; on the deterministic path the bootstrap shim also grants the pending master and revokes its own seed), and master can grant/revoke further sweepers via the instant `setSweeper` — the trigger needs no timelock because swept funds can only reach the 3-day-timelocked `workerAddress` and the reserve floor holds regardless of caller. It takes fresh tail slot 61 and moves no pre-existing slot. `DepositAccumPrevDeclaration` follows it as the final tail shard extending the deposit accumulator with its previous bucket, split out so the shards before it keep their slots: `depositWindowPrevTotal` carries a user's full previous accumulation bucket forward, and `applyDepositAccumHook` always tests it together with the current `depositWindowTotal`, so deposits within one `depositAccumWindow` of each other can never dodge the `graceThresholdAmount` sum by straddling a bucket boundary while co-counting never reaches back two full windows; it is cleared alongside the other window records on every stamp and on a two-window skip. Appended last, it takes fresh tail slot 62 and moves no pre-existing slot. @@ -98,6 +101,10 @@ The former tail shards `VaultBridgeFlowDeclaration` (`bridgedInflow`/`bridgedOut `InterestRemainderDeclaration` is appended last of all shards, after the transient `HookGuardDeclaration`: `interestRemainder` carries the sub-unit fraction of accrued interest that the floored `_calculateInterest` would otherwise discard when `_assignInterest` resets `lastSyncTimeStamp`. Each sync adds the scaled pending (the `_calculateInterest` numerator before the final `/ PRECISION_FACTOR_E18`) to the carried remainder, banks only the whole units into `cashedInterest`, and stores the leftover (always `< PRECISION_FACTOR_E18`, i.e. less than one base unit of interest) back here, so accrual is lossless across an arbitrary number of syncs and a hostile force-sync (a 1-wei transfer or `moveMyInterestTo` targeting the victim every block) can no longer floor a small holder's interest to zero. Never counted in `totalCashedInterest` until it crosses a whole unit and banks, so the INT-7 accumulator identity holds. It takes fresh tail slot 63 and moves no pre-existing slot. +### 2026-08 auto-compound shard + +`AutoCompoundDeclaration` is appended last, after `InterestRemainderDeclaration`: `isAutoCompoundBot` is the master-set keeper allowlist, `autoCompoundAllowed` the per-user opt-in, and `autoCompoundFeeBps` the capped keeper fee for `AutoCompoundFacet.compoundInterestOnBehalf`: an allowlisted bot compounds an opted-in user's settled interest into shares and keeps the fee in USD. Its three fields take fresh tail slots 64–66 and move no pre-existing slot. Unlike every earlier shard this one is the first activated onto the ALREADY-LIVE diamonds via a routed facet rather than shipping in genesis bytecode: safe because the deployed layout ends at slot 63, so no live code reads or writes slots 64+ and the shard begins on untouched zero slots. That claim is anchored to the deployed code itself by `script/check_live_layout_prefix.sh` (fetches the Etherscan-verified source of a live vault, byte-compares its compiled runtime against the on-chain code of all 9 vaults, and asserts the deployed layout is an exact prefix of the committed snapshot). Because the frozen live dispatchers cannot serve the shard's compiler-generated getters, all seven `AutoCompoundFacet` selectors (four functions + three getters) are routed on live vaults; freshly-deployed diamonds serve the getters natively. + ### Legacy comparison Slots 0–7 are identical between legacy and diamond. They come from the same OZ base contracts inherited in the same order. @@ -119,4 +126,4 @@ The JSON layout is committed to `test/diamond/storage_snapshot/vault_diamond_lay forge inspect src/diamond/vault/WiseTelecomNodesDiamond.sol:WiseTelecomNodesDiamond storage-layout --json > test/diamond/storage_snapshot/vault_diamond_layout.json ``` -`script/check_storage_layout.sh` (run in CI next to the test suite) proves the diamond and all 16 facets compile to byte-identical storage layouts (label/slot/offset/type, AST ids normalized away) and that the committed snapshot still matches the compiled diamond. Any storage edit that lands outside the shared declaration chain — or forgets to regenerate the snapshot — fails the check with a unified diff. +`script/check_storage_layout.sh` (run in CI next to the test suite) proves the diamond and all 18 full-identity facets compile to byte-identical storage layouts (label/slot/offset/type, AST ids normalized away), that the three deploy-slim facets pin their mirrored slots label-for-label, and that the committed snapshot still matches the compiled diamond. Any storage edit that lands outside the shared declaration chain, or forgets to regenerate the snapshot, fails the check with a unified diff. `script/check_live_layout_prefix.sh` additionally anchors the snapshot to the DEPLOYED code: it compiles the Etherscan-verified source of a live vault, byte-compares the compiled runtime against the on-chain bytecode of the fleet, and asserts the deployed layout is an exact prefix of the snapshot (run before activating any post-genesis tail shard). diff --git a/src/diamond/vault/WiseTelecomNodesDiamondErrors.sol b/src/diamond/vault/WiseTelecomNodesDiamondErrors.sol index ffd1acf..156d37b 100644 --- a/src/diamond/vault/WiseTelecomNodesDiamondErrors.sol +++ b/src/diamond/vault/WiseTelecomNodesDiamondErrors.sol @@ -149,4 +149,12 @@ abstract contract WiseTelecomNodesDiamondErrors { error SameIncentive(); error ProtectedToken(); + + // ---- Auto-compound ---- + + error NotAutoCompoundBot(); + + error AutoCompoundNotAllowed(); + + error AutoCompoundFeeTooHigh(); } diff --git a/src/diamond/vault/WiseTelecomNodesDiamondEvents.sol b/src/diamond/vault/WiseTelecomNodesDiamondEvents.sol index c0dcc24..1261c54 100644 --- a/src/diamond/vault/WiseTelecomNodesDiamondEvents.sol +++ b/src/diamond/vault/WiseTelecomNodesDiamondEvents.sol @@ -395,4 +395,27 @@ abstract contract WiseTelecomNodesDiamondEvents { event NegativeIncentivesNotAllowedSet( bool negativeIncentivesNotAllowed ); + + // ---- Auto-compound ---- + + event CompoundInterestOnBehalf( + address indexed user, + address indexed bot, + uint256 netAmount, + uint256 feeAmount + ); + + event AutoCompoundBotSet( + address indexed bot, + bool allowed + ); + + event AutoCompoundAllowedSet( + address indexed user, + bool allowed + ); + + event AutoCompoundFeeBpsSet( + uint256 autoCompoundFeeBps + ); } diff --git a/src/diamond/vault/declarations/AutoCompoundDeclaration.sol b/src/diamond/vault/declarations/AutoCompoundDeclaration.sol new file mode 100755 index 0000000..96f5adf --- /dev/null +++ b/src/diamond/vault/declarations/AutoCompoundDeclaration.sol @@ -0,0 +1,27 @@ +// SPDX-License-Identifier: -- WISE -- + +pragma solidity =0.8.36; + +/** + * @dev Auto-compound tail shard. `isAutoCompoundBot` is the + * master-set keeper allowlist, `autoCompoundAllowed` the per-user + * opt-in and `autoCompoundFeeBps` the keeper reward in basis points + * of the compounded interest (100 = 1%), capped at + * `MAX_AUTO_COMPOUND_FEE_BPS`. All three default to zero, so a + * diamond routing the facet with zeroed tail storage compounds + * nothing until configured. Appended as a tail shard so it takes + * fresh slots 64-66 and moves no pre-existing slot; first shard + * activated onto live diamonds post-genesis, whose frozen + * dispatchers cannot serve the getters, so their selectors are + * routed alongside the functions. + */ +abstract contract AutoCompoundDeclaration { + + mapping(address => bool) public isAutoCompoundBot; + + mapping(address => bool) public autoCompoundAllowed; + + uint256 public autoCompoundFeeBps; + + uint256 internal constant MAX_AUTO_COMPOUND_FEE_BPS = 500; +} diff --git a/src/diamond/vault/declarations/WiseTelecomNodesDeclarations.sol b/src/diamond/vault/declarations/WiseTelecomNodesDeclarations.sol index b20eacf..62ddc70 100644 --- a/src/diamond/vault/declarations/WiseTelecomNodesDeclarations.sol +++ b/src/diamond/vault/declarations/WiseTelecomNodesDeclarations.sol @@ -37,6 +37,7 @@ import {SweeperDeclaration} from "./SweeperDeclaration.sol"; import {DepositAccumPrevDeclaration} from "./DepositAccumPrevDeclaration.sol"; import {HookGuardDeclaration} from "./HookGuardDeclaration.sol"; import {InterestRemainderDeclaration} from "./InterestRemainderDeclaration.sol"; +import {AutoCompoundDeclaration} from "./AutoCompoundDeclaration.sol"; /** * @title WiseTelecomNodesDeclarations @@ -76,7 +77,8 @@ abstract contract WiseTelecomNodesDeclarations is SweeperDeclaration, DepositAccumPrevDeclaration, HookGuardDeclaration, - InterestRemainderDeclaration + InterestRemainderDeclaration, + AutoCompoundDeclaration { constructor( diff --git a/src/diamond/vault/facets/AutoCompoundFacet.sol b/src/diamond/vault/facets/AutoCompoundFacet.sol new file mode 100755 index 0000000..1dbb691 --- /dev/null +++ b/src/diamond/vault/facets/AutoCompoundFacet.sol @@ -0,0 +1,174 @@ +// SPDX-License-Identifier: -- WISE -- + +pragma solidity =0.8.36; + +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; + +import {FacetBase} from "./FacetBase.sol"; + +/** + * @dev Keeper-driven compounding: a master-allowlisted bot converts + * an opted-in user's settled interest into shares and keeps + * `autoCompoundFeeBps` of it as its USD reward. The compound is the + * audited `compoundInterest` flow plus a fee split: identical + * modifier order, bucket cleared and net minted before the two USD + * transfers, and `fee + net` exactly offsets the cleared liability. + * It stamps the user's grace lock on the minted net. Opting out is + * always callable. Inherits the full declaration chain because it + * needs the OZ `_mint` and the remainder-carrying banking. + */ +contract AutoCompoundFacet is FacetBase { + + using SafeERC20 for IERC20; + + constructor() + FacetBase() + {} + + modifier onlyAutoCompoundBot() { + _onlyAutoCompoundBot(); + _; + } + + modifier autoCompoundOptedIn( + address _user + ) { + _autoCompoundOptedIn( + _user + ); + _; + } + + function _onlyAutoCompoundBot() + internal + view + { + require( + isAutoCompoundBot[msg.sender], + NotAutoCompoundBot() + ); + } + + function _autoCompoundOptedIn( + address _user + ) + internal + view + { + require( + autoCompoundAllowed[_user], + AutoCompoundNotAllowed() + ); + } + + function compoundInterestOnBehalf( + address _user + ) + external + onlyDelegateCall + whenNotPaused + nonReentrant + onlyAutoCompoundBot + autoCompoundOptedIn(_user) + assignInterest(_user) + gracePeriodCheck(_user) + registerLargeDeposit(_user) + returns (uint256 netAmount) + { + uint256 interest = _prepareClaim( + _user + ); + + uint256 feeAmount = interest + * autoCompoundFeeBps + / PRECISION_RATE; + + netAmount = interest - feeAmount; + + _checkDepositCap( + netAmount + ); + + _mint( + _user, + netAmount + ); + + if (feeAmount > 0) { + USD_TOKEN.safeTransfer( + msg.sender, + feeAmount + ); + } + + USD_TOKEN.safeTransfer( + thirdPartyAddress, + netAmount + ); + + emit CompoundInterestOnBehalf( + _user, + msg.sender, + netAmount, + feeAmount + ); + } + + function setAutoCompoundAllowed( + bool _allowed + ) + external + onlyDelegateCall + nonReentrant + { + autoCompoundAllowed[msg.sender] = _allowed; + + emit AutoCompoundAllowedSet( + msg.sender, + _allowed + ); + } + + function setAutoCompoundBot( + address _bot, + bool _allowed + ) + external + onlyDelegateCall + onlyMaster + nonReentrant + { + require( + _bot != ZERO_ADDRESS, + InvalidValue() + ); + + isAutoCompoundBot[_bot] = _allowed; + + emit AutoCompoundBotSet( + _bot, + _allowed + ); + } + + function setAutoCompoundFeeBps( + uint256 _bps + ) + external + onlyDelegateCall + onlyMaster + nonReentrant + { + require( + _bps <= MAX_AUTO_COMPOUND_FEE_BPS, + AutoCompoundFeeTooHigh() + ); + + autoCompoundFeeBps = _bps; + + emit AutoCompoundFeeBpsSet( + _bps + ); + } +} diff --git a/test/diamond/WiseTelecomNodesAutoCompoundFacet.t.sol b/test/diamond/WiseTelecomNodesAutoCompoundFacet.t.sol new file mode 100755 index 0000000..1d6ab28 --- /dev/null +++ b/test/diamond/WiseTelecomNodesAutoCompoundFacet.t.sol @@ -0,0 +1,1110 @@ +// SPDX-License-Identifier: UNLICENSED + +pragma solidity =0.8.36; + +import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; + +import {DiamondTestHarness} from "./utils/DiamondTestHarness.sol"; + +import {WiseTelecomNodesDiamond} from "../../src/diamond/vault/WiseTelecomNodesDiamond.sol"; +import {AdminFacet} from "../../src/diamond/vault/facets/AdminFacet.sol"; +import {UserFacet} from "../../src/diamond/vault/facets/UserFacet.sol"; +import {CashedInterestFacet} from "../../src/diamond/vault/facets/CashedInterestFacet.sol"; +import {InterestAdminFacet} from "../../src/diamond/vault/facets/InterestAdminFacet.sol"; +import {AutoCompoundFacet} from "../../src/diamond/vault/facets/AutoCompoundFacet.sol"; + +import {WiseTelecomNodesDiamondErrors} from "../../src/diamond/vault/WiseTelecomNodesDiamondErrors.sol"; +import {NotMaster} from "../../src/diamond/shared/OwnableMaster.sol"; +import {OnlyDelegateCall} from "../../src/diamond/shared/DiamondErrors.sol"; + +import { + WiseTelecomNodesDiamondSelectors, + IAutoCompoundGetters +} from "../../script/diamond/WiseTelecomNodesDiamondSelectors.sol"; + +contract MockUSD is ERC20 { + + constructor() + ERC20("Mock USD", "MUSD") + {} + + function decimals() + public + pure + override + returns (uint8) + { + return 6; + } + + function mint( + address _to, + uint256 _amount + ) + external + { + _mint( + _to, + _amount + ); + } +} + +/** + * @dev Exercises {AutoCompoundFacet}. The load-bearing properties: + * a bot-driven compound is the audited compound path plus a clean + * fee split (net + fee == interest, INT-7 lockstep, USD out equals + * the cleared liability), both gates are required, opt-out can + * never be blocked, the grace stamp is measured on the minted NET + * (the legacy-faithful boundary divergence is pinned explicitly), + * and the getter mirror interface matches the shard's real + * getters. Buckets are granted via {InterestAdminFacet} so every + * fee-math expectation is exact. + */ +contract WiseTelecomNodesAutoCompoundFacetTest is DiamondTestHarness { + + event CompoundInterestOnBehalf( + address indexed user, + address indexed bot, + uint256 netAmount, + uint256 feeAmount + ); + + event AutoCompoundBotSet( + address indexed bot, + bool allowed + ); + + event AutoCompoundAllowedSet( + address indexed user, + bool allowed + ); + + event AutoCompoundFeeBpsSet( + uint256 autoCompoundFeeBps + ); + + uint256 internal constant FEE_BPS = 100; + uint256 internal constant PRECISION_RATE = 10_000; + uint256 internal constant SECONDS_IN_YEAR = 31_540_000; + + address internal bot = address(0xB07); + address internal user = address(0xA1); + address internal userB = address(0xA2); + address internal stranger = address(0xBEEF); + + WiseTelecomNodesDiamond internal diamond; + AutoCompoundFacet internal ac; + MockUSD internal usd; + + address internal facetInstance; + + function setUp() + public + { + vm.warp( + 1_700_000_000 + ); + + usd = new MockUSD(); + + diamond = _newDiamond( + address(usd) + ); + + _wireAllFacets( + diamond + ); + + _wireOne( + diamond, + address(new InterestAdminFacet()), + WiseTelecomNodesDiamondSelectors.interestAdminSelectors() + ); + + facetInstance = address( + new AutoCompoundFacet() + ); + + _wireOne( + diamond, + facetInstance, + WiseTelecomNodesDiamondSelectors.autoCompoundSelectors() + ); + + diamond.finalizeSetup(); + + ac = AutoCompoundFacet( + address(diamond) + ); + + usd.mint( + address(diamond), + 1_000_000 * 1e6 + ); + + ac.setAutoCompoundFeeBps( + FEE_BPS + ); + + ac.setAutoCompoundBot( + bot, + true + ); + } + + function _grant( + address _user, + uint256 _amount + ) + internal + { + InterestAdminFacet(address(diamond)).setCashedInterest( + _user, + _amount + ); + } + + function _optIn( + address _user + ) + internal + { + vm.prank( + _user + ); + + ac.setAutoCompoundAllowed( + true + ); + } + + function _compound( + address _user + ) + internal + returns (uint256) + { + vm.prank( + bot + ); + + return ac.compoundInterestOnBehalf( + _user + ); + } + + function _total() + internal + view + returns (uint256) + { + return CashedInterestFacet(address(diamond)).getTotalCashedInterest(); + } + + // ---- compoundInterestOnBehalf ---- + + function test_compound_happyPath_feeSplitAndConservation() + public + { + uint256 interest = 1_000 * 1e6; + uint256 fee = interest * FEE_BPS / PRECISION_RATE; + uint256 net = interest - fee; + + _grant( + user, + interest + ); + + _optIn( + user + ); + + uint256 totalBefore = _total(); + uint256 vaultUsdBefore = usd.balanceOf(address(diamond)); + uint256 supplyBefore = diamond.totalSupply(); + + uint256 returned = _compound( + user + ); + + assertEq( + returned, + net + ); + + assertEq( + diamond.balanceOf(user), + net + ); + + assertEq( + diamond.cashedInterest(user), + 0 + ); + + assertEq( + usd.balanceOf(bot), + fee + ); + + assertEq( + usd.balanceOf(thirdPty), + net + ); + + assertEq( + _total(), + totalBefore - interest, + "accumulator must drop by the full cleared interest" + ); + + assertEq( + usd.balanceOf(address(diamond)), + vaultUsdBefore - interest, + "USD out must equal the cleared liability" + ); + + assertEq( + diamond.totalSupply(), + supplyBefore + net + ); + } + + function test_compound_emitsEvent() + public + { + uint256 interest = 500 * 1e6; + uint256 fee = interest * FEE_BPS / PRECISION_RATE; + + _grant( + user, + interest + ); + + _optIn( + user + ); + + vm.expectEmit( + true, + true, + false, + true + ); + + emit CompoundInterestOnBehalf( + user, + bot, + interest - fee, + fee + ); + + _compound( + user + ); + } + + function test_compound_zeroFeeBps_skipsBotPayout() + public + { + ac.setAutoCompoundFeeBps( + 0 + ); + + uint256 interest = 1_000 * 1e6; + + _grant( + user, + interest + ); + + _optIn( + user + ); + + uint256 net = _compound( + user + ); + + assertEq( + net, + interest + ); + + assertEq( + usd.balanceOf(bot), + 0 + ); + + assertEq( + diamond.balanceOf(user), + interest + ); + } + + function test_compound_dustInterest_feeRoundsDownToZero() + public + { + uint256 interest = 99; + + _grant( + user, + interest + ); + + _optIn( + user + ); + + uint256 net = _compound( + user + ); + + assertEq( + net, + interest, + "a sub-unit fee must round down to the user" + ); + + assertEq( + usd.balanceOf(bot), + 0 + ); + } + + function test_compound_banksPendingAccrualFirst() + public + { + AdminFacet(address(diamond)).mintSupply( + user, + 1_000 * 1e6 + ); + + vm.warp( + block.timestamp + SECONDS_IN_YEAR + ); + + uint256 interest = diamond.getTotalInterestUser( + user + ); + + assertEq( + interest, + 200 * 1e6, + "20% APR over one rate-year on 1000 units" + ); + + _optIn( + user + ); + + uint256 fee = interest * FEE_BPS / PRECISION_RATE; + + uint256 net = _compound( + user + ); + + assertEq( + net, + interest - fee + ); + + assertEq( + diamond.balanceOf(user), + 1_000 * 1e6 + net + ); + + assertEq( + diamond.cashedInterest(user), + 0 + ); + } + + // ---- grace stamping on the minted net ---- + + function test_compound_stampsGrace_netAtThreshold_thenLocks() + public + { + AdminFacet(address(diamond)).setGraceThresholdAmount( + 100 * 1e6 + ); + + _grant( + user, + 102 * 1e6 + ); + + _optIn( + user + ); + + _compound( + user + ); + + assertEq( + diamond.lastLargeDepositAt(user), + block.timestamp, + "net >= threshold must stamp the grace lock" + ); + + _grant( + user, + 102 * 1e6 + ); + + vm.prank( + bot + ); + + vm.expectRevert( + WiseTelecomNodesDiamondErrors.GracePeriodNotElapsed.selector + ); + + ac.compoundInterestOnBehalf( + user + ); + } + + function test_compound_noStamp_netBelowThreshold() + public + { + AdminFacet(address(diamond)).setGraceThresholdAmount( + 100 * 1e6 + ); + + _grant( + user, + 99 * 1e6 + ); + + _optIn( + user + ); + + _compound( + user + ); + + assertEq( + diamond.lastLargeDepositAt(user), + 0 + ); + } + + function test_compound_boundaryBand_stampMeasuresNetNotInterest() + public + { + AdminFacet(address(diamond)).setGraceThresholdAmount( + 100 * 1e6 + ); + + uint256 interest = 100_500_000; + + _grant( + user, + interest + ); + + _optIn( + user + ); + + _compound( + user + ); + + assertEq( + diamond.lastLargeDepositAt(user), + 0, + "interest >= threshold > net must NOT stamp on the fee path" + ); + + _grant( + userB, + interest + ); + + vm.prank( + userB + ); + + UserFacet(address(diamond)).compoundInterest(); + + assertEq( + diamond.lastLargeDepositAt(userB), + block.timestamp, + "the self-compound control mints the full interest and stamps" + ); + } + + // ---- differential vs the audited self-compound ---- + + function test_differential_selfCompoundMinusFee() + public + { + uint256 interest = 1_000 * 1e6; + uint256 fee = interest * FEE_BPS / PRECISION_RATE; + + _grant( + user, + interest + ); + + _grant( + userB, + interest + ); + + _optIn( + userB + ); + + uint256 totalBefore = _total(); + + vm.prank( + user + ); + + uint256 selfMinted = UserFacet(address(diamond)).compoundInterest(); + + uint256 thirdPtyAfterSelf = usd.balanceOf( + thirdPty + ); + + uint256 net = _compound( + userB + ); + + assertEq( + net, + selfMinted - fee, + "on-behalf must equal the audited self-compound minus the fee" + ); + + assertEq( + diamond.balanceOf(userB), + diamond.balanceOf(user) - fee + ); + + assertEq( + usd.balanceOf(thirdPty) - thirdPtyAfterSelf, + thirdPtyAfterSelf - fee, + "third-party USD legs differ by exactly the fee" + ); + + assertEq( + _total(), + totalBefore - 2 * interest, + "both paths clear the identical liability" + ); + } + + // ---- gates and reverts ---- + + function test_compound_notBot_reverts() + public + { + _grant( + user, + 1e6 + ); + + _optIn( + user + ); + + vm.prank( + stranger + ); + + vm.expectRevert( + WiseTelecomNodesDiamondErrors.NotAutoCompoundBot.selector + ); + + ac.compoundInterestOnBehalf( + user + ); + } + + function test_compound_notOptedIn_reverts() + public + { + _grant( + user, + 1e6 + ); + + vm.prank( + bot + ); + + vm.expectRevert( + WiseTelecomNodesDiamondErrors.AutoCompoundNotAllowed.selector + ); + + ac.compoundInterestOnBehalf( + user + ); + } + + function test_compound_afterOptOut_reverts() + public + { + _grant( + user, + 1e6 + ); + + _optIn( + user + ); + + vm.prank( + user + ); + + ac.setAutoCompoundAllowed( + false + ); + + vm.prank( + bot + ); + + vm.expectRevert( + WiseTelecomNodesDiamondErrors.AutoCompoundNotAllowed.selector + ); + + ac.compoundInterestOnBehalf( + user + ); + } + + function test_compound_noInterest_reverts() + public + { + _optIn( + user + ); + + vm.prank( + bot + ); + + vm.expectRevert( + WiseTelecomNodesDiamondErrors.NoInterest.selector + ); + + ac.compoundInterestOnBehalf( + user + ); + } + + function test_compound_overDepositCap_reverts() + public + { + _grant( + user, + 1_000 * 1e6 + ); + + _optIn( + user + ); + + AdminFacet(address(diamond)).setTotalDepositCap( + 0 + ); + + vm.prank( + bot + ); + + vm.expectRevert( + WiseTelecomNodesDiamondErrors.DepositExceedCap.selector + ); + + ac.compoundInterestOnBehalf( + user + ); + } + + function test_compound_paused_reverts() + public + { + _grant( + user, + 1e6 + ); + + _optIn( + user + ); + + AdminFacet(address(diamond)).pauseDeposits(); + + vm.prank( + bot + ); + + vm.expectRevert( + bytes("Pausable: paused") + ); + + ac.compoundInterestOnBehalf( + user + ); + } + + // ---- setAutoCompoundAllowed ---- + + function test_setAllowed_togglesAndEmits() + public + { + assertEq( + diamond.autoCompoundAllowed(user), + false + ); + + vm.expectEmit( + true, + false, + false, + true + ); + + emit AutoCompoundAllowedSet( + user, + true + ); + + _optIn( + user + ); + + assertEq( + diamond.autoCompoundAllowed(user), + true + ); + + vm.prank( + user + ); + + ac.setAutoCompoundAllowed( + false + ); + + assertEq( + diamond.autoCompoundAllowed(user), + false + ); + } + + function test_setAllowed_optOutWorksWhilePaused() + public + { + _optIn( + user + ); + + AdminFacet(address(diamond)).pauseDeposits(); + + vm.prank( + user + ); + + ac.setAutoCompoundAllowed( + false + ); + + assertEq( + diamond.autoCompoundAllowed(user), + false, + "opting out must never be blocked by a pause" + ); + } + + // ---- setAutoCompoundBot ---- + + function test_setBot_setsRevokesAndEmits() + public + { + vm.expectEmit( + true, + false, + false, + true + ); + + emit AutoCompoundBotSet( + stranger, + true + ); + + ac.setAutoCompoundBot( + stranger, + true + ); + + assertEq( + diamond.isAutoCompoundBot(stranger), + true + ); + + ac.setAutoCompoundBot( + stranger, + false + ); + + assertEq( + diamond.isAutoCompoundBot(stranger), + false + ); + } + + function test_setBot_zeroAddress_reverts() + public + { + vm.expectRevert( + WiseTelecomNodesDiamondErrors.InvalidValue.selector + ); + + ac.setAutoCompoundBot( + address(0), + true + ); + } + + function test_setBot_nonMaster_reverts() + public + { + vm.prank( + stranger + ); + + vm.expectRevert( + NotMaster.selector + ); + + ac.setAutoCompoundBot( + stranger, + true + ); + } + + // ---- setAutoCompoundFeeBps ---- + + function test_setFeeBps_setsAndEmits() + public + { + vm.expectEmit( + false, + false, + false, + true + ); + + emit AutoCompoundFeeBpsSet( + 250 + ); + + ac.setAutoCompoundFeeBps( + 250 + ); + + assertEq( + diamond.autoCompoundFeeBps(), + 250 + ); + } + + function test_setFeeBps_capBoundary() + public + { + ac.setAutoCompoundFeeBps( + 500 + ); + + assertEq( + diamond.autoCompoundFeeBps(), + 500 + ); + + vm.expectRevert( + WiseTelecomNodesDiamondErrors.AutoCompoundFeeTooHigh.selector + ); + + ac.setAutoCompoundFeeBps( + 501 + ); + } + + function test_setFeeBps_nonMaster_reverts() + public + { + vm.prank( + stranger + ); + + vm.expectRevert( + NotMaster.selector + ); + + ac.setAutoCompoundFeeBps( + 1 + ); + } + + // ---- facet plumbing ---- + + function test_directCall_reverts() + public + { + AutoCompoundFacet direct = AutoCompoundFacet( + facetInstance + ); + + vm.expectRevert( + OnlyDelegateCall.selector + ); + + direct.compoundInterestOnBehalf( + user + ); + + vm.expectRevert( + OnlyDelegateCall.selector + ); + + direct.setAutoCompoundAllowed( + true + ); + + vm.expectRevert( + OnlyDelegateCall.selector + ); + + direct.setAutoCompoundBot( + bot, + true + ); + + vm.expectRevert( + OnlyDelegateCall.selector + ); + + direct.setAutoCompoundFeeBps( + 1 + ); + } + + /** + * @dev Direct calls through the mirror interface prove each + * mirrored signature resolves to a real getter in the facet + * bytecode; the routing loop proves the diamond carries all + * seven selectors. + */ + function test_getterMirrorInterface_matchesFacetBytecode() + public + { + IAutoCompoundGetters direct = IAutoCompoundGetters( + facetInstance + ); + + assertEq( + direct.isAutoCompoundBot(bot), + false + ); + + assertEq( + direct.autoCompoundAllowed(user), + false + ); + + assertEq( + direct.autoCompoundFeeBps(), + 0 + ); + + bytes4[] memory sels = WiseTelecomNodesDiamondSelectors.autoCompoundSelectors(); + + for (uint256 i = 0; i < sels.length; i++) { + assertEq( + diamond.selectorToFacet(sels[i]), + facetInstance, + "every auto-compound selector must be routed" + ); + } + } + + // ---- fee-math fuzz ---- + + function testFuzz_feeMath_conservation( + uint256 _interest, + uint256 _bps + ) + public + { + _interest = bound( + _interest, + 1, + 1_000_000 * 1e6 + ); + + _bps = bound( + _bps, + 0, + 500 + ); + + ac.setAutoCompoundFeeBps( + _bps + ); + + _grant( + user, + _interest + ); + + _optIn( + user + ); + + uint256 totalBefore = _total(); + + uint256 net = _compound( + user + ); + + uint256 fee = _interest * _bps / PRECISION_RATE; + + assertEq( + net, + _interest - fee + ); + + assertEq( + usd.balanceOf(bot), + fee + ); + + assertEq( + diamond.balanceOf(user), + net + ); + + assertGt( + net, + 0, + "the capped fee can never consume the whole interest" + ); + + assertEq( + _total(), + totalBefore - _interest + ); + } +} diff --git a/test/diamond/storage_snapshot/vault_diamond_layout.json b/test/diamond/storage_snapshot/vault_diamond_layout.json index 345a487..776bee5 100644 --- a/test/diamond/storage_snapshot/vault_diamond_layout.json +++ b/test/diamond/storage_snapshot/vault_diamond_layout.json @@ -1,7 +1,7 @@ { "storage": [ { - "astId": 48281, + "astId": 50368, "contract": "src/diamond/vault/WiseTelecomNodesDiamond.sol:WiseTelecomNodesDiamond", "label": "master", "offset": 0, @@ -9,7 +9,7 @@ "type": "t_address" }, { - "astId": 48283, + "astId": 50370, "contract": "src/diamond/vault/WiseTelecomNodesDiamond.sol:WiseTelecomNodesDiamond", "label": "proposedMaster", "offset": 0, @@ -73,7 +73,7 @@ "type": "t_string_storage" }, { - "astId": 49441, + "astId": 51595, "contract": "src/diamond/vault/WiseTelecomNodesDiamond.sol:WiseTelecomNodesDiamond", "label": "USD_TOKEN", "offset": 0, @@ -81,7 +81,7 @@ "type": "t_contract(IERC20)41572" }, { - "astId": 49443, + "astId": 51597, "contract": "src/diamond/vault/WiseTelecomNodesDiamond.sol:WiseTelecomNodesDiamond", "label": "interestRate", "offset": 0, @@ -89,7 +89,7 @@ "type": "t_uint256" }, { - "astId": 49445, + "astId": 51599, "contract": "src/diamond/vault/WiseTelecomNodesDiamond.sol:WiseTelecomNodesDiamond", "label": "decimalsSet", "offset": 0, @@ -97,7 +97,7 @@ "type": "t_uint8" }, { - "astId": 49447, + "astId": 51601, "contract": "src/diamond/vault/WiseTelecomNodesDiamond.sol:WiseTelecomNodesDiamond", "label": "thirdPartyAddress", "offset": 1, @@ -105,7 +105,7 @@ "type": "t_address" }, { - "astId": 49449, + "astId": 51603, "contract": "src/diamond/vault/WiseTelecomNodesDiamond.sol:WiseTelecomNodesDiamond", "label": "InterestRateProxy", "offset": 0, @@ -113,7 +113,7 @@ "type": "t_address" }, { - "astId": 49451, + "astId": 51605, "contract": "src/diamond/vault/WiseTelecomNodesDiamond.sol:WiseTelecomNodesDiamond", "label": "currentProxyBenefactor", "offset": 0, @@ -121,7 +121,7 @@ "type": "t_address" }, { - "astId": 49453, + "astId": 51607, "contract": "src/diamond/vault/WiseTelecomNodesDiamond.sol:WiseTelecomNodesDiamond", "label": "supplyChangeByOwnerNotAllowed", "offset": 20, @@ -129,7 +129,7 @@ "type": "t_bool" }, { - "astId": 49455, + "astId": 51609, "contract": "src/diamond/vault/WiseTelecomNodesDiamond.sol:WiseTelecomNodesDiamond", "label": "totalDepositCap", "offset": 0, @@ -137,7 +137,7 @@ "type": "t_uint256" }, { - "astId": 49736, + "astId": 51890, "contract": "src/diamond/vault/WiseTelecomNodesDiamond.sol:WiseTelecomNodesDiamond", "label": "lastSyncTimeStamp", "offset": 0, @@ -145,7 +145,7 @@ "type": "t_mapping(t_address,t_uint256)" }, { - "astId": 49740, + "astId": 51894, "contract": "src/diamond/vault/WiseTelecomNodesDiamond.sol:WiseTelecomNodesDiamond", "label": "cashedInterest", "offset": 0, @@ -153,7 +153,7 @@ "type": "t_mapping(t_address,t_uint256)" }, { - "astId": 49606, + "astId": 51760, "contract": "src/diamond/vault/WiseTelecomNodesDiamond.sol:WiseTelecomNodesDiamond", "label": "proxyBalance", "offset": 0, @@ -161,7 +161,7 @@ "type": "t_mapping(t_address,t_uint256)" }, { - "astId": 49752, + "astId": 52550, "contract": "src/diamond/vault/WiseTelecomNodesDiamond.sol:WiseTelecomNodesDiamond", "label": "workerAddress", "offset": 0, @@ -169,7 +169,7 @@ "type": "t_address" }, { - "astId": 49754, + "astId": 52552, "contract": "src/diamond/vault/WiseTelecomNodesDiamond.sol:WiseTelecomNodesDiamond", "label": "bufferInterestRate", "offset": 0, @@ -177,7 +177,7 @@ "type": "t_uint256" }, { - "astId": 49756, + "astId": 52554, "contract": "src/diamond/vault/WiseTelecomNodesDiamond.sol:WiseTelecomNodesDiamond", "label": "proposedWorkerAddress", "offset": 0, @@ -185,7 +185,7 @@ "type": "t_address" }, { - "astId": 49758, + "astId": 52556, "contract": "src/diamond/vault/WiseTelecomNodesDiamond.sol:WiseTelecomNodesDiamond", "label": "workerChangeQueuedAt", "offset": 0, @@ -193,7 +193,7 @@ "type": "t_uint256" }, { - "astId": 49591, + "astId": 51745, "contract": "src/diamond/vault/WiseTelecomNodesDiamond.sol:WiseTelecomNodesDiamond", "label": "peerVault", "offset": 0, @@ -201,7 +201,7 @@ "type": "t_mapping(t_address,t_bool)" }, { - "astId": 49595, + "astId": 51749, "contract": "src/diamond/vault/WiseTelecomNodesDiamond.sol:WiseTelecomNodesDiamond", "label": "peerVaultChangeQueuedAt", "offset": 0, @@ -209,7 +209,7 @@ "type": "t_mapping(t_address,t_uint256)" }, { - "astId": 49675, + "astId": 51829, "contract": "src/diamond/vault/WiseTelecomNodesDiamond.sol:WiseTelecomNodesDiamond", "label": "selectorToFacet", "offset": 0, @@ -217,7 +217,7 @@ "type": "t_mapping(t_bytes4,t_address)" }, { - "astId": 49679, + "astId": 51833, "contract": "src/diamond/vault/WiseTelecomNodesDiamond.sol:WiseTelecomNodesDiamond", "label": "proposedSelectorFacet", "offset": 0, @@ -225,7 +225,7 @@ "type": "t_mapping(t_bytes4,t_address)" }, { - "astId": 49683, + "astId": 51837, "contract": "src/diamond/vault/WiseTelecomNodesDiamond.sol:WiseTelecomNodesDiamond", "label": "selectorChangeQueuedAt", "offset": 0, @@ -233,7 +233,7 @@ "type": "t_mapping(t_bytes4,t_uint256)" }, { - "astId": 49685, + "astId": 51839, "contract": "src/diamond/vault/WiseTelecomNodesDiamond.sol:WiseTelecomNodesDiamond", "label": "initialized", "offset": 0, @@ -241,15 +241,15 @@ "type": "t_bool" }, { - "astId": 49632, + "astId": 51786, "contract": "src/diamond/vault/WiseTelecomNodesDiamond.sol:WiseTelecomNodesDiamond", "label": "QueMemberByIdAndIncentive", "offset": 0, "slot": "27", - "type": "t_mapping(t_uint256,t_mapping(t_int256,t_struct(QueMember)49337_storage))" + "type": "t_mapping(t_uint256,t_mapping(t_int256,t_struct(QueMember)51474_storage))" }, { - "astId": 49636, + "astId": 51790, "contract": "src/diamond/vault/WiseTelecomNodesDiamond.sol:WiseTelecomNodesDiamond", "label": "earliestValidQueMemberByIncentive", "offset": 0, @@ -257,7 +257,7 @@ "type": "t_mapping(t_int256,t_uint256)" }, { - "astId": 49640, + "astId": 51794, "contract": "src/diamond/vault/WiseTelecomNodesDiamond.sol:WiseTelecomNodesDiamond", "label": "currentOrderIdByIncentive", "offset": 0, @@ -265,7 +265,7 @@ "type": "t_mapping(t_int256,t_uint256)" }, { - "astId": 49644, + "astId": 51798, "contract": "src/diamond/vault/WiseTelecomNodesDiamond.sol:WiseTelecomNodesDiamond", "label": "incentiveAllowed", "offset": 0, @@ -273,7 +273,7 @@ "type": "t_mapping(t_int256,t_bool)" }, { - "astId": 49648, + "astId": 51802, "contract": "src/diamond/vault/WiseTelecomNodesDiamond.sol:WiseTelecomNodesDiamond", "label": "activeOrderCountByIncentive", "offset": 0, @@ -281,7 +281,7 @@ "type": "t_mapping(t_int256,t_uint256)" }, { - "astId": 49650, + "astId": 51804, "contract": "src/diamond/vault/WiseTelecomNodesDiamond.sol:WiseTelecomNodesDiamond", "label": "totalActiveOrders", "offset": 0, @@ -289,7 +289,7 @@ "type": "t_uint256" }, { - "astId": 49612, + "astId": 51766, "contract": "src/diamond/vault/WiseTelecomNodesDiamond.sol:WiseTelecomNodesDiamond", "label": "minDepositAmount", "offset": 0, @@ -297,7 +297,7 @@ "type": "t_uint256" }, { - "astId": 49614, + "astId": 51768, "contract": "src/diamond/vault/WiseTelecomNodesDiamond.sol:WiseTelecomNodesDiamond", "label": "negativeIncentivesNotAllowed", "offset": 0, @@ -305,7 +305,7 @@ "type": "t_bool" }, { - "astId": 49481, + "astId": 51635, "contract": "src/diamond/vault/WiseTelecomNodesDiamond.sol:WiseTelecomNodesDiamond", "label": "ccipRouter", "offset": 1, @@ -313,7 +313,7 @@ "type": "t_address" }, { - "astId": 49485, + "astId": 51639, "contract": "src/diamond/vault/WiseTelecomNodesDiamond.sol:WiseTelecomNodesDiamond", "label": "crossChainPeer", "offset": 0, @@ -321,7 +321,7 @@ "type": "t_mapping(t_uint64,t_address)" }, { - "astId": 49489, + "astId": 51643, "contract": "src/diamond/vault/WiseTelecomNodesDiamond.sol:WiseTelecomNodesDiamond", "label": "crossChainPeerEnabled", "offset": 0, @@ -329,7 +329,7 @@ "type": "t_mapping(t_uint64,t_bool)" }, { - "astId": 49493, + "astId": 51647, "contract": "src/diamond/vault/WiseTelecomNodesDiamond.sol:WiseTelecomNodesDiamond", "label": "crossChainPeerDecimals", "offset": 0, @@ -337,7 +337,7 @@ "type": "t_mapping(t_uint64,t_uint8)" }, { - "astId": 49497, + "astId": 51651, "contract": "src/diamond/vault/WiseTelecomNodesDiamond.sol:WiseTelecomNodesDiamond", "label": "crossChainPeerChangeQueuedAt", "offset": 0, @@ -345,7 +345,7 @@ "type": "t_mapping(t_uint64,t_uint256)" }, { - "astId": 49746, + "astId": 51900, "contract": "src/diamond/vault/WiseTelecomNodesDiamond.sol:WiseTelecomNodesDiamond", "label": "WISE_TOKEN", "offset": 0, @@ -353,7 +353,7 @@ "type": "t_address" }, { - "astId": 49405, + "astId": 51559, "contract": "src/diamond/vault/WiseTelecomNodesDiamond.sol:WiseTelecomNodesDiamond", "label": "processedMessageId", "offset": 0, @@ -361,7 +361,7 @@ "type": "t_mapping(t_bytes32,t_bool)" }, { - "astId": 49409, + "astId": 51563, "contract": "src/diamond/vault/WiseTelecomNodesDiamond.sol:WiseTelecomNodesDiamond", "label": "proposedCrossChainPeer", "offset": 0, @@ -369,7 +369,7 @@ "type": "t_mapping(t_uint64,t_address)" }, { - "astId": 49413, + "astId": 51567, "contract": "src/diamond/vault/WiseTelecomNodesDiamond.sol:WiseTelecomNodesDiamond", "label": "proposedCrossChainPeerDecimals", "offset": 0, @@ -377,7 +377,7 @@ "type": "t_mapping(t_uint64,t_uint8)" }, { - "astId": 49702, + "astId": 51856, "contract": "src/diamond/vault/WiseTelecomNodesDiamond.sol:WiseTelecomNodesDiamond", "label": "proposedThirdPartyAddress", "offset": 0, @@ -385,7 +385,7 @@ "type": "t_address" }, { - "astId": 49704, + "astId": 51858, "contract": "src/diamond/vault/WiseTelecomNodesDiamond.sol:WiseTelecomNodesDiamond", "label": "thirdPartyChangeQueuedAt", "offset": 0, @@ -393,7 +393,7 @@ "type": "t_uint256" }, { - "astId": 49713, + "astId": 51867, "contract": "src/diamond/vault/WiseTelecomNodesDiamond.sol:WiseTelecomNodesDiamond", "label": "transferHookFacet", "offset": 0, @@ -401,7 +401,7 @@ "type": "t_address" }, { - "astId": 49715, + "astId": 51869, "contract": "src/diamond/vault/WiseTelecomNodesDiamond.sol:WiseTelecomNodesDiamond", "label": "proposedTransferHookFacet", "offset": 0, @@ -409,7 +409,7 @@ "type": "t_address" }, { - "astId": 49717, + "astId": 51871, "contract": "src/diamond/vault/WiseTelecomNodesDiamond.sol:WiseTelecomNodesDiamond", "label": "transferHookChangeQueuedAt", "offset": 0, @@ -417,7 +417,7 @@ "type": "t_uint256" }, { - "astId": 49419, + "astId": 51573, "contract": "src/diamond/vault/WiseTelecomNodesDiamond.sol:WiseTelecomNodesDiamond", "label": "burnWiseIndex", "offset": 0, @@ -425,7 +425,7 @@ "type": "t_uint256" }, { - "astId": 49423, + "astId": 51577, "contract": "src/diamond/vault/WiseTelecomNodesDiamond.sol:WiseTelecomNodesDiamond", "label": "lastBurnWiseAt", "offset": 0, @@ -433,7 +433,7 @@ "type": "t_mapping(t_address,t_uint256)" }, { - "astId": 49656, + "astId": 51810, "contract": "src/diamond/vault/WiseTelecomNodesDiamond.sol:WiseTelecomNodesDiamond", "label": "referralEnabled", "offset": 0, @@ -441,7 +441,7 @@ "type": "t_bool" }, { - "astId": 49658, + "astId": 51812, "contract": "src/diamond/vault/WiseTelecomNodesDiamond.sol:WiseTelecomNodesDiamond", "label": "bridgeGasLimit", "offset": 1, @@ -449,7 +449,7 @@ "type": "t_uint64" }, { - "astId": 49531, + "astId": 51685, "contract": "src/diamond/vault/WiseTelecomNodesDiamond.sol:WiseTelecomNodesDiamond", "label": "depositsDisabled", "offset": 9, @@ -457,7 +457,7 @@ "type": "t_bool" }, { - "astId": 49558, + "astId": 51712, "contract": "src/diamond/vault/WiseTelecomNodesDiamond.sol:WiseTelecomNodesDiamond", "label": "gracePeriodDuration", "offset": 0, @@ -465,7 +465,7 @@ "type": "t_uint256" }, { - "astId": 49560, + "astId": 51714, "contract": "src/diamond/vault/WiseTelecomNodesDiamond.sol:WiseTelecomNodesDiamond", "label": "graceThresholdAmount", "offset": 0, @@ -473,7 +473,7 @@ "type": "t_uint256" }, { - "astId": 49564, + "astId": 51718, "contract": "src/diamond/vault/WiseTelecomNodesDiamond.sol:WiseTelecomNodesDiamond", "label": "lastLargeDepositAt", "offset": 0, @@ -481,7 +481,7 @@ "type": "t_mapping(t_address,t_uint256)" }, { - "astId": 49566, + "astId": 51720, "contract": "src/diamond/vault/WiseTelecomNodesDiamond.sol:WiseTelecomNodesDiamond", "label": "graceFreezeEnabled", "offset": 0, @@ -489,7 +489,7 @@ "type": "t_bool" }, { - "astId": 49537, + "astId": 51691, "contract": "src/diamond/vault/WiseTelecomNodesDiamond.sol:WiseTelecomNodesDiamond", "label": "depositHookFacet", "offset": 1, @@ -497,7 +497,7 @@ "type": "t_address" }, { - "astId": 49539, + "astId": 51693, "contract": "src/diamond/vault/WiseTelecomNodesDiamond.sol:WiseTelecomNodesDiamond", "label": "proposedDepositHookFacet", "offset": 0, @@ -505,7 +505,7 @@ "type": "t_address" }, { - "astId": 49541, + "astId": 51695, "contract": "src/diamond/vault/WiseTelecomNodesDiamond.sol:WiseTelecomNodesDiamond", "label": "depositHookChangeQueuedAt", "offset": 0, @@ -513,7 +513,7 @@ "type": "t_uint256" }, { - "astId": 49509, + "astId": 51663, "contract": "src/diamond/vault/WiseTelecomNodesDiamond.sol:WiseTelecomNodesDiamond", "label": "depositAccumWindow", "offset": 0, @@ -521,7 +521,7 @@ "type": "t_uint256" }, { - "astId": 49513, + "astId": 51667, "contract": "src/diamond/vault/WiseTelecomNodesDiamond.sol:WiseTelecomNodesDiamond", "label": "depositWindowStart", "offset": 0, @@ -529,7 +529,7 @@ "type": "t_mapping(t_address,t_uint256)" }, { - "astId": 49517, + "astId": 51671, "contract": "src/diamond/vault/WiseTelecomNodesDiamond.sol:WiseTelecomNodesDiamond", "label": "depositWindowTotal", "offset": 0, @@ -537,7 +537,7 @@ "type": "t_mapping(t_address,t_uint256)" }, { - "astId": 49432, + "astId": 51586, "contract": "src/diamond/vault/WiseTelecomNodesDiamond.sol:WiseTelecomNodesDiamond", "label": "totalCashedInterest", "offset": 0, @@ -545,7 +545,7 @@ "type": "t_uint256" }, { - "astId": 49696, + "astId": 51850, "contract": "src/diamond/vault/WiseTelecomNodesDiamond.sol:WiseTelecomNodesDiamond", "label": "isSweeper", "offset": 0, @@ -553,7 +553,7 @@ "type": "t_mapping(t_address,t_bool)" }, { - "astId": 49525, + "astId": 51679, "contract": "src/diamond/vault/WiseTelecomNodesDiamond.sol:WiseTelecomNodesDiamond", "label": "depositWindowPrevTotal", "offset": 0, @@ -561,12 +561,36 @@ "type": "t_mapping(t_address,t_uint256)" }, { - "astId": 49583, + "astId": 51737, "contract": "src/diamond/vault/WiseTelecomNodesDiamond.sol:WiseTelecomNodesDiamond", "label": "interestRemainder", "offset": 0, "slot": "63", "type": "t_mapping(t_address,t_uint256)" + }, + { + "astId": 51542, + "contract": "src/diamond/vault/WiseTelecomNodesDiamond.sol:WiseTelecomNodesDiamond", + "label": "isAutoCompoundBot", + "offset": 0, + "slot": "64", + "type": "t_mapping(t_address,t_bool)" + }, + { + "astId": 51546, + "contract": "src/diamond/vault/WiseTelecomNodesDiamond.sol:WiseTelecomNodesDiamond", + "label": "autoCompoundAllowed", + "offset": 0, + "slot": "65", + "type": "t_mapping(t_address,t_bool)" + }, + { + "astId": 51548, + "contract": "src/diamond/vault/WiseTelecomNodesDiamond.sol:WiseTelecomNodesDiamond", + "label": "autoCompoundFeeBps", + "offset": 0, + "slot": "66", + "type": "t_uint256" } ], "types": { @@ -649,12 +673,12 @@ "numberOfBytes": "32", "value": "t_bool" }, - "t_mapping(t_int256,t_struct(QueMember)49337_storage)": { + "t_mapping(t_int256,t_struct(QueMember)51474_storage)": { "encoding": "mapping", "key": "t_int256", "label": "mapping(int256 => struct WiseTelecomNodesQueueStructs.QueMember)", "numberOfBytes": "32", - "value": "t_struct(QueMember)49337_storage" + "value": "t_struct(QueMember)51474_storage" }, "t_mapping(t_int256,t_uint256)": { "encoding": "mapping", @@ -663,12 +687,12 @@ "numberOfBytes": "32", "value": "t_uint256" }, - "t_mapping(t_uint256,t_mapping(t_int256,t_struct(QueMember)49337_storage))": { + "t_mapping(t_uint256,t_mapping(t_int256,t_struct(QueMember)51474_storage))": { "encoding": "mapping", "key": "t_uint256", "label": "mapping(uint256 => mapping(int256 => struct WiseTelecomNodesQueueStructs.QueMember))", "numberOfBytes": "32", - "value": "t_mapping(t_int256,t_struct(QueMember)49337_storage)" + "value": "t_mapping(t_int256,t_struct(QueMember)51474_storage)" }, "t_mapping(t_uint64,t_address)": { "encoding": "mapping", @@ -703,13 +727,13 @@ "label": "string", "numberOfBytes": "32" }, - "t_struct(QueMember)49337_storage": { + "t_struct(QueMember)51474_storage": { "encoding": "inplace", "label": "struct WiseTelecomNodesQueueStructs.QueMember", "numberOfBytes": "128", "members": [ { - "astId": 49330, + "astId": 51467, "contract": "src/diamond/vault/WiseTelecomNodesDiamond.sol:WiseTelecomNodesDiamond", "label": "member", "offset": 0, @@ -717,7 +741,7 @@ "type": "t_address" }, { - "astId": 49332, + "astId": 51469, "contract": "src/diamond/vault/WiseTelecomNodesDiamond.sol:WiseTelecomNodesDiamond", "label": "amount", "offset": 0, @@ -725,7 +749,7 @@ "type": "t_uint256" }, { - "astId": 49334, + "astId": 51471, "contract": "src/diamond/vault/WiseTelecomNodesDiamond.sol:WiseTelecomNodesDiamond", "label": "tailPointer", "offset": 0, @@ -733,7 +757,7 @@ "type": "t_uint256" }, { - "astId": 49336, + "astId": 51473, "contract": "src/diamond/vault/WiseTelecomNodesDiamond.sol:WiseTelecomNodesDiamond", "label": "headPointer", "offset": 0,