Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 44 additions & 1 deletion script/check_storage_layout.sh
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,50 @@ if ! diff -u "$tmp/snapshot.txt" "$tmp/diamond.txt" > "$tmp/snapshot.diff"; then
fail=1
fi

# Deploy-slim facets mirror only the slots they touch, padded into place
# with __gap*/__pad* fillers. Every pinned (non-filler) entry must sit at
# exactly the slot/offset/type the diamond assigns to the same label, or
# the facet reads garbage through the delegatecall.
PINNED_FACETS=(
InterestAdminFacet
QueueForecastFacet
)

for name in "${PINNED_FACETS[@]}"; do
forge inspect "src/diamond/vault/facets/$name.sol:$name" storage-layout --json \
| normalize > "$tmp/$name.pinned.txt"
if ! python3 -c '
import json, sys
diamond = {}
for line in open(sys.argv[1]):
e = json.loads(line)
diamond[e["label"]] = (e["slot"], e["offset"], e["type"])
bad = 0
pinned = 0
for line in open(sys.argv[2]):
e = json.loads(line)
if e["label"].startswith("__gap") or e["label"].startswith("__pad"):
continue
pinned += 1
want = diamond.get(e["label"])
got = (e["slot"], e["offset"], e["type"])
if want is None:
print(f" {e['\''label'\'']}: not present in the diamond layout")
bad = 1
elif want != got:
print(f" {e['\''label'\'']}: facet has slot={got[0]} offset={got[1]} type={got[2]}, diamond has slot={want[0]} offset={want[1]} type={want[2]}")
bad = 1
if pinned == 0:
print(" no pinned entries found (unexpected)")
bad = 1
sys.exit(bad)
' "$tmp/diamond.txt" "$tmp/$name.pinned.txt"; then
echo "PINNED SLOT MISMATCH: $name does not line up with the diamond layout."
fail=1
fi
done

if [ "$fail" -eq 0 ]; then
echo "storage layout OK: diamond + ${#FACETS[@]} facets identical, snapshot matches ($(wc -l < "$tmp/diamond.txt") entries)"
echo "storage layout OK: diamond + ${#FACETS[@]} facets identical, ${#PINNED_FACETS[@]} slim facets pinned correctly, snapshot matches ($(wc -l < "$tmp/diamond.txt") entries)"
fi
exit "$fail"
24 changes: 23 additions & 1 deletion script/diamond/WiseTelecomNodesDiamondSelectors.sol
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ import {MulticallFacet} from "../../src/diamond/vault/facets/MulticallFacet.sol"
import {QueueAdminFacet} from "../../src/diamond/vault/facets/QueueAdminFacet.sol";
import {QueueJoinLeaveFacet} from "../../src/diamond/vault/facets/QueueJoinLeaveFacet.sol";
import {QueueFulfillFacet} from "../../src/diamond/vault/facets/QueueFulfillFacet.sol";
import {QueueForecastFacet} from "../../src/diamond/vault/facets/QueueForecastFacet.sol";
import {InterestAdminFacet} from "../../src/diamond/vault/facets/InterestAdminFacet.sol";
import {WiseTelecomNodesQueueUIHelper} from "../../src/diamond/vault/helpers/WiseTelecomNodesQueueUIHelper.sol";
import {WiseTelecomNodesQueueHelper} from "../../src/diamond/vault/helpers/WiseTelecomNodesQueueHelper.sol";

