Skip to content
Draft
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
42 changes: 39 additions & 3 deletions contracts/contracts/mocks/crosschainV3/MockOTokenVault.sol
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.0;

import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";

import { MockMintableBurnableOToken } from "./MockMintableBurnableOToken.sol";

interface IStrategyForMock {
Expand All @@ -19,12 +22,19 @@ interface IStrategyForMock {

/**
* @title MockOTokenVault
* @notice TEST-ONLY minimal vault that exposes `mintForStrategy` / `burnForStrategy` to
* whitelisted strategies for the V3 strategy unit tests. Skips all the real Vault
* surface area (assets registry, allocate, redeem queue, rebase, etc.).
* @notice TEST-ONLY minimal vault that mirrors the production VaultCore user surface
* (`mint`/`redeem` against a bridge asset 1:1) plus the strategy surface
* (`mintForStrategy` / `burnForStrategy`) used by the V3 strategy.
*
* Skips the rest of the real Vault surface area (assets registry, allocate,
* redeem queue, rebase, etc.). Mock OToken is plain ERC-20 — no rebasing on
* testnet; observe yield via mock wOETH share rate or `remoteStrategyBalance`.
*/
contract MockOTokenVault {
using SafeERC20 for IERC20;

MockMintableBurnableOToken public oToken;
address public bridgeAsset;
mapping(address => bool) public isMintWhitelistedStrategy;
address public strategistAddr;

Expand All @@ -35,10 +45,36 @@ contract MockOTokenVault {
oToken = _oToken;
}

function setBridgeAsset(address _bridgeAsset) external {
bridgeAsset = _bridgeAsset;
}

function setStrategistAddr(address _strategist) external {
strategistAddr = _strategist;
}

// --- Production-mirror user surface ------------------------------------
// `mint(amount)` pulls bridgeAsset from caller, mints OToken to caller 1:1.
// `redeem(amount, minAmount)` burns caller's OToken, returns bridgeAsset 1:1.
// Mirrors MockEthOTokenVault on Sepolia and the production VaultCore.mint flow.

function mint(uint256 _amount) external {
require(bridgeAsset != address(0), "MockVault: bridge asset not set");
IERC20(bridgeAsset).safeTransferFrom(
msg.sender,
address(this),
_amount
);
oToken.mint(msg.sender, _amount);
}

function redeem(uint256 _amount, uint256 _minAmount) external {
require(bridgeAsset != address(0), "MockVault: bridge asset not set");
require(_amount >= _minAmount, "MockVault: below min");
oToken.burn(msg.sender, _amount);
IERC20(bridgeAsset).safeTransfer(msg.sender, _amount);
}

function whitelistStrategy(address _strategy) external {
isMintWhitelistedStrategy[_strategy] = true;
emit StrategyWhitelisted(_strategy);
Expand Down
58 changes: 58 additions & 0 deletions contracts/deploy/baseSepolia/001_mock_oethb.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
/**
* Base Sepolia testnet (Master side) — mock OETHb + mock L2 vault.
*
* Stands in for the production OETHb / OETHBaseVault on Base. The Master
* strategy's only interaction with the vault is `mintForStrategy` /
* `burnForStrategy` (bridge channel) and `Withdrawal` events; the mock
* implements just that surface area.
*/
module.exports = async (hre) => {
const { ethers, deployments, getNamedAccounts } = hre;
const { deploy } = deployments;
const { deployerAddr } = await getNamedAccounts();

console.log(`[baseSepolia] 001_mock_oethb — deployer=${deployerAddr}`);

// Deploy MockOTokenVault first (OToken constructor needs vault address).
const dVault = await deploy("MockOETHbVault", {
from: deployerAddr,
contract: "MockOTokenVault",
args: [],
log: true,
});
console.log(`MockOETHbVault: ${dVault.address}`);

// Deploy MockMintableBurnableOToken (OETHb).
const dOToken = await deploy("MockOETHb", {
from: deployerAddr,
contract: "MockMintableBurnableOToken",
args: ["Mock OETHb", "mOETHb", dVault.address],
log: true,
});
console.log(`MockOETHb: ${dOToken.address}`);

// Wire the vault to the OToken (one-time setup; mock has no access control).
const sDeployer = await ethers.provider.getSigner(deployerAddr);
const cVault = await ethers.getContractAt(
"MockOTokenVault",
dVault.address,
sDeployer
);
const currentOToken = await cVault.oToken();
if (currentOToken === ethers.constants.AddressZero) {
const tx = await cVault.setOToken(dOToken.address);
await tx.wait();
console.log("Wired vault.oToken = MockOETHb");
} else {
console.log(`Vault already wired (oToken=${currentOToken})`);
}

return true;
};

module.exports.id = "baseSepolia_001_mock_oethb";
module.exports.tags = ["baseSepolia"];
module.exports.skip = async () => {
const hre = require("hardhat");
return hre.network.name !== "baseSepolia";
};
136 changes: 136 additions & 0 deletions contracts/deploy/baseSepolia/002_master_strategy.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
/**
* Base Sepolia testnet (Master side) — MasterWOTokenStrategy impl + proxy.
*
* Proxy deployed via CreateX so its address matches the Remote proxy on
* Sepolia (CREATE3 peer parity is REQUIRED — the adapters dispatch inbound
* messages to `envelopeSender`, which is the source-side strategy address,
* and that address must resolve to the destination strategy on the peer
* chain).
*
* The deployer also acts as governor + operator on testnet so initialization
* runs in a single tx.
*/
const addresses = require("../../utils/addresses");
const { encodeSaltForCreateX } = require("../../utils/deploy");
const createxAbi = require("../../abi/createx.json");

// Salt for the OETHb wOETH V3 testnet strategy pair. See
// `deploy/base/100_oethb_v3_master_proxy.js` for the salt-naming convention
// (same salt on paired chains, different between testnet and production).
const SALT = "OETHb V3 Testnet wOETH Strategy 1";

module.exports = async (hre) => {
const { ethers, deployments, getNamedAccounts } = hre;
const { deploy } = deployments;
const { deployerAddr } = await getNamedAccounts();
const sDeployer = await ethers.provider.getSigner(deployerAddr);

console.log(`[baseSepolia] 002_master_strategy — deployer=${deployerAddr}`);

// --- 1. Deploy strategy proxy at deterministic CreateX address ---
// Same logic as deployProxyWithCreateX in deployActions.js, inlined so testnet
// doesn't depend on the production governance plumbing.
const cCreateX = await ethers.getContractAt(createxAbi, addresses.createX);
// Fixed "originprotocol" identifier as the salt-prefix address — keeps the salt
// identical to what the Sepolia (Remote) side would compute.
const addrForSalt = "0x0000000000006f726967696e70726f746f636f6c";
const encodedSalt = encodeSaltForCreateX(addrForSalt, false, SALT);

const ProxyFactory = await ethers.getContractFactory(
"CrossChainStrategyProxy"
);
const proxyInitCode = ethers.utils.hexConcat([
ProxyFactory.bytecode,
ProxyFactory.interface.encodeDeploy([deployerAddr]),
]);

// CreateX `_guard` for our "originprotocol" salt prefix (neither msg.sender
// nor address(0) for the first 20 bytes) hits the else branch:
// guardedSalt = keccak256(abi.encode(salt)) == keccak256(salt) (bytes32)
const guardedSalt = ethers.utils.keccak256(encodedSalt);
const predictedProxyAddr = await cCreateX[
"computeCreate2Address(bytes32,bytes32)"
](guardedSalt, ethers.utils.keccak256(proxyInitCode));
console.log(`Predicted proxy address: ${predictedProxyAddr}`);

const proxyCode = await ethers.provider.getCode(predictedProxyAddr);
let proxyAddress = predictedProxyAddr;
if (proxyCode === "0x") {
const tx = await cCreateX
.connect(sDeployer)
.deployCreate2(encodedSalt, proxyInitCode);
const receipt = await tx.wait();
const ContractCreationTopic =
"0xb8fda7e00c6b06a2b54e58521bc5894fee35f1090e5a3bb6390bfe2b98b497f7";
proxyAddress = ethers.utils.getAddress(
`0x${receipt.events
.find((e) => e.topics[0] === ContractCreationTopic)
.topics[1].slice(26)}`
);
console.log(`Deployed MasterWOTokenStrategyProxy at ${proxyAddress}`);
} else {
console.log(`Proxy already deployed at ${proxyAddress}`);
}

// Persist the address under a deployment artefact so subsequent scripts can
// resolve it via deployments.get(...). Use the standard hardhat-deploy save.
await deployments.save("MasterWOTokenStrategyProxy", {
address: proxyAddress,
abi: ProxyFactory.interface.format("json"),
});

// --- 2. Deploy Master impl ---
const dVault = await deployments.get("MockOETHbVault");
const dOToken = await deployments.get("MockOETHb");

const dMasterImpl = await deploy("MasterWOTokenStrategy", {
from: deployerAddr,
args: [
{
platformAddress: ethers.constants.AddressZero,
vaultAddress: dVault.address,
},
addresses.baseSepolia.WETH,
dOToken.address,
],
log: true,
});
console.log(`MasterWOTokenStrategy impl: ${dMasterImpl.address}`);

// --- 3. Initialise the proxy ---
const cProxy = await ethers.getContractAt(
"InitializeGovernedUpgradeabilityProxy",
proxyAddress,
sDeployer
);
const implOnProxy = await cProxy.implementation();
if (implOnProxy === ethers.constants.AddressZero) {
const cMasterImpl = await ethers.getContractAt(
"MasterWOTokenStrategy",
dMasterImpl.address
);
const initData = cMasterImpl.interface.encodeFunctionData(
"initialize(address)",
[deployerAddr] // operator = deployer on testnet
);
const tx = await cProxy["initialize(address,address,bytes)"](
dMasterImpl.address,
deployerAddr, // governor = deployer on testnet
initData
);
await tx.wait();
console.log(`Initialised proxy → impl + governor + operator = deployer`);
} else {
console.log(`Proxy already initialised (impl=${implOnProxy})`);
}

return true;
};

module.exports.id = "baseSepolia_002_master_strategy";
module.exports.tags = ["baseSepolia"];
module.exports.dependencies = ["baseSepolia_001_mock_oethb"];
module.exports.skip = async () => {
const hre = require("hardhat");
return hre.network.name !== "baseSepolia";
};
72 changes: 72 additions & 0 deletions contracts/deploy/baseSepolia/003_adapters.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
/**
* Base Sepolia testnet (Master side) — adapter deployments.
*
* Each adapter is deployed BEHIND a `BridgeAdapterProxy` via CREATE3. The
* proxy gets a deterministic address (because its initcode contains only a
* fixed governor placeholder), matching the Sepolia (Remote) side. The impl
* is deployed plain with chain-specific constructor args (CCIPRouter,
* L1StandardBridge, WETH). Impl addresses differ across chains but only the
* proxy is part of the adapter's `transportSender == address(this)`
* peer-parity check, so that's fine.
*
* Routing:
* - CCIPAdapter — outbound (B→E for the yield channel, B→E for bridge channel)
* - SuperbridgeAdapter L2-mode — inbound (E→B; L1StandardBridge unused on this side)
*/
const addresses = require("../../utils/addresses");
const {
deployBridgeAdapterProxy,
initBridgeAdapterProxy,
} = require("../../utils/createXProxyHelper");

const CCIP_PROXY_SALT = "OETHb V3 Testnet CCIPAdapter Proxy 1";
const SUPER_PROXY_SALT = "OETHb V3 Testnet SuperbridgeAdapter Proxy 1";

module.exports = async (hre) => {
const { ethers, deployments } = hre;
const { deploy } = deployments;
const { deployerAddr } = await hre.getNamedAccounts();
console.log(`[baseSepolia] 003_adapters — deployer=${deployerAddr}`);

// --- 1. CCIPAdapter impl + proxy ---
const dCCIPImpl = await deploy("CCIPAdapter", {
from: deployerAddr,
args: [addresses.baseSepolia.CCIPRouter],
log: true,
});
console.log(`CCIPAdapter impl: ${dCCIPImpl.address}`);
const ccipProxyAddr = await deployBridgeAdapterProxy(
hre,
"CCIPAdapter",
CCIP_PROXY_SALT
);
await initBridgeAdapterProxy(hre, ccipProxyAddr, dCCIPImpl.address);

// --- 2. SuperbridgeAdapter impl + proxy (L2 mode: _l1 = 0) ---
const dSuperImpl = await deploy("SuperbridgeAdapter", {
from: deployerAddr,
args: [
ethers.constants.AddressZero,
addresses.baseSepolia.CCIPRouter,
addresses.baseSepolia.WETH,
],
log: true,
});
console.log(`SuperbridgeAdapter impl: ${dSuperImpl.address}`);
const superProxyAddr = await deployBridgeAdapterProxy(
hre,
"SuperbridgeAdapter",
SUPER_PROXY_SALT
);
await initBridgeAdapterProxy(hre, superProxyAddr, dSuperImpl.address);

return true;
};

module.exports.id = "baseSepolia_003_adapters";
module.exports.tags = ["baseSepolia"];
module.exports.dependencies = ["baseSepolia_002_master_strategy"];
module.exports.skip = async () => {
const hre = require("hardhat");
return hre.network.name !== "baseSepolia";
};
Loading
Loading