feat: expand non-evm relay and route planning support

This commit is contained in:
defiQUG
2026-04-18 12:05:34 -07:00
parent da78073104
commit 843cdbf71c
113 changed files with 8542 additions and 222 deletions

View File

@@ -0,0 +1,73 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import "@openzeppelin/contracts/access/AccessControl.sol";
import "./interfaces/IWLPProgramEvents.sol";
/**
* @title Chain138LPLocker
* @notice Escrows DODO PMM LP ERC20 on Chain 138; releases only to BRIDGE_RELEASE_ROLE (Option A).
* @dev Per-lock `lockRef` is used as the cross-chain idempotency key for destination mint.
* One deployment typically corresponds to one `lpToken` (one pools LP token).
*/
contract Chain138LPLocker is AccessControl, IWLPProgramEvents {
using SafeERC20 for IERC20;
bytes32 public constant BRIDGE_RELEASE_ROLE = keccak256("BRIDGE_RELEASE_ROLE");
IERC20 public immutable lpToken;
struct Deposit {
address depositor;
uint256 amount;
bool released;
}
uint256 public lockCounter;
uint256 public totalEscrowed;
mapping(bytes32 => Deposit) public deposits;
constructor(address lpToken_, address admin) {
require(lpToken_ != address(0) && admin != address(0), "Locker: zero");
lpToken = IERC20(lpToken_);
_grantRole(DEFAULT_ADMIN_ROLE, admin);
}
/**
* @notice Lock LP into escrow; `lockRef` must be relayed to the public chain for mint.
*/
function deposit(uint256 amount) external returns (bytes32 lockRef) {
require(amount > 0, "Locker: zero amount");
lpToken.safeTransferFrom(msg.sender, address(this), amount);
lockRef = keccak256(abi.encode(block.chainid, address(this), lockCounter++, msg.sender, amount));
deposits[lockRef] = Deposit({depositor: msg.sender, amount: amount, released: false});
totalEscrowed += amount;
emit LPLocked(lockRef, msg.sender, amount, address(lpToken));
}
/**
* @notice Release one full lock to `to` after redemption message (or operational unwind).
*/
function releaseLock(bytes32 lockRef, address to) external onlyRole(BRIDGE_RELEASE_ROLE) {
Deposit storage d = deposits[lockRef];
require(d.amount > 0 && !d.released, "Locker: bad lock");
d.released = true;
uint256 amt = d.amount;
totalEscrowed -= amt;
lpToken.safeTransfer(to, amt);
emit LPReleased(lockRef, to, amt, address(lpToken));
}
/**
* @notice Aggregate release for pro-rata / FIFO policies (requires off-chain ordering).
*/
function releaseAmount(address to, uint256 amount) external onlyRole(BRIDGE_RELEASE_ROLE) {
require(amount > 0 && amount <= totalEscrowed, "Locker: amount");
totalEscrowed -= amount;
lpToken.safeTransfer(to, amount);
emit LPReleased(bytes32(0), to, amount, address(lpToken));
}
}

View File

@@ -0,0 +1,46 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "@openzeppelin/contracts/access/AccessControl.sol";
import "./WLPReceiptToken.sol";
import "./interfaces/IWLPProgramEvents.sol";
/**
* @title PublicChainMintController
* @notice Destination-chain controller: mints `wLP` once per `lockRef` (replay protection).
* @dev Relayer must verify Chain 138 `LPLocked` event / attestation off-chain or via future ZK proof.
*/
contract PublicChainMintController is AccessControl, IWLPProgramEvents {
bytes32 public constant RELAYER_ROLE = keccak256("RELAYER_ROLE");
WLPReceiptToken public immutable wlp;
/// @notice Optional Chain 138 locker address for documentation / future cross-verify hooks.
address public immutable chain138Locker;
mapping(bytes32 => bool) public mintedForLock;
bool public mintPaused;
constructor(address wlp_, address chain138Locker_, address admin) {
require(wlp_ != address(0) && admin != address(0), "MintCtl: zero");
wlp = WLPReceiptToken(wlp_);
chain138Locker = chain138Locker_;
_grantRole(DEFAULT_ADMIN_ROLE, admin);
}
/**
* @notice Idempotent mint keyed by `lockRef` (must match Chain 138 locker emission).
*/
function mintForLock(bytes32 lockRef, address recipient, uint256 amount) external onlyRole(RELAYER_ROLE) {
require(!mintPaused, "MintCtl: paused");
require(lockRef != bytes32(0), "MintCtl: zero lockRef");
require(recipient != address(0) && amount > 0, "MintCtl: bad args");
require(!mintedForLock[lockRef], "MintCtl: replay");
mintedForLock[lockRef] = true;
wlp.mint(recipient, amount);
emit WLPMinted(lockRef, recipient, amount);
}
function setMintPaused(bool paused) external onlyRole(DEFAULT_ADMIN_ROLE) {
mintPaused = paused;
}
}

