WIP: Chain138 deployment scripts, flash receivers, HYBX OMNL recovery

This commit is contained in:
defiQUG
2026-06-02 06:09:44 -07:00
parent e1560a880b
commit f04a7cb7c8
35 changed files with 2279 additions and 83 deletions

View File

@@ -4,7 +4,9 @@ pragma solidity ^0.8.19;
import {Test} from "forge-std/Test.sol";
import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import {IRouterClient} from "../../contracts/ccip/IRouterClient.sol";
import {IRouterClient} from "@chainlink/contracts-ccip/contracts/interfaces/IRouterClient.sol";
import {Client} from "@chainlink/contracts-ccip/contracts/libraries/Client.sol";
import {IRouterClient as LegacyRouterClient} from "../../contracts/ccip/IRouterClient.sol";
import {CWMultiTokenBridgeL1} from "../../contracts/bridge/CWMultiTokenBridgeL1.sol";
import {CWMultiTokenBridgeL2} from "../../contracts/bridge/CWMultiTokenBridgeL2.sol";
import {CompliantWrappedToken} from "../../contracts/tokens/CompliantWrappedToken.sol";
@@ -21,7 +23,7 @@ contract MockCanonicalToken is ERC20 {
contract MockRouter is IRouterClient {
uint256 public fee;
bytes32 public nextMessageId = keccak256("message");
EVM2AnyMessage internal _lastMessage;
Client.EVM2AnyMessage internal _lastMessage;
uint64 public lastDestinationChainSelector;
function setFee(uint256 newFee) external {
@@ -30,31 +32,19 @@ contract MockRouter is IRouterClient {
function ccipSend(
uint64 destinationChainSelector,
EVM2AnyMessage memory message
) external payable returns (bytes32 messageId, uint256 fees) {
fees = fee;
Client.EVM2AnyMessage memory message
) external payable returns (bytes32 messageId) {
if (message.feeToken == address(0)) {
require(msg.value >= fees, "native fee");
require(msg.value >= fee, "native fee");
}
lastDestinationChainSelector = destinationChainSelector;
_lastMessage = message;
emit MessageSent(
nextMessageId,
destinationChainSelector,
msg.sender,
message.receiver,
message.data,
message.tokenAmounts,
message.feeToken,
message.extraArgs
);
return (nextMessageId, fees);
messageId = nextMessageId;
return messageId;
}
function getFee(uint64, EVM2AnyMessage memory) external view returns (uint256) {
function getFee(uint64, Client.EVM2AnyMessage memory) external view returns (uint256) {
return fee;
}
@@ -145,7 +135,9 @@ contract CWMultiTokenBridgeTest is Test {
assertEq(l2Bridge.mintedTotal(address(wrapped)), amount);
assertEq(l2Bridge.burnedTotal(address(wrapped)), amount);
(, bytes memory returnData,,) = routerAvax.lastMessage();
(, bytes memory returnData, address feeToken, bytes memory extraArgs) = routerAvax.lastMessage();
assertTrue(extraArgs.length > 0, "missing CCIP extraArgs");
assertEq(feeToken, address(0));
vm.prank(address(0x138138));
l1Bridge.ccipReceive(_message(returnMessageId, AVALANCHE_SELECTOR, address(l2Bridge), returnData));
@@ -308,8 +300,8 @@ contract CWMultiTokenBridgeTest is Test {
relayRouter.authorizeBridge(address(receiveBridge));
relayRouter.grantRelayerRole(address(this));
IRouterClient.TokenAmount[] memory noTokens = new IRouterClient.TokenAmount[](0);
IRouterClient.Any2EVMMessage memory message = IRouterClient.Any2EVMMessage({
LegacyRouterClient.TokenAmount[] memory noTokens = new LegacyRouterClient.TokenAmount[](0);
LegacyRouterClient.Any2EVMMessage memory message = LegacyRouterClient.Any2EVMMessage({
messageId: keccak256("three-field"),
sourceChainSelector: CHAIN138_SELECTOR,
sender: abi.encode(address(0xCAFE)),
@@ -326,14 +318,13 @@ contract CWMultiTokenBridgeTest is Test {
uint64 sourceChainSelector,
address sender,
bytes memory data
) internal pure returns (IRouterClient.Any2EVMMessage memory message) {
IRouterClient.TokenAmount[] memory noTokens = new IRouterClient.TokenAmount[](0);
message = IRouterClient.Any2EVMMessage({
) internal pure returns (Client.Any2EVMMessage memory message) {
message = Client.Any2EVMMessage({
messageId: messageId,
sourceChainSelector: sourceChainSelector,
sender: abi.encode(sender),
data: data,
tokenAmounts: noTokens
destTokenAmounts: new Client.EVMTokenAmount[](0)
});
}
}

View File

@@ -4,7 +4,8 @@ pragma solidity ^0.8.20;
import {Test} from "forge-std/Test.sol";
import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import {IRouterClient} from "../../contracts/ccip/IRouterClient.sol";
import {IRouterClient} from "@chainlink/contracts-ccip/contracts/interfaces/IRouterClient.sol";
import {Client} from "@chainlink/contracts-ccip/contracts/libraries/Client.sol";
import {CWMultiTokenBridgeL1} from "../../contracts/bridge/CWMultiTokenBridgeL1.sol";
import {CWMultiTokenBridgeL2} from "../../contracts/bridge/CWMultiTokenBridgeL2.sol";
import {CompliantWrappedToken} from "../../contracts/tokens/CompliantWrappedToken.sol";
@@ -23,18 +24,18 @@ contract MockCanonicalBTC is ERC20 {
contract MockRouterBTC is IRouterClient {
bytes32 public nextMessageId = keccak256("btc-message");
EVM2AnyMessage internal _lastMessage;
Client.EVM2AnyMessage internal _lastMessage;
function ccipSend(
uint64 destinationChainSelector,
EVM2AnyMessage memory message
) external payable returns (bytes32 messageId, uint256 fees) {
Client.EVM2AnyMessage memory message
) external payable returns (bytes32 messageId) {
destinationChainSelector;
_lastMessage = message;
return (nextMessageId, fees);
return nextMessageId;
}
function getFee(uint64, EVM2AnyMessage memory) external pure returns (uint256) {
function getFee(uint64, Client.EVM2AnyMessage memory) external pure returns (uint256) {
return 0;
}
@@ -122,14 +123,13 @@ contract CWMultiTokenBridgeBTCTest is Test {
uint64 sourceChainSelector,
address sender,
bytes memory data
) internal pure returns (IRouterClient.Any2EVMMessage memory message) {
IRouterClient.TokenAmount[] memory noTokens = new IRouterClient.TokenAmount[](0);
message = IRouterClient.Any2EVMMessage({
) internal pure returns (Client.Any2EVMMessage memory message) {
message = Client.Any2EVMMessage({
messageId: messageId,
sourceChainSelector: sourceChainSelector,
sender: abi.encode(sender),
data: data,
tokenAmounts: noTokens
destTokenAmounts: new Client.EVMTokenAmount[](0)
});
}
}

View File

@@ -3,7 +3,8 @@ pragma solidity ^0.8.20;
import {Test} from "forge-std/Test.sol";
import {IRouterClient} from "../../contracts/ccip/IRouterClient.sol";
import {IRouterClient} from "@chainlink/contracts-ccip/contracts/interfaces/IRouterClient.sol";
import {Client} from "@chainlink/contracts-ccip/contracts/libraries/Client.sol";
import {OfficialStableMirrorToken} from "../../contracts/tokens/OfficialStableMirrorToken.sol";
import {CompliantUSDCTokenV2} from "../../contracts/tokens/CompliantUSDCTokenV2.sol";
import {CompliantUSDTTokenV2} from "../../contracts/tokens/CompliantUSDTTokenV2.sol";
@@ -16,19 +17,19 @@ import {CompliantWrappedToken} from "../../contracts/tokens/CompliantWrappedToke
contract MockRouterVaultVerifierV2 is IRouterClient {
uint256 public fee;
bytes32 public nextMessageId = keccak256("cw-reserve-vault-v2-message");
EVM2AnyMessage internal _lastMessage;
Client.EVM2AnyMessage internal _lastMessage;
uint64 public lastDestinationChainSelector;
function ccipSend(
uint64 destinationChainSelector,
EVM2AnyMessage memory message
) external payable returns (bytes32 messageId, uint256 fees) {
Client.EVM2AnyMessage memory message
) external payable returns (bytes32 messageId) {
lastDestinationChainSelector = destinationChainSelector;
_lastMessage = message;
return (nextMessageId, fee);
return nextMessageId;
}
function getFee(uint64, EVM2AnyMessage memory) external view returns (uint256) {
function getFee(uint64, Client.EVM2AnyMessage memory) external view returns (uint256) {
return fee;
}
@@ -182,14 +183,13 @@ contract CWReserveVerifierVaultV2IntegrationTest is Test {
uint64 sourceChainSelector,
address sender,
bytes memory data
) internal pure returns (IRouterClient.Any2EVMMessage memory message) {
IRouterClient.TokenAmount[] memory noTokens = new IRouterClient.TokenAmount[](0);
message = IRouterClient.Any2EVMMessage({
) internal pure returns (Client.Any2EVMMessage memory message) {
message = Client.Any2EVMMessage({
messageId: messageId,
sourceChainSelector: sourceChainSelector,
sender: abi.encode(sender),
data: data,
tokenAmounts: noTokens
destTokenAmounts: new Client.EVMTokenAmount[](0)
});
}
}

View File

@@ -0,0 +1,28 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "forge-std/Test.sol";
import "../../contracts/compliance/libraries/MonetaryFormulas.sol";
contract MonetaryFormulasTest is Test {
function test_MoneySupplyCD() public pure {
assertEq(MonetaryFormulas.moneySupplyCD(100, 50), 150);
}
function test_SimpleMultiplier() public pure {
assertEq(MonetaryFormulas.simpleMultiplier(1000), 10e18);
}
function test_CoverageRatioBps() public pure {
assertEq(MonetaryFormulas.coverageRatioBps(120, 100), 12000);
}
function test_GruFanout() public pure {
assertEq(MonetaryFormulas.gruM00ToM1Fanout(), 25);
}
function test_CoverageWeightedVelocity() public pure {
uint256 v = MonetaryFormulas.coverageWeightedVelocity(2e18, 12000);
assertEq(v, 2e18);
}
}

View File

@@ -0,0 +1,140 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import {Test} from "forge-std/Test.sol";
import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import {CWNavOracle} from "../../contracts/cw-settlement/CWNavOracle.sol";
import {CWRedemptionQueue} from "../../contracts/cw-settlement/CWRedemptionQueue.sol";
import {CWStabilityFund} from "../../contracts/cw-settlement/CWStabilityFund.sol";
contract MockNavBridge {
mapping(address => uint256) internal _locked;
mapping(address => uint256) internal _outstanding;
function setLocked(address token, uint256 amount) external {
_locked[token] = amount;
}
function lockedBalance(address token) external view returns (uint256) {
return _locked[token];
}
function totalOutstanding(address token) external view returns (uint256) {
return _outstanding[token];
}
}
contract MockReserveSystem {
mapping(address => uint256) internal _balances;
function setBalance(address asset, uint256 amount) external {
_balances[asset] = amount;
}
function getReserveBalance(address asset) external view returns (uint256) {
return _balances[asset];
}
}
contract MockERC20Six is ERC20 {
constructor() ERC20("Mock", "MOCK") {}
function mint(address to, uint256 amount) external {
_mint(to, amount);
}
function decimals() public pure override returns (uint8) {
return 6;
}
}
contract CWNavOracleTest is Test {
MockNavBridge internal bridge;
MockReserveSystem internal reserveSystem;
CWNavOracle internal navOracle;
MockERC20Six internal canonical;
function setUp() public {
bridge = new MockNavBridge();
reserveSystem = new MockReserveSystem();
navOracle = new CWNavOracle(address(this), address(bridge), address(reserveSystem));
canonical = new MockERC20Six();
navOracle.configureToken(address(canonical), address(canonical));
}
function test_publishNav_computesBackingAndNavPerShare() public {
bridge.setLocked(address(canonical), 120 * 1e6);
reserveSystem.setBalance(address(canonical), 30 * 1e6);
navOracle.publishNav(
address(canonical),
100 * 1e6,
8200,
1500,
300,
keccak256("attestation-v1")
);
CWNavOracle.NavSnapshot memory snap = navOracle.getNavSnapshot(address(canonical));
assertEq(snap.totalLockedAssets, 120 * 1e6);
assertEq(snap.totalReserveAssets, 30 * 1e6);
assertEq(snap.totalCwSupply, 100 * 1e6);
assertEq(snap.backingRatioBps, 15000);
assertEq(snap.navPerShare, 15e17);
}
}
contract CWRedemptionQueueTest is Test {
CWRedemptionQueue internal queue;
MockERC20Six internal token;
function setUp() public {
queue = new CWRedemptionQueue(address(this));
token = new MockERC20Six();
queue.grantRole(queue.BRIDGE_ROLE(), address(this));
}
function test_instantTier_claimableImmediately() public {
token.mint(address(this), 1_000 * 1e6);
token.approve(address(queue), 1_000 * 1e6);
uint256 requestId = queue.fundAndEnqueue(address(this), address(token), 1_000 * 1e6);
queue.claim(requestId);
assertEq(token.balanceOf(address(this)), 1_000 * 1e6);
}
function test_standardTier_requiresDelay() public {
uint256 requestId = queue.enqueueRedemption(address(this), address(token), 10_000 * 1e6);
vm.expectRevert();
queue.claim(requestId);
vm.warp(block.timestamp + 24 hours);
queue.processDueRequests(_arr(requestId));
vm.expectRevert();
queue.claim(requestId);
}
function _arr(uint256 id) internal pure returns (uint256[] memory ids) {
ids = new uint256[](1);
ids[0] = id;
}
}
contract CWStabilityFundTest is Test {
CWStabilityFund internal fund;
MockERC20Six internal token;
function setUp() public {
fund = new CWStabilityFund(address(this));
token = new MockERC20Six();
fund.setSupportedAsset(address(token), true);
}
function test_depositAndEmergencyWithdraw() public {
token.mint(address(this), 100e6);
token.approve(address(fund), 100e6);
fund.deposit(address(token), 100e6);
fund.emergencyWithdraw(address(token), address(this), 50e6, keccak256("depeg"));
assertEq(token.balanceOf(address(this)), 50e6);
}
}

View File

@@ -0,0 +1,122 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import {Test} from "forge-std/Test.sol";
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import {
AaveUniV2CwStableRebalanceFlashReceiver
} from "../../contracts/flash/AaveUniV2CwStableRebalanceFlashReceiver.sol";
contract AaveUniV2CwStableRebalanceFlashReceiverMainnetForkTest is Test {
address constant AAVE_POOL = 0x87870Bca3F3fD6335C3F4ce8392D69350B4fA4E2;
address constant PAIR = 0xC28706F899266b36BC43cc072b3a921BDf2C48D9;
address constant ROUTER = 0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D;
address constant CWUSDC = 0x2de5F116bFcE3d0f922d9C8351e0c5Fc24b9284a;
address constant USDC = 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48;
address constant TOKEN_JAR = 0xf38521f130fcCF29dB1961597bc5d2B60F995f85;
bool internal forkAvailable;
AaveUniV2CwStableRebalanceFlashReceiver internal receiver;
modifier skipIfNoFork() {
if (!forkAvailable) return;
_;
}
function setUp() public {
string memory rpcUrl = vm.envOr("ETHEREUM_MAINNET_RPC", string(""));
if (bytes(rpcUrl).length == 0) {
forkAvailable = false;
return;
}
try vm.createSelectFork(rpcUrl) {
forkAvailable = true;
} catch {
forkAvailable = false;
return;
}
receiver = new AaveUniV2CwStableRebalanceFlashReceiver(AAVE_POOL, address(this));
}
function testFork_flashRebalanceRemove_withTokenJarLp() public skipIfNoFork {
uint256 lpBal = IERC20(PAIR).balanceOf(TOKEN_JAR);
if (lpBal == 0) {
return;
}
(uint112 r0, uint112 r1,) = _reserves();
uint256 baseRaw = address(IERC20(CWUSDC)) < address(IERC20(USDC)) ? r0 : r1;
uint256 quoteRaw = address(IERC20(CWUSDC)) < address(IERC20(USDC)) ? r1 : r0;
if (quoteRaw == 0 || baseRaw <= quoteRaw) {
return;
}
uint256 flashIn = _quoteInToEqualize(baseRaw, quoteRaw);
uint256 premium = (flashIn * 5 + 9999) / 10000;
uint256 lpUse = lpBal;
vm.prank(TOKEN_JAR);
IERC20(PAIR).transfer(address(receiver), lpUse);
uint256 minCwRebalance = 1;
uint256 minCwRemove = 1;
uint256 minStableRemove = 1;
uint256 cwToSell = flashIn + premium + 50_000;
uint256 minStableRepay = flashIn + premium;
address recipient = address(0xBEEF);
receiver.runRebalanceRemove(
USDC,
flashIn,
AaveUniV2CwStableRebalanceFlashReceiver.RebalanceRemoveParams({
router: ROUTER,
pair: PAIR,
cwToken: CWUSDC,
stableToken: USDC,
lpAmount: lpUse,
rebalanceStableIn: flashIn,
minCwFromRebalance: minCwRebalance,
minStableFromRemove: minStableRemove,
minCwFromRemove: minCwRemove,
cwToSellForRepay: cwToSell,
minStableFromRepaySwap: minStableRepay,
recipient: recipient
})
);
assertEq(IERC20(PAIR).balanceOf(address(receiver)), 0, "LP consumed");
assertGt(IERC20(USDC).balanceOf(recipient) + IERC20(CWUSDC).balanceOf(recipient), 0, "recipient funded");
}
function _reserves() internal view returns (uint112 r0, uint112 r1, uint32 ts) {
(r0, r1, ts) = _getReserves(PAIR);
}
function _getReserves(address pair) internal view returns (uint112, uint112, uint32) {
(bool ok, bytes memory data) = pair.staticcall(abi.encodeWithSignature("getReserves()"));
require(ok, "getReserves failed");
return abi.decode(data, (uint112, uint112, uint32));
}
function _quoteInToEqualize(uint256 baseRaw, uint256 quoteRaw) internal pure returns (uint256) {
uint256 y = quoteRaw;
uint256 x = baseRaw;
// Integer approximation of closed-form quote-in (matches planner within rounding).
uint256 xy = x * y;
uint256 target = _isqrt(xy);
if (target <= y) return y + 1;
uint256 need = target - y;
return (need * 1005) / 997 + 1;
}
function _isqrt(uint256 n) internal pure returns (uint256) {
if (n == 0) return 0;
uint256 x = n;
uint256 z = (x + 1) / 2;
while (z < x) {
x = z;
z = (x + n / x) / 2;
}
return x;
}
}

View File

@@ -0,0 +1,72 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "forge-std/Test.sol";
import "../../contracts/rwa/RWAToken.sol";
import "../../contracts/rwa/GruMonetaryPolicyGate.sol";
import "../../contracts/rwa/IGruMonetaryPolicyGate.sol";
contract GruMonetaryPolicyGateTest is Test {
GruMonetaryPolicyGate gate;
RWAToken liToken;
address admin = address(0xA11CE);
function setUp() public {
vm.startPrank(admin);
gate = new GruMonetaryPolicyGate(admin, 12000, 11800, 25);
liToken = new RWAToken(
"LiXAU Test",
"LiXAU",
6,
"LiXAU",
"Commodity",
"Gold",
"Index",
"XAU",
"M00",
admin,
admin,
admin,
1_000_000,
0,
keccak256("methodology-test")
);
gate.setTokenGate(address(liToken), true);
liToken.setPolicyGate(address(gate));
vm.stopPrank();
}
function test_AllowsMintWhenGreen() public {
vm.prank(admin);
gate.updateMetrics(15000, 10, IGruMonetaryPolicyGate.VelocityZone.Green, false);
vm.prank(admin);
liToken.mint(admin, 1000e6);
assertEq(liToken.totalSupply(), 1000e6);
}
function test_BlocksMintOnCoverageAlert() public {
vm.prank(admin);
gate.updateMetrics(11700, 10, IGruMonetaryPolicyGate.VelocityZone.Green, false);
vm.prank(admin);
vm.expectRevert("RWAToken: policy gate");
liToken.mint(admin, 1000e6);
}
function test_BlocksMintOnUtilization() public {
vm.prank(admin);
gate.updateMetrics(15000, 30, IGruMonetaryPolicyGate.VelocityZone.Green, false);
vm.prank(admin);
vm.expectRevert("RWAToken: policy gate");
liToken.mint(admin, 1000e6);
}
function test_SkipGateWhenTokenNotEnabled() public {
vm.prank(admin);
gate.setTokenGate(address(liToken), false);
vm.prank(admin);
gate.updateMetrics(10000, 99, IGruMonetaryPolicyGate.VelocityZone.Red, true);
vm.prank(admin);
liToken.mint(admin, 1000e6);
assertEq(liToken.totalSupply(), 1000e6);
}
}

View File

@@ -0,0 +1,84 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "forge-std/Test.sol";
import "../../contracts/rwa/RWAToken.sol";
import "../../contracts/rwa/LiIndexFlashVault.sol";
import {IERC3156FlashBorrower} from "@openzeppelin/contracts/interfaces/IERC3156FlashBorrower.sol";
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
contract MockFlashBorrower is IERC3156FlashBorrower {
bytes32 private constant _RETURN = keccak256("ERC3156FlashBorrower.onFlashLoan");
function onFlashLoan(
address,
address token,
uint256 amount,
uint256 fee,
bytes calldata
) external returns (bytes32) {
IERC20(token).approve(msg.sender, amount + fee);
return _RETURN;
}
}
contract LiIndexFlashVaultTest is Test {
LiIndexFlashVault vault;
RWAToken liToken;
MockFlashBorrower borrower;
address admin = address(0xA11CE);
function setUp() public {
vm.startPrank(admin);
liToken = new RWAToken(
"LiXAU Test",
"LiXAU",
6,
"LiXAU",
"Commodity",
"Gold",
"Index",
"XAU",
"M00",
admin,
admin,
admin,
1_000_000,
0,
keccak256("methodology-test")
);
vault = new LiIndexFlashVault(
admin,
8000,
5000,
5,
250,
86400,
2400_00000000
);
vault.setSupportedToken(address(liToken), true);
borrower = new MockFlashBorrower();
liToken.mint(admin, 1_000_000e6);
liToken.approve(address(vault), type(uint256).max);
vault.deposit(address(liToken), 500_000e6);
vm.stopPrank();
}
function test_MaxFlashLoanWithinLtv() public view {
uint256 max = vault.maxFlashLoan(address(liToken));
assertEq(max, 250_000e6);
}
function test_FlashLoanRepays() public {
vm.prank(admin);
vault.flashLoan(IERC3156FlashBorrower(address(borrower)), address(liToken), 100_000e6, "");
assertEq(vault.outstandingFlash(address(liToken)), 0);
}
function test_BlocksWhenUtilizationExceeded() public {
vm.startPrank(admin);
vault.flashLoan(IERC3156FlashBorrower(address(borrower)), address(liToken), 400_000e6, "");
vm.stopPrank();
assertEq(vault.maxFlashLoan(address(liToken)), 0);
}
}