Expand All @@ -26,7 +28,9 @@ import {WiseTelecomNodesQueueHelper} from "../../src/diamond/vault/helpers/WiseT
* Counts: admin=27, proxy=3, user=8, sweep=2, cashedInterest=1,
* burnWise=3, move=7, bridge=14, permit2=3, multicall=1,
* queueAdmin=2, queueJoinLeave=5, queueFulfill=4, queueView=10 —
* total 90.
* total 90. Post-launch additions (registered via the timelocked
* selector proposals, not part of the genesis 90): queueForecast=1,
* interestAdmin=1.
*/
library WiseTelecomNodesDiamondSelectors {

Expand Down Expand Up @@ -111,6 +115,24 @@ library WiseTelecomNodesDiamondSelectors {
sels[0] = CashedInterestFacet.getTotalCashedInterest.selector;
}

function queueForecastSelectors()
internal
pure
returns (bytes4[] memory sels)
{
sels = new bytes4[](1);
sels[0] = QueueForecastFacet.solveForAmountAfterFulfill.selector;
}

function interestAdminSelectors()
internal
pure
returns (bytes4[] memory sels)
{
sels = new bytes4[](1);
sels[0] = InterestAdminFacet.setCashedInterest.selector;
}

function burnWiseSelectors()
internal
pure
Expand Down
6 changes: 6 additions & 0 deletions src/diamond/vault/WiseTelecomNodesDiamondEvents.sol
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,12 @@ abstract contract WiseTelecomNodesDiamondEvents {
uint256 totalCashedInterest
);

event CashedInterestSet(
address indexed user,
uint256 previousAmount,
uint256 newAmount
);

event SweeperSet(
address indexed sweeper,
bool allowed
Expand Down
123 changes: 123 additions & 0 deletions src/diamond/vault/facets/InterestAdminFacet.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
// SPDX-License-Identifier: -- WISE --

pragma solidity =0.8.36;

import {WiseTelecomNodesDiamondErrors} from "../WiseTelecomNodesDiamondErrors.sol";
import {WiseTelecomNodesDiamondEvents} from "../WiseTelecomNodesDiamondEvents.sol";

import {NotMaster} from "../../shared/OwnableMaster.sol";
import {OnlyDelegateCall} from "../../shared/DiamondErrors.sol";

/**
* @dev Master-only setter for a wallet's settled interest bucket,
* for positions whose owner can no longer sign (lost keys) and for
* deliberate grants or write-offs. Writes `cashedInterest[user]`
* directly and keeps the `totalCashedInterest` accumulator in
* lockstep with the delta, exactly like every organic write site,
* so the INT-7 sum law and the sweep-buffer reservation hold in
* both directions.
*
* The setter touches ONLY the settled bucket: pending accrual is
* not banked first and keeps accruing on top for a wallet that
* still holds shares. A wallet-to-wallet rescue is two calls, read
* the source bucket, set it to zero, set the destination to the
* read value; the accumulator nets out unchanged.
*
* Gated by the same one-way `supplyChangeByOwnerNotAllowed` latch
* as `mintSupply`/`burnSupply`: throwing the latch renounces every
* master balance-surgery power, this one included. No reentrancy
* guard: the function makes no external calls.
*
* DEPLOY-SLIM STORAGE MIRROR: instead of inheriting the full
* declaration chain, only the four slots this facet touches are
* pinned, padded to their exact positions in the deployed diamond
* layout (master 0, supplyChangeByOwnerNotAllowed 12/20,
* cashedInterest 15, totalCashedInterest 60). The pinned entries
* are asserted label-for-label against the diamond's committed
* layout snapshot by script/check_storage_layout.sh, so any drift
* fails CI before it can ship.
*/
contract InterestAdminFacet is
WiseTelecomNodesDiamondErrors,
WiseTelecomNodesDiamondEvents
{

address internal master;

uint256[11] private __gap1;

address private __pad12;
bool internal supplyChangeByOwnerNotAllowed;

uint256[2] private __gap13;

mapping(address => uint256) internal cashedInterest;

uint256[44] private __gap16;

uint256 internal totalCashedInterest;

address internal immutable _self;

constructor() {
_self = address(this);
}

modifier onlyDelegateCall() {
require(
address(this) != _self,
OnlyDelegateCall()
);
_;
}

modifier onlyMaster() {
require(
msg.sender == master,
NotMaster()
);
_;
}

modifier supplyChangeAllowed() {
require(
supplyChangeByOwnerNotAllowed == false,
SupplyChangeNotAllowed()
);
_;
}

function setCashedInterest(
address _user,
uint256 _amount
)
external
onlyDelegateCall
onlyMaster
supplyChangeAllowed
{
require(
_user != address(0)
&& _user != address(this),
InvalidValue()
);

uint256 previous = cashedInterest[_user];

cashedInterest[_user] = _amount;

totalCashedInterest = totalCashedInterest
+ _amount
- previous;

emit CashedInterestSet(
_user,
previous,
_amount
);

emit TotalCashedInterestChanged(
totalCashedInterest
);
}
}
Loading
Loading