feat: expand non-evm relay and route planning support
This commit is contained in:
73
contracts/wrapped-lp-public/Chain138LPLocker.sol
Normal file
73
contracts/wrapped-lp-public/Chain138LPLocker.sol
Normal 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 pool’s 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));
|
||||
}
|
||||
}
|
||||
46
contracts/wrapped-lp-public/PublicChainMintController.sol
Normal file
46
contracts/wrapped-lp-public/PublicChainMintController.sol
Normal 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;
|
||||
}
|
||||
}
|
||||
92
contracts/wrapped-lp-public/README.md
Normal file
92
contracts/wrapped-lp-public/README.md
Normal 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`
|
||||
52
contracts/wrapped-lp-public/WLPNAVOracle.sol
Normal file
52
contracts/wrapped-lp-public/WLPNAVOracle.sol
Normal 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;
|
||||
}
|
||||
}
|
||||
41
contracts/wrapped-lp-public/WLPReceiptToken.sol
Normal file
41
contracts/wrapped-lp-public/WLPReceiptToken.sol
Normal 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);
|
||||
}
|
||||
}
|
||||
33
contracts/wrapped-lp-public/WLPRedemptionGateway.sol
Normal file
33
contracts/wrapped-lp-public/WLPRedemptionGateway.sol
Normal 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);
|
||||
}
|
||||
}
|
||||
41
contracts/wrapped-lp-public/WrappedLPNAVVault.sol
Normal file
41
contracts/wrapped-lp-public/WrappedLPNAVVault.sol
Normal 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;
|
||||
}
|
||||
}
|
||||
14
contracts/wrapped-lp-public/interfaces/IWLPProgramEvents.sol
Normal file
14
contracts/wrapped-lp-public/interfaces/IWLPProgramEvents.sol
Normal 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);
|
||||
}
|
||||
Reference in New Issue
Block a user