Files
zeekayandHanzo Dev 188a4f4e8a chain: the deploy key signs, and owns nothing afterwards
The markets could not be built at all — LiquidInitializationParams grew a
maxPriceDeviation rate limiter, and SecurityTokenAdapter reports one address
for both halves of a pair the engine now checks, which a like-kind market can
never satisfy. Index is that pair's price: collateral in its own synthetic,
opening at parity, and unable to fall, so an oracle can hand out room but never
seize a position.

What did deploy left the deploy key holding all of it: unlimited synthetic and
collateral mint, arbitrary NAV, halt, and the fee vaults. Ownership now goes to
OWNER and the yield index to ORACLE, and each step re-reads the chain and fails
the run if the deploy key kept anything. Both handovers are by nomination, so
each market is built by a Regent that spends itself in the transaction that
creates it rather than by a key that would stay admin until the multisig signs.

Nothing mints synthetic outside a market: the fee vaults' float was supply
totalSyntheticsIssued never counted, which the transmuter's own arithmetic
underflows on.

The lib/ symlinks point at working trees, so pins records a hash of every
source each step compiles and deploy.sh stops before broadcasting if upstream
moved. deploy.json is gone — deploy/<chainId>.json is the one address book, and
the two disagreed about which address held LBTC.

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-25 12:30:56 -07:00

114 lines
5.5 KiB
Solidity

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.31;
import {Script, console} from "forge-std/Script.sol";
import {WLUX} from "@luxfi/standard/tokens/WLUX.sol";
import {LRC20B} from "@luxfi/standard/bridge/LRC20B.sol";
import {BridgedETH} from "@luxfi/standard/bridge/collateral/ETH.sol";
import {BridgedBTC} from "@luxfi/standard/bridge/collateral/BTC.sol";
import {LiquidToken} from "@luxfi/standard/liquid/LiquidToken.sol";
/// @title Tokens — the asset layer, deployed per chain.
/// @notice Two tiers, and they are not interchangeable.
///
/// Collateral is the bridged tier: LUX is the chain's own coin so it is not
/// deployed, WLUX is its ERC-20 wrapper, and BridgedETH and BridgedBTC are what
/// a bridge mints when ETH or BTC arrives. Their symbols are plain "ETH" and
/// "BTC", and BridgedBTC keeps Bitcoin's 8 decimals rather than padding to 18.
///
/// Debt is the liquid tier — LETH, LBTC, LLUX — one per collateral, and this is
/// where the safety argument lives. A 90% ceiling is only sane when the debt is
/// denominated in the same asset as the collateral, because then a price move
/// changes both sides at once and cannot move the ratio between them. Borrow a
/// dollar-denominated synthetic against volatile collateral at 90% and an
/// ordinary drawdown takes the position out. So each market gets its own
/// like-kind synthetic and there is no shared dollar debt token.
///
/// The liquid tier is deployed from LiquidToken rather than from the concrete
/// LiquidETH/LiquidBTC in liquid/tokens/. Those extend LRC20B, which offers
/// `burn(address,uint256)` and no `burnFrom` — while the protocol repays debt
/// through `burnFrom(address,uint256)`. LiquidToken, which the standard already
/// documents as the base for exactly these tokens, implements the surface the
/// protocol actually calls; the LRC20B-based ones would take deposits and never
/// let the debt be repaid.
///
/// The bridged tier leaves here finished: minting it is a bridge's job, so the
/// only account that can is `OWNER`, and the float this run creates is the
/// owner's too. The liquid tier does not — its markets do not exist yet, so it
/// stays under the deploy key until Grants points each synthetic at its market
/// and hands it over in turn.
contract Tokens is Script {
function run() external {
uint256 pk = vm.envUint("PRIVATE_KEY");
address deployer = vm.addr(pk);
address owner = vm.envAddress("OWNER");
require(owner != deployer, "owner is the deploy key");
// Native coin the treasury wraps, so a market has ERC-20 collateral.
uint256 wrap = vm.envOr("WRAP_NATIVE", uint256(1_000 ether));
// Bridged float the treasury holds, standing in for what a bridge would
// mint. Nothing has actually crossed a bridge, so a chain that means its
// collateral to be real sets this to zero and waits for the bridge.
uint256 bridged = vm.envOr("BRIDGE_FLOAT", uint256(1_000 ether));
vm.startBroadcast(pk);
WLUX wlux = new WLUX();
if (wrap > 0) {
wlux.deposit{value: wrap}();
require(wlux.transfer(owner, wrap), "wrapped LUX did not reach the owner");
}
BridgedETH eth = new BridgedETH();
if (bridged > 0) eth.mint(owner, bridged);
_hand(eth, deployer, owner);
BridgedBTC btc = new BridgedBTC();
if (bridged > 0) btc.mint(owner, bridged / 1e10); // 8 decimals, not 18
_hand(btc, deployer, owner);
// Flash fee at the floor the token enforces (1bp). The markets never
// flash-mint; this only satisfies the constructor's minimum.
LiquidToken llux = new LiquidToken("Liquid LUX", "LLUX", 1);
LiquidToken leth = new LiquidToken("Liquid ETH", "LETH", 1);
LiquidToken lbtc = new LiquidToken("Liquid BTC", "LBTC", 1);
vm.stopBroadcast();
_audit(eth, deployer, owner);
_audit(btc, deployer, owner);
string memory j = "tokens";
vm.serializeAddress(j, "WLUX", address(wlux));
vm.serializeAddress(j, "ETH", address(eth));
vm.serializeAddress(j, "BTC", address(btc));
vm.serializeAddress(j, "LLUX", address(llux));
vm.serializeAddress(j, "LETH", address(leth));
string memory out = vm.serializeAddress(j, "LBTC", address(lbtc));
vm.writeJson(out, string.concat("./out/tokens.", vm.toString(block.chainid), ".json"));
console.log("chain", block.chainid);
console.log("WLUX", address(wlux));
console.log("ETH ", address(eth));
console.log("BTC ", address(btc));
}
/// Moves every power a bridged token grants its deployer over to `owner`.
/// Admin goes last: it is what authorizes the rest.
function _hand(LRC20B token, address deployer, address owner) internal {
token.grantAdmin(owner);
token.grantMinter(owner);
token.transferOwnership(owner);
token.revokeMinter(deployer);
token.revokeAdmin(deployer);
}
function _audit(LRC20B token, address deployer, address owner) internal view {
require(token.owner() == owner, "bridged token is not the owner's");
require(token.hasRole(token.DEFAULT_ADMIN_ROLE(), owner), "owner cannot administer the bridged token");
require(token.hasRole(token.MINTER_ROLE(), owner), "owner cannot bridge the token in");
require(!token.hasRole(token.DEFAULT_ADMIN_ROLE(), deployer), "deploy key can administer the bridged token");
require(!token.hasRole(token.MINTER_ROLE(), deployer), "deploy key can mint collateral");
}
}