Copy // SPDX-License-Identifier: MIT
pragma solidity ^0.8.21;
import "forge-std/Test.sol";
import "forge-std/console.sol";
import {MoonwellWETHStrategy} from "../src/strategies/optimism/MoonwellWETHStrategy.sol";
import {MYTStrategy} from "../src/MYTStrategy.sol";
import {IERC20} from "../lib/openzeppelin-contracts/contracts/token/ERC20/IERC20.sol";
/**
* @title Finding39_EndToEnd_POC
* @notice Comprehensive end-to-end POC for Finding 39: ETH wrap condition bug in MoonwellWETHStrategy
*
* ## 🧪 Test Objective
*
* To verify if the conditional logic bug in MoonwellWETHStrategy._deallocate() causes transaction failures in a realistic scenario.
*
* ## 🔬 Problem Description
*
* Line 64-66 of MoonwellWETHStrategy.sol:
* ```solidity
* if (ethRedeemed + ethBalanceBefore >= amount) {
* weth.deposit{value: ethRedeemed}();
* }
* ```
*
* This condition is equivalent to: `if (ethBalanceAfter >= amount)`
*
* **Flaw**: The wrap only occurs if the current ETH balance >= requested amount, but it should unconditionally wrap all redeemed ETH.
*
* ## 🎯 Test Scenarios
*
* ### Scenario 1: No ETH Residue + Redemption Loss
* - Strategy has 0 ETH residue
* - Moonwell redemption returns less ETH than requested (due to exchange rate or rounding)
* - Condition: `ethRedeemed + 0 < amount` → false
* - Result: ETH is not wrapped → require fails → REVERT
*
* ### Scenario 2: WETH Residue + Redemption Loss (Critical Scenario)
* - Strategy has some WETH residue (e.g., 10 WETH)
* - Request to deallocate 100 WETH
* - Moonwell only returns 95 ETH (5 ETH loss)
* - Condition: `95 + 0 < 100` → false
* - ETH is not wrapped
* - Only 10 WETH residue is available
* - require(10 >= 100) → REVERT ❌
*
* **But if wrapped correctly**:
* - wrap 95 ETH → 95 WETH
* - Total WETH = 10 + 95 = 105 WETH
* - require(105 >= 100) → PASS ✅
*
* ### Scenario 3: Fixed Version Comparison
* - Using the fixed version with unconditional wrap
* - Should succeed under the same conditions
*
* ## ⚙️ Environment Information
*
* - Network: Optimism Mainnet Fork
* - Moonwell WETH Market: 0xb4104C02BBf4E9be85AAa41a62974E4e28D59A33
* - WETH: 0x4200000000000000000000000000000000000006
* - Compiler: solc 0.8.21
*/
contract Finding39_EndToEnd_POC is Test {
// Optimism mainnet addresses
address constant WETH_OPTIMISM = 0x4200000000000000000000000000000000000006;
address constant MOONWELL_WETH = 0xb4104C02BBf4E9be85AAa41a62974E4e28D59A33;
address constant PERMIT2 = 0x000000000022D473030F116dDEE9F6B43aC78BA3;
// Interfaces
IERC20 weth;
IMToken mWETH;
// Mock contracts
MockMoonwellToken mockMWETH;
// Test contracts
MoonwellWETHStrategyVulnerable strategyVulnerable;
MoonwellWETHStrategyFixed strategyFixed;
// Test actors
address vault = makeAddr("vault");
address admin = makeAddr("admin");
address user = makeAddr("user");
// Constants
uint256 constant INITIAL_WETH = 1000 ether;
function setUp() public {
// Deploy mock contracts instead of forking
MockWETH mockWeth = new MockWETH();
mockMWETH = new MockMoonwellToken(address(mockWeth));
weth = IERC20(address(mockWeth));
mWETH = IMToken(address(mockMWETH));
// Label addresses for better traces
vm.label(address(weth), "WETH");
vm.label(address(mWETH), "mWETH");
vm.label(vault, "Vault");
vm.label(admin, "Admin");
// Deploy vulnerable strategy (current implementation)
strategyVulnerable = new MoonwellWETHStrategyVulnerable(
address(weth),
address(mWETH)
);
vm.label(address(strategyVulnerable), "StrategyVulnerable");
// Deploy fixed strategy
strategyFixed = new MoonwellWETHStrategyFixed(
address(weth),
address(mWETH)
);
vm.label(address(strategyFixed), "StrategyFixed");
// Give test contract some ETH and WETH
vm.deal(address(this), 1000 ether);
vm.deal(user, 100 ether);
}
/// @notice Helper: Convert ETH to WETH
function _getWETH(address recipient, uint256 amount) internal {
vm.deal(recipient, amount);
vm.prank(recipient);
IWETH(address(weth)).deposit{value: amount}();
}
/// @notice Helper: Set redemption loss in mock Moonwell
function _setRedemptionLoss(uint256 lossBPS) internal {
mockMWETH.setRedeemLoss(lossBPS);
}
/// @notice Helper: Calculate expected ETH from mWETH redemption with loss
function _calculateExpectedRedemption(uint256 amount, uint256 lossBPS) internal pure returns (uint256) {
// Apply loss percentage
return amount - (amount * lossBPS / 10000);
}
/**
* ═══════════════════════════════════════════════════════════════════
* SCENARIO 1: No Residue + Redemption Loss = REVERT
* ═══════════════════════════════════════════════════════════════════
*/
function testScenario1_NoResidue_WithLoss_Reverts() public {
console.log("\n=== SCENARIO 1: No Residue + Loss = REVERT ===\n");
uint256 depositAmount = 100 ether;
uint256 lossBPS = 500; // 5% loss
// 1. Give strategy some WETH
_getWETH(address(strategyVulnerable), depositAmount);
console.log("Step 1: Strategy WETH balance:", weth.balanceOf(address(strategyVulnerable)) / 1e18, "WETH");
// 2. Allocate to Moonwell
vm.prank(address(strategyVulnerable));
weth.approve(address(mWETH), depositAmount);
vm.prank(address(strategyVulnerable));
uint256 mintResult = mWETH.mint(depositAmount);
require(mintResult == 0, "Moonwell mint failed");
console.log("Step 2: Allocated to Moonwell");
console.log(" Strategy WETH balance:", weth.balanceOf(address(strategyVulnerable)) / 1e18, "WETH");
console.log(" Strategy mWETH balance:", mWETH.balanceOf(address(strategyVulnerable)) / 1e18, "mWETH");
// 3. Verify no ETH or WETH residue
assertEq(weth.balanceOf(address(strategyVulnerable)), 0, "Should have no WETH residue");
assertEq(address(strategyVulnerable).balance, 0, "Should have no ETH residue");
// 4. Set redemption loss
_setRedemptionLoss(lossBPS);
uint256 expectedEth = _calculateExpectedRedemption(depositAmount, lossBPS);
console.log("\nStep 3: Set 5% redemption loss");
console.log(" Expected ETH from redemption:", expectedEth / 1e18, "ETH");
console.log(" Requested amount:", depositAmount / 1e18, "ETH");
console.log(" Loss:", (depositAmount - expectedEth) / 1e18, "ETH");
// 5. Attempt to deallocate
console.log("\nStep 4: Attempting to deallocate", depositAmount / 1e18, "WETH...");
console.log("\n[VULNERABILITY TRIGGER]");
console.log(" ethBalanceBefore = 0");
console.log(" ethRedeemed (ETH):", expectedEth / 1e18);
console.log(" amount requested (ETH):", depositAmount / 1e18);
console.log(" Condition result: FALSE - will NOT wrap ETH");
console.log(" -> ETH will NOT be wrapped");
console.log(" -> require(weth >= amount) will FAIL");
console.log(" -> Transaction REVERTS");
// Should revert due to insufficient WETH
vm.expectRevert("Strategy balance is less than the amount needed");
vm.prank(vault);
strategyVulnerable.deallocate(depositAmount);
console.log("\nCONFIRMED: Transaction reverted as expected");
}
/**
* ═══════════════════════════════════════════════════════════════════
* SCENARIO 2: WETH Residue + Redemption Loss (Critical Scenario)
* ═══════════════════════════════════════════════════════════════════
*/
function testScenario2_WETHResidue_WithLoss_FailsButShouldSucceed() public {
console.log("\n=== SCENARIO 2: WETH Residue + Loss = Should Succeed But Fails ===\n");
uint256 depositAmount = 100 ether;
uint256 wethResidue = 10 ether;
uint256 totalWeth = depositAmount + wethResidue;
// 1. Give strategy WETH (including residue)
_getWETH(address(strategyVulnerable), totalWeth);
console.log("Step 1: Initial state");
console.log(" Total WETH given:", totalWeth / 1e18, "WETH");
console.log(" Will deposit:", depositAmount / 1e18, "WETH");
console.log(" Will keep as residue:", wethResidue / 1e18, "WETH");
// 2. Allocate only depositAmount to Moonwell, keep wethResidue
vm.prank(address(strategyVulnerable));
weth.approve(address(mWETH), depositAmount);
vm.prank(address(strategyVulnerable));
uint256 mintResult = mWETH.mint(depositAmount);
require(mintResult == 0, "Moonwell mint failed");
console.log("\nStep 2: After allocation");
console.log(" Strategy WETH residue:", weth.balanceOf(address(strategyVulnerable)) / 1e18, "WETH");
console.log(" Strategy mWETH balance:", mWETH.balanceOf(address(strategyVulnerable)) / 1e18, "mWETH");
assertEq(weth.balanceOf(address(strategyVulnerable)), wethResidue, "Should have WETH residue");
assertEq(address(strategyVulnerable).balance, 0, "Should have no ETH residue");
// 3. Set redemption loss and calculate expected redemption
uint256 lossBPS = 500; // 5% loss
_setRedemptionLoss(lossBPS);
uint256 expectedEth = _calculateExpectedRedemption(depositAmount, lossBPS);
console.log("\nStep 3: Set 5% redemption loss");
console.log(" Expected ETH from redemption:", expectedEth / 1e18, "ETH");
console.log(" Requested amount:", depositAmount / 1e18, "ETH");
console.log(" Loss:", (depositAmount - expectedEth) / 1e18, "ETH");
// 4. Attempt to deallocate
console.log("\nStep 4: Attempting to deallocate", depositAmount / 1e18, "WETH...");
if (expectedEth < depositAmount) {
uint256 potentialTotal = wethResidue + expectedEth;
console.log("\n[CRITICAL VULNERABILITY CASE]");
console.log(" WETH residue (WETH):", wethResidue / 1e18);
console.log(" ETH to be redeemed (ETH):", expectedEth / 1e18);
console.log(" ethBalanceBefore = 0 ETH");
console.log(" Condition: FALSE - will NOT wrap");
console.log(" -> ETH will NOT be wrapped");
console.log(" -> Only WETH available:", wethResidue / 1e18);
console.log(" -> Amount required:", depositAmount / 1e18);
console.log(" -> Transaction REVERTS");
console.log("\n[IF WRAPPED CORRECTLY]");
console.log(" -> Would wrap ETH:", expectedEth / 1e18);
console.log(" -> Total WETH would be:", potentialTotal / 1e18);
console.log(" -> Would pass require:", potentialTotal >= depositAmount);
if (potentialTotal >= depositAmount) {
console.log("\nBUG CONFIRMED: Transaction should succeed but will fail!");
// Should revert with current buggy implementation
vm.expectRevert("Strategy balance is less than the amount needed");
vm.prank(vault);
strategyVulnerable.deallocate(depositAmount);
console.log("Transaction reverted as predicted by vulnerability analysis");
}
}
}
/**
* ═══════════════════════════════════════════════════════════════════
* SCENARIO 3: Fixed Version - Same Conditions = SUCCESS
* ═══════════════════════════════════════════════════════════════════
*/
function testScenario3_FixedVersion_WETHResidue_WithLoss_Succeeds() public {
console.log("\n=== SCENARIO 3: Fixed Version - WETH Residue + Loss = SUCCESS ===\n");
uint256 depositAmount = 100 ether;
uint256 wethResidue = 10 ether;
uint256 totalWeth = depositAmount + wethResidue;
// 1. Give strategy WETH (including residue)
_getWETH(address(strategyFixed), totalWeth);
console.log("Step 1: Initial state");
console.log(" Total WETH given:", totalWeth / 1e18, "WETH");
console.log(" Will deposit:", depositAmount / 1e18, "WETH");
console.log(" Will keep as residue:", wethResidue / 1e18, "WETH");
// 2. Allocate only depositAmount to Moonwell, keep wethResidue
vm.prank(address(strategyFixed));
weth.approve(address(mWETH), depositAmount);
vm.prank(address(strategyFixed));
uint256 mintResult = mWETH.mint(depositAmount);
require(mintResult == 0, "Moonwell mint failed");
console.log("\nStep 2: After allocation");
console.log(" Strategy WETH residue:", weth.balanceOf(address(strategyFixed)) / 1e18, "WETH");
console.log(" Strategy ETH residue:", address(strategyFixed).balance / 1e18, "ETH");
assertEq(weth.balanceOf(address(strategyFixed)), wethResidue, "Should have WETH residue");
// 3. Set redemption loss and calculate expected redemption
uint256 lossBPS = 500; // 5% loss
_setRedemptionLoss(lossBPS);
uint256 expectedEth = _calculateExpectedRedemption(depositAmount, lossBPS);
console.log("\nStep 3: Set 5% redemption loss");
console.log(" Expected ETH from redemption:", expectedEth / 1e18, "ETH");
// 4. Deallocate with fixed version
console.log("\nStep 4: Deallocating with FIXED version...");
uint256 wethBefore = weth.balanceOf(address(strategyFixed));
console.log(" WETH before deallocate:", wethBefore / 1e18, "WETH");
vm.prank(vault);
uint256 returned = strategyFixed.deallocate(depositAmount);
uint256 wethAfter = weth.balanceOf(address(strategyFixed));
console.log(" WETH after deallocate:", wethAfter / 1e18, "WETH");
console.log(" Returned amount:", returned / 1e18, "WETH");
console.log("\n[FIXED VERSION LOGIC]");
console.log(" -> Unconditionally wrapped (ETH):", expectedEth / 1e18);
console.log(" -> Total WETH:", (wethResidue + expectedEth) / 1e18);
console.log(" -> require passed");
console.log(" -> Approved to vault (WETH):", depositAmount / 1e18);
assertEq(returned, depositAmount, "Should return requested amount");
console.log("\nSUCCESS: Fixed version works correctly!");
}
/**
* ═══════════════════════════════════════════════════════════════════
* SCENARIO 4: Direct Comparison - Vulnerable vs Fixed
* ═══════════════════════════════════════════════════════════════════
*/
function testScenario4_DirectComparison_VulnerableVsFixed() public {
console.log("\n=== SCENARIO 4: Direct Comparison - Vulnerable vs Fixed ===\n");
uint256 depositAmount = 100 ether;
uint256 wethResidue = 10 ether;
// Setup vulnerable strategy
_getWETH(address(strategyVulnerable), depositAmount + wethResidue);
vm.prank(address(strategyVulnerable));
weth.approve(address(mWETH), depositAmount);
vm.prank(address(strategyVulnerable));
mWETH.mint(depositAmount);
// Setup fixed strategy
_getWETH(address(strategyFixed), depositAmount + wethResidue);
vm.prank(address(strategyFixed));
weth.approve(address(mWETH), depositAmount);
vm.prank(address(strategyFixed));
mWETH.mint(depositAmount);
// Set redemption loss
uint256 lossBPS = 500; // 5% loss
_setRedemptionLoss(lossBPS);
uint256 expectedEth = _calculateExpectedRedemption(depositAmount, lossBPS);
console.log("Initial Setup (Both Strategies):");
console.log(" Deposited:", depositAmount / 1e18, "WETH");
console.log(" Residue:", wethResidue / 1e18, "WETH");
console.log(" Expected redemption:", expectedEth / 1e18, "ETH");
// Test vulnerable version
console.log("\n[Testing VULNERABLE version]");
bool vulnerableFailed = false;
try strategyVulnerable.deallocate(depositAmount) {
console.log(" Result: SUCCESS (unexpected)");
} catch {
console.log(" Result: FAILED (as expected due to bug)");
vulnerableFailed = true;
}
// Test fixed version
console.log("\n[Testing FIXED version]");
bool fixedSucceeded = false;
try strategyFixed.deallocate(depositAmount) returns (uint256 returned) {
console.log(" Result: SUCCESS");
console.log(" Returned:", returned / 1e18, "WETH");
fixedSucceeded = true;
} catch {
console.log(" Result: FAILED (unexpected)");
}
console.log("\n[COMPARISON]");
console.log(" Vulnerable version failed:", vulnerableFailed ? "YES" : "NO");
console.log(" Fixed version succeeded:", fixedSucceeded ? "YES" : "NO");
if (vulnerableFailed && fixedSucceeded) {
console.log("\nVULNERABILITY CONFIRMED:");
console.log(" The buggy condition causes unnecessary failures");
console.log(" The fixed version (unconditional wrap) works correctly");
}
}
/**
* ═══════════════════════════════════════════════════════════════════
* SCENARIO 5: Simulate Realistic Loss Scenario
* ═══════════════════════════════════════════════════════════════════
*/
function testScenario5_RealisticLossScenario() public {
console.log("\n=== SCENARIO 5: Realistic Loss Scenario ===\n");
console.log("Simulating realistic Moonwell exchange rate fluctuation");
uint256 depositAmount = 50 ether; // Smaller amount for realistic test
uint256 wethResidue = 5 ether;
// Give vulnerable strategy WETH
_getWETH(address(strategyVulnerable), depositAmount + wethResidue);
console.log("Step 1: Deposit to Moonwell");
console.log(" Amount:", depositAmount / 1e18, "WETH");
console.log(" Residue kept:", wethResidue / 1e18, "WETH");
vm.prank(address(strategyVulnerable));
weth.approve(address(mWETH), depositAmount);
vm.prank(address(strategyVulnerable));
mWETH.mint(depositAmount);
// Get exchange rate info
uint256 exchangeRateBefore = mWETH.exchangeRateStored();
console.log("\nStep 2: Check exchange rate");
console.log(" Exchange rate:", exchangeRateBefore / 1e14, "/ 1e18");
// Simulate time passing and some borrowing activity (which changes exchange rate)
vm.warp(block.timestamp + 7 days);
vm.roll(block.number + 50000);
uint256 exchangeRateAfter = mWETH.exchangeRateCurrent();
console.log(" Exchange rate after 7 days:", exchangeRateAfter / 1e14, "/ 1e18");
// Calculate expected redemption
uint256 mTokenBalance = mWETH.balanceOf(address(strategyVulnerable));
uint256 expectedEth = (mTokenBalance * exchangeRateAfter) / 1e18;
console.log("\nStep 3: Prepare to redeem");
console.log(" mToken balance:", mTokenBalance / 1e18, "mWETH");
console.log(" Expected ETH:", expectedEth / 1e18, "ETH");
console.log(" Requested amount:", depositAmount / 1e18, "WETH");
if (expectedEth < depositAmount) {
console.log(" -> Loss detected:", (depositAmount - expectedEth) / 1e18, "ETH");
}
// Try to deallocate
console.log("\nStep 4: Attempt deallocate with vulnerable version");
uint256 potentialTotal = wethResidue + expectedEth;
console.log(" If wrapped correctly, would have:", potentialTotal / 1e18, "WETH");
console.log(" Required:", depositAmount / 1e18, "WETH");
console.log(" Would pass?", potentialTotal >= depositAmount ? "YES" : "NO");
if (expectedEth < depositAmount && potentialTotal >= depositAmount) {
console.log("\nCRITICAL BUG CASE:");
console.log(" Should succeed but will fail due to wrap condition bug");
vm.expectRevert();
vm.prank(vault);
strategyVulnerable.deallocate(depositAmount);
console.log(" Confirmed: Failed as predicted");
}
}
}
/**
* ═══════════════════════════════════════════════════════════════════
* MOCK CONTRACTS FOR TESTING
* ═══════════════════════════════════════════════════════════════════
*/
/// @notice Moonwell mToken interface
interface IMToken {
function mint(uint256 mintAmount) external returns (uint256);
function redeemUnderlying(uint256 redeemAmount) external returns (uint256);
function balanceOf(address owner) external view returns (uint256);
function exchangeRateStored() external view returns (uint256);
function exchangeRateCurrent() external returns (uint256);
}
/// @notice WETH interface
interface IWETH {
function deposit() external payable;
function withdraw(uint256) external;
function balanceOf(address) external view returns (uint256);
function approve(address, uint256) external returns (bool);
}
/**
* @notice Vulnerable version of MoonwellWETHStrategy (current implementation)
* Contains the buggy condition at line 64-66
*/
contract MoonwellWETHStrategyVulnerable {
IWETH public immutable weth;
IMToken public immutable mWETH;
event StrategyDeallocationLoss(string message, uint256 requested, uint256 actual);
constructor(address _weth, address _mWETH) {
weth = IWETH(_weth);
mWETH = IMToken(_mWETH);
}
function deallocate(uint256 amount) external returns (uint256) {
uint256 ethBalanceBefore = address(this).balance;
// Pull exact amount of underlying WETH out
mWETH.redeemUnderlying(amount);
// wrap any ETH received (Moonwell redeems to ETH for WETH markets)
uint256 ethBalanceAfter = address(this).balance;
uint256 ethRedeemed = ethBalanceAfter - ethBalanceBefore;
if (ethRedeemed < amount) {
emit StrategyDeallocationLoss("Strategy deallocation loss.", amount, ethRedeemed);
}
// 🚨 BUG: Wrong condition!
// This means: only wrap if total ETH >= amount
// Equivalent to: if (ethBalanceAfter >= amount)
if (ethRedeemed + ethBalanceBefore >= amount) {
weth.deposit{value: ethRedeemed}();
}
require(weth.balanceOf(address(this)) >= amount, "Strategy balance is less than the amount needed");
weth.approve(msg.sender, amount);
return amount;
}
receive() external payable {}
}
/**
* @notice Fixed version of MoonwellWETHStrategy
* Unconditionally wraps all redeemed ETH
*/
contract MoonwellWETHStrategyFixed {
IWETH public immutable weth;
IMToken public immutable mWETH;
event StrategyDeallocationLoss(string message, uint256 requested, uint256 actual);
constructor(address _weth, address _mWETH) {
weth = IWETH(_weth);
mWETH = IMToken(_mWETH);
}
function deallocate(uint256 amount) external returns (uint256) {
uint256 ethBalanceBefore = address(this).balance;
// Pull exact amount of underlying WETH out
mWETH.redeemUnderlying(amount);
// wrap any ETH received (Moonwell redeems to ETH for WETH markets)
uint256 ethBalanceAfter = address(this).balance;
uint256 ethRedeemed = ethBalanceAfter - ethBalanceBefore;
if (ethRedeemed < amount) {
emit StrategyDeallocationLoss("Strategy deallocation loss.", amount, ethRedeemed);
}
// ✅ FIX: Unconditionally wrap all redeemed ETH
if (ethRedeemed > 0) {
weth.deposit{value: ethRedeemed}();
}
require(weth.balanceOf(address(this)) >= amount, "Strategy balance is less than the amount needed");
weth.approve(msg.sender, amount);
return amount;
}
receive() external payable {}
}
/// @notice Mock WETH contract
contract MockWETH {
mapping(address => uint256) public balanceOf;
mapping(address => mapping(address => uint256)) public allowance;
function deposit() external payable {
balanceOf[msg.sender] += msg.value;
}
function withdraw(uint256 amount) external {
require(balanceOf[msg.sender] >= amount);
balanceOf[msg.sender] -= amount;
payable(msg.sender).transfer(amount);
}
function approve(address spender, uint256 amount) external returns (bool) {
allowance[msg.sender][spender] = amount;
return true;
}
function transfer(address to, uint256 amount) external returns (bool) {
require(balanceOf[msg.sender] >= amount);
balanceOf[msg.sender] -= amount;
balanceOf[to] += amount;
return true;
}
function transferFrom(address from, address to, uint256 amount) external returns (bool) {
require(balanceOf[from] >= amount);
require(allowance[from][msg.sender] >= amount);
balanceOf[from] -= amount;
balanceOf[to] += amount;
allowance[from][msg.sender] -= amount;
return true;
}
receive() external payable {
balanceOf[msg.sender] += msg.value;
}
}
/// @notice Mock Moonwell mToken
contract MockMoonwellToken {
MockWETH public weth;
mapping(address => uint256) public balanceOf;
uint256 public redeemLossBPS; // Loss in basis points (100 = 1%)
constructor(address _weth) {
weth = MockWETH(payable(_weth));
}
function setRedeemLoss(uint256 lossBPS) external {
redeemLossBPS = lossBPS;
}
function mint(uint256 amount) external returns (uint256) {
weth.transferFrom(msg.sender, address(this), amount);
weth.withdraw(amount); // Convert to ETH
balanceOf[msg.sender] += amount;
return 0;
}
function redeemUnderlying(uint256 amount) external returns (uint256) {
require(balanceOf[msg.sender] >= amount);
balanceOf[msg.sender] -= amount;
// Apply loss
uint256 actualAmount = amount - (amount * redeemLossBPS / 10000);
// Send ETH to caller
payable(msg.sender).transfer(actualAmount);
return 0;
}
function exchangeRateStored() external pure returns (uint256) {
return 1e18; // 1:1 rate
}
function exchangeRateCurrent() external pure returns (uint256) {
return 1e18; // 1:1 rate
}
receive() external payable {}
}