View File

@@ -0,0 +1,92 @@
# Wrapped LP / Public Chain Program (Scaffold)
Solidity for **Option A** (lock → mint `wLP` → redeem) and **Option B** (ERC-4626 vault + NAV oracle). This is a **new vertical**: **not** an extension of `CWMultiTokenBridgeL1/L2` or catalog **cW*** CCIP paths.
---
## Documentation (exhaustive index)
| Doc | Description |
|-----|-------------|
| [WRAPPED_LP_PROGRAM_REFERENCE.md](../../../docs/04-configuration/WRAPPED_LP_PROGRAM_REFERENCE.md) | **Master index**, invariants, glossary, phase gates |
| [ADR_WRAPPED_LP_VAULT_PUBLIC_CHAIN.md](../../../docs/03-deployment/ADR_WRAPPED_LP_VAULT_PUBLIC_CHAIN.md) | Architecture decisions, alternatives |
| [WRAPPED_LP_MESSAGE_SCHEMA.md](../../../docs/04-configuration/WRAPPED_LP_MESSAGE_SCHEMA.md) | `lockRef`, payloads, CCIP, redemption |
| [WRAPPED_LP_ORACLE_STACK.md](../../../docs/04-configuration/WRAPPED_LP_ORACLE_STACK.md) | Two-price model, keepers, lending |
| [WRAPPED_LP_LENDING_GOVERNANCE.md](../../../docs/04-configuration/WRAPPED_LP_LENDING_GOVERNANCE.md) | Venues, listing, risk matrix |
| [WRAPPED_LP_OPERATIONS_RUNBOOK.md](../../../docs/03-deployment/WRAPPED_LP_OPERATIONS_RUNBOOK.md) | SLOs, RACI, incidents |
| [WRAPPED_LP_AUDIT_CHECKLIST.md](../../../docs/03-deployment/WRAPPED_LP_AUDIT_CHECKLIST.md) | Auditor scope, threats |
---
## Contracts
| File | Role |
|------|------|
| `Chain138LPLocker.sol` | Escrow LP on 138; `lockRef`; `releaseLock` / `releaseAmount` |
| `WLPReceiptToken.sol` | Public-chain `wLP` (MINTER/BURNER) |
| `PublicChainMintController.sol` | Idempotent `mintForLock`; `mintPaused` |
| `WLPRedemptionGateway.sol` | Burn + `RedemptionRequested` |
| `WrappedLPNAVVault.sol` | ERC-4626 + `depositCap` |
| `WLPNAVOracle.sol` | USD-style feed, heartbeat, breaker, `isStale()` |
| `interfaces/IWLPProgramEvents.sol` | Shared events |
---
## Build / test (scoped Forge)
Full-repo `forge test` may fail on legacy **0.5.x/0.6.x** vendor trees; always use **scope**:
```bash
cd smom-dbis-138
bash scripts/forge/scope.sh build wrapped-lp-public
bash scripts/forge/scope.sh test wrapped-lp-public --match-path 'test/wrapped-lp-public/*.t.sol'
```
---
## Deployment
| Script | Use |
|--------|-----|
| [DeployWrappedLPLockerChain138.s.sol](../../script/wrapped-lp-public/DeployWrappedLPLockerChain138.s.sol) | **Chain 138 only**`Chain138LPLocker` |
| [DeployWrappedLPPublicChain.s.sol](../../script/wrapped-lp-public/DeployWrappedLPPublicChain.s.sol) | **Public chain** — wLP, mint controller, gateway, oracle, optional vault |
| [DeployWrappedLPProgram.s.sol](../../script/wrapped-lp-public/DeployWrappedLPProgram.s.sol) | **Single-chain / Anvil** — full stack on one RPC (not production two-chain) |
Production: two `forge script` runs (138 RPC then destination RPC). See [WRAPPED_LP_DEPLOYMENT_RUNBOOK.md](../../../docs/03-deployment/WRAPPED_LP_DEPLOYMENT_RUNBOOK.md).
### Post-deploy role wiring (required)
| Grant | To |
|-------|-----|
| `WLPReceiptToken.MINTER_ROLE` | `PublicChainMintController` |
| `WLPReceiptToken.BURNER_ROLE` | `WLPRedemptionGateway` |
| `PublicChainMintController.RELAYER_ROLE` | Relayer worker(s) |
| `Chain138LPLocker.BRIDGE_RELEASE_ROLE` | Relayer (138 release path) |
| `WLPNAVOracle.KEEPER_ROLE` | Price keeper |
Use **multisig** for `DEFAULT_ADMIN_ROLE` on production.
### Environment variables (deploy script)
| Variable | Purpose |
|----------|---------|
| `LP_TOKEN` | DODO LP ERC20 on 138 |
| `ADMIN` | Multisig / admin |
| `PRIVATE_KEY` | Broadcast key (lab only) |
| `USDC` | Optional; if set, deploys `WrappedLPNAVVault` |
---
## Limitations (read before mainnet)
1. **Relayer trust:** Mint path assumes **honest** relayer until ZK/attestation hardening.
2. **Fungible wLP:** Secondary trading breaks **simple** per-lock redemption; see message schema doc.
3. **Vault:** `totalAssets()` follows **ERC-20 balance**; off-chain strategy on 138 is **operational**.
---
## Repo context
- PMM pools: `docs/11-references/DEPLOYED_TOKENS_BRIDGES_LPS_AND_ROUTING_STATUS.md` §4.1
- cW bridge **pattern only**: `docs/07-ccip/CW_BRIDGE_APPROACH.md`
- GRU policy **orthogonality**: `docs/04-configuration/GRU_REFERENCE_PRIMACY_AND_MESH_EXECUTION_MODEL.md`

View File

@@ -0,0 +1,52 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "@openzeppelin/contracts/access/AccessControl.sol";
/**
* @title WLPNAVOracle
* @notice Chainlink-style read interface for wLP / vault share price in USD (8 decimals) for lending adapters.
* @dev Keeper updates `latestAnswer` + `updatedAt`. Consumers must check `isStale()` before liquidations.
*/
contract WLPNAVOracle is AccessControl {
bytes32 public constant KEEPER_ROLE = keccak256("KEEPER_ROLE");
/// @notice Price with 8 decimals (Chainlink USD convention).
int256 public latestAnswer;
uint256 public updatedAt;
uint256 public heartbeat;
bool public circuitBreaker;
event AnswerUpdated(int256 indexed answer, uint256 updatedAt, address indexed keeper);
event HeartbeatUpdated(uint256 heartbeat);
event CircuitBreakerSet(bool tripped);
constructor(address admin, uint256 heartbeat_) {
require(admin != address(0), "NAV: zero admin");
heartbeat = heartbeat_;
_grantRole(DEFAULT_ADMIN_ROLE, admin);
}
function setHeartbeat(uint256 h) external onlyRole(DEFAULT_ADMIN_ROLE) {
heartbeat = h;
emit HeartbeatUpdated(h);
}
function setCircuitBreaker(bool on) external onlyRole(DEFAULT_ADMIN_ROLE) {
circuitBreaker = on;
emit CircuitBreakerSet(on);
}
function submitAnswer(int256 answer) external onlyRole(KEEPER_ROLE) {
require(!circuitBreaker, "NAV: breaker");
latestAnswer = answer;
updatedAt = block.timestamp;
emit AnswerUpdated(answer, updatedAt, msg.sender);
}
function isStale() external view returns (bool) {
if (circuitBreaker) return true;
if (heartbeat == 0) return false;
return block.timestamp > updatedAt + heartbeat;
}
}

View File

@@ -0,0 +1,41 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/access/AccessControl.sol";
/**
* @title WLPReceiptToken
* @notice Fungible receipt token for Chain 138 LP exposure minted on a public chain (Option A).
* @dev Only addresses with MINTER_ROLE may mint; BURNER_ROLE for redemption gateway / bridge burn paths.
* Not a cW* token; separate from CompliantWrappedToken governance surface.
*/
contract WLPReceiptToken is ERC20, AccessControl {
bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE");
bytes32 public constant BURNER_ROLE = keccak256("BURNER_ROLE");
uint8 private immutable _decimalsOverride;
constructor(
string memory name_,
string memory symbol_,
uint8 decimals_,
address admin
) ERC20(name_, symbol_) {
require(admin != address(0), "WLP: zero admin");
_decimalsOverride = decimals_;
_grantRole(DEFAULT_ADMIN_ROLE, admin);
}
function decimals() public view override returns (uint8) {
return _decimalsOverride;
}
function mint(address to, uint256 amount) external onlyRole(MINTER_ROLE) {
_mint(to, amount);
}
function burn(address from, uint256 amount) external onlyRole(BURNER_ROLE) {
_burn(from, amount);
}
}

View File

@@ -0,0 +1,33 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "@openzeppelin/contracts/access/AccessControl.sol";
import "./WLPReceiptToken.sol";
import "./interfaces/IWLPProgramEvents.sol";
/**
* @title WLPRedemptionGateway
* @notice Burns wLP from user and emits `RedemptionRequested` for relayers to release LP on Chain 138.
* @dev Relayer observes event, submits `Chain138LPLocker.release*` on 138. Fungible wLP + secondary trading
* may break 1:1 depositor-only redemption; production may require NFT receipts or curated policy.
*/
contract WLPRedemptionGateway is AccessControl, IWLPProgramEvents {
bytes32 public constant BURNER_ROLE = keccak256("BURNER_ROLE");
WLPReceiptToken public immutable wlp;
constructor(address wlp_, address admin) {
require(wlp_ != address(0) && admin != address(0), "Gateway: zero");
wlp = WLPReceiptToken(wlp_);
_grantRole(DEFAULT_ADMIN_ROLE, admin);
}
/**
* @notice User burns wLP; relayer fulfills LP release on 138 per operational policy.
*/
function requestRedeem(uint256 wlpAmount) external {
require(wlpAmount > 0, "Gateway: zero");
wlp.burn(msg.sender, wlpAmount);
emit RedemptionRequested(msg.sender, wlpAmount);
}
}

View File

@@ -0,0 +1,41 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/token/ERC20/extensions/ERC4626.sol";
import "@openzeppelin/contracts/access/AccessControl.sol";
/**
* @title WrappedLPNAVVault
* @notice Option B scaffold: standard ERC-4626 vault over an underlying asset (e.g. USDC on mainnet).
* @dev Strategy deployment of vault assets into Chain 138 LP is **operational** (outside this contract).
* Use `depositCap` for soft-launch; seed initial deposit to mitigate donation attacks per OZ ERC4626 docs.
*/
contract WrappedLPNAVVault is ERC4626, AccessControl {
bytes32 public constant CAP_ADMIN_ROLE = keccak256("CAP_ADMIN_ROLE");
uint256 public depositCap;
constructor(
IERC20 asset_,
string memory name_,
string memory symbol_,
address admin
) ERC20(name_, symbol_) ERC4626(asset_) {
require(admin != address(0), "Vault: zero admin");
_grantRole(DEFAULT_ADMIN_ROLE, admin);
_grantRole(CAP_ADMIN_ROLE, admin);
}
function maxDeposit(address) public view override returns (uint256) {
uint256 cap = depositCap;
if (cap == 0) return type(uint256).max;
uint256 a = totalAssets();
if (a >= cap) return 0;
return cap - a;
}
function setDepositCap(uint256 cap) external onlyRole(CAP_ADMIN_ROLE) {
depositCap = cap;
}
}

View File

@@ -0,0 +1,14 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
/**
* @title IWLPProgramEvents
* @notice Canonical events for wrapped-LP / bridge attestation flows (Option A).
* @dev `lockRef` is the idempotency key for destination mint (replay protection).
*/
interface IWLPProgramEvents {
event LPLocked(bytes32 indexed lockRef, address indexed depositor, uint256 amount, address indexed lpToken);
event LPReleased(bytes32 indexed lockRef, address indexed to, uint256 amount, address indexed lpToken);
event WLPMinted(bytes32 indexed lockRef, address indexed recipient, uint256 amount);
event RedemptionRequested(address indexed holder, uint256 wlpAmount);
}