Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
2ee695e
feat: mtoken min balance contract
kostyamospan Jul 14, 2026
8b90792
chore: generator scripts min balance
kostyamospan Jul 14, 2026
4257ca4
fix: skills, refactor min balance contract
kostyamospan Jul 14, 2026
bbfe232
fix: migrated to roles based exempt list from mapping
kostyamospan Jul 15, 2026
2125ba8
refactor: removed flag from _validateMinBalance
kostyamospan Jul 15, 2026
a54a98f
fix: upgrade tests added
kostyamospan Jul 15, 2026
5bbf996
fix: removed extra gaps for upgrade safety
kostyamospan Jul 15, 2026
e1488bb
fix: typo
kostya-midas Jul 17, 2026
98bb0ce
fix: mwin metadata and contract
kostya-midas Jul 20, 2026
3af60a5
Merge remote-tracking branch 'origin/main' into feat/mtoken-min-balance
kostya-midas Jul 20, 2026
4b3c772
chore: new impl deployed
kostya-midas Jul 20, 2026
2a5fb21
chore: mwin aggregator upgrade propose
kostya-midas Jul 21, 2026
4e52c6a
fix: propose reinitializer values
kostya-midas Jul 22, 2026
198375c
fix: execute script
kostya-midas Jul 24, 2026
8bd6800
fix: collect redemption fees before burning mTokens
dmytroh-dev Jul 27, 2026
0452797
fix: reuse redemption helpers for min-balance flows
dmytroh-dev Jul 27, 2026
0fb1ecb
fix: cover min-balance redemptions across all vaults
dmytroh-dev Jul 28, 2026
8b18fc3
Merge pull request #248 from midas-apps/fix/redemption-fee-before-burn
kostya-midas Jul 29, 2026
d97b00a
chore: mwin mtoken rv upgrade
kostya-midas Jul 29, 2026
885bc08
feat: mGLOeuro on Ethereum
dmytroh-dev Jul 30, 2026
9ed3453
feat: mGLO on Optimism
dmytroh-dev Jul 30, 2026
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
2,954 changes: 2,884 additions & 70 deletions .openzeppelin/mainnet.json

Large diffs are not rendered by default.

1,552 changes: 1,552 additions & 0 deletions .openzeppelin/optimism.json

Large diffs are not rendered by default.

14 changes: 14 additions & 0 deletions ROLES.md
Original file line number Diff line number Diff line change
Expand Up @@ -933,6 +933,7 @@ All the roles for the Midas protocol smart contracts are listed below.
| **_depositVaultAdmin_** | `0x5e436b58093eec5c9bb0bf88a6e66765dc4e9b5fd9716a7da608c87d5f7b02bf` |
| **_redemptionVaultAdmin_** | `0xf1f308589835b1481efea09554e2f72656725481c02aa4aaadab93e7045e5a53` |
| **_greenlisted_** | `0xc7021654dd5cc2dc098b5f088ed0c6a046d28f5eaf2f8d0008903e2b8520acb2` |
| **_minBalanceExempt_** | `0x23e8bb4597bf064a5742e658e9f935a80bebc3543fc490fd89c0e3a5dcb527ab` |

### qHVNUSD Roles

Expand Down Expand Up @@ -981,3 +982,16 @@ All the roles for the Midas protocol smart contracts are listed below.
| **_depositVaultAdmin_** | `0x0f6a147de8cdfffe171daade1586996aefc68576e55cdec9b77b1e419cac1ce1` |
| **_redemptionVaultAdmin_** | `0x5f1f545d6af70010d52bd3c02c8e59828c88b7384e5388ce3b2902ad63da8a43` |
| **_greenlisted_** | `0xd4cee0f1caf45844ac124a181a38faea72039907def2059414beb261dc86eb67` |

### mGLOeuro Roles

| Role Name | Role |
| -------------------------- | -------------------------------------------------------------------- |
| **_minter_** | `0x29b52ad8b5aa9311f7fde983a723c52e504b21cb5c9bbd739a967c9e0cd715ee` |
| **_burner_** | `0x070ba01379eed310ddfca9075a00e040cc41c90d55c2a52e697ce7473e0b499f` |
| **_pauser_** | `0x96e4a6769d73f487be8632f4c7283e8a7bd60f67e46f1c472960bfe52eb1d698` |
| **_customFeedAdmin_** | `0x8cfa174b39696c5fd7407caa7ed3dd721ef06ed4f345ca9e3827d221f9bbf4da` |
| **_depositVaultAdmin_** | `0xbea31e5142f943ef913e5422e0e9a586e9d0303ccd63e0eead6b4189eb11cb27` |
| **_redemptionVaultAdmin_** | `0x0fada8a7f8ae9df6a95f6bc75af039633867da152deda5fb8ade34998ef95ae8` |
| **_greenlisted_** | `0x49a103d47daa98d445728ba0f2e848dccbbd73dea56729c961450eeb09890acc` |
| **_minBalanceExempt_** | `0x4c160e777147efaadeec64143e540a0cb03c77322c06777f48cab5184c7a2a03` |
39 changes: 39 additions & 0 deletions config/constants/addresses.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,25 @@ export const midasAddressesPerNetwork: ConfigPerNetwork<
token: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48',
dataFeed: '0x3aAc6fd73fA4e16Ec683BD4aaF5Ec89bb2C0EdC2',
},
eurc: {
token: '0x1aBaEA1f7C830bD89Acc67eC4af516284b1bC33c',
numerator: {
// Chainlink EURC/USD: USD per EURC, 8 decimals.
aggregator: '0x04F84020Fdf10d9ee64D1dcC2986EDF2F556DA11',
dataFeed: '0x2A2895f343CA5a5E3d81A53e4C959367EB339f11',
},
denominator: {
// Chainlink EUR/USD: USD per EUR, 8 decimals.
aggregator: '0xb49f677943BC038e9857d61E7d053CaA2C1734C1',
dataFeed: '0xFBD70c323238B5EDA94b6DEfFc5dB8c70F1f0e6D',
},
dataFeed: '0x04DbaB674457A696cd4884c2E7c8cc6cA3118513',
},
eurcv: {
token: '0x5F7827FDeb7c20b443265Fc2F40845B715385Ff2',
aggregator: '0xD9795cDFb09f1c599e16f6b150f642eC529b7aBB',
dataFeed: '0x4D88E4a32cf289ECfA6C9303B57aF0b6c132a733',
},
rlusd: {
aggregator: '0x26C46B7aD0012cA71F2298ada567dC9Af14E7f2A',
token: '0x8292Bb45bf1Ee4d140127049757C2E0fF06317eD',
Expand Down Expand Up @@ -646,6 +665,16 @@ export const midasAddressesPerNetwork: ConfigPerNetwork<
depositVault: '0xcEDCa505839c01Cc4FbE65496AA3Bb03B8ff98Ed',
redemptionVaultSwapper: '0x7F72AA0339b4944E1A77df847168AE1936c1EBBB',
},
mGLOeuro: {
token: '0x2F8bae126b8d416C009b82Ccd60e4f9cAbbd7702',
customFeed: '0x8B7B1689396ef4468df1c0faB181D29B47089609',
customFeedDv: '0x165a52261202c0e32F70dE8eD715A1e3cF9a228c',
customFeedRv: '0x0f46c1E4F51B92c2966BEA4bd5A52E71E423D6e0',
dataFeedDv: '0xc27Cd7c0203B73933dcbcF7bdB4cC5D7bDA80A64',
dataFeedRv: '0xF76653eBc3e47B80e89F0e5b9CF6A5CAaB848946',
depositVaultMorpho: '0xE0Dcf20b0460e1f9222528F3997F9D71Ad6375C5',
redemptionVaultMorpho: '0x1E9f04408FF38d1FB70CE939fC87D26D61F6F758',
},
},
arbitrum: {
accessControl: '0x0312A9D1Ff2372DDEdCBB21e4B6389aFc919aC4B',
Expand Down Expand Up @@ -1598,6 +1627,16 @@ export const midasAddressesPerNetwork: ConfigPerNetwork<
depositVault: '0x97b30c9D53A010009136b830f8A12f8d5624Bc43',
redemptionVaultSwapper: '0x12Ae90dCe5C2a4Ee5141FBfc408ff1022D051F42',
},
mGLO: {
token: '0x1Eaf7cceAA5DC6d605760718722fbcC58579b3Ff',
customFeed: '0x4c8f5CEd7F3211ad1D70B6F33e585309Fb1AcA75',
customFeedDv: '0x116fc90020456f33C801850e89a05a7edCd04673',
customFeedRv: '0x02E16Cc29B99A27725E77976400dDf204c295705',
dataFeedDv: '0xAf91e7754E12c90F3F07a1719E39a65Fc58204ff',
dataFeedRv: '0xB391486b0d058176A845aCcF2Cb160A2be9CfCb8',
depositVault: '0xa5d0E53059b648B7615B67602De4489079EAe2B4',
redemptionVaultSwapper: '0x99656E9753047F2769D10fa65B9D8ebfE44B35ab',
},
},
robinhood: {
accessControl: '0xe5F087203F9e7A6104c821ec25b1F0a4505D3cb5',
Expand Down
2 changes: 2 additions & 0 deletions config/types/tokens.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ export enum MTokenNameEnum {
sGold = 'sGold',
turtlePST = 'turtlePST',
mM1BTC = 'mM1BTC',
mGLOeuro = 'mGLOeuro',
}

export type MTokenName = keyof typeof MTokenNameEnum;
Expand Down Expand Up @@ -136,6 +137,7 @@ export enum PaymentTokenNameEnum {
winj = 'winj',
yinj = 'yinj',
eurc = 'eurc',
eurcv = 'eurcv',
usdg = 'usdg',
pyusd = 'pyusd',
ausd = 'ausd',
Expand Down
14 changes: 7 additions & 7 deletions contracts/RedemptionVault.sol
Original file line number Diff line number Diff line change
Expand Up @@ -615,14 +615,14 @@ contract RedemptionVault is ManageableVault, IRedemptionVault {

_requireAndUpdateAllowance(tokenOutCopy, amountTokenOut);

mToken.burn(user, calcResult.amountMTokenWithoutFee);
if (calcResult.feeAmount > 0)
_tokenTransferFromUser(
address(mToken),
feeReceiver,
calcResult.feeAmount,
18
);
mToken.burn(user, calcResult.amountMTokenWithoutFee);

_tokenTransferToUser(
tokenOutCopy,
Expand Down Expand Up @@ -682,19 +682,19 @@ contract RedemptionVault is ManageableVault, IRedemptionVault {

uint256 mTokenRate = mTokenDataFeed.getDataInBase18();

_tokenTransferFromUser(
address(mToken),
address(this),
calcResult.amountMTokenWithoutFee,
18 // mToken always have 18 decimals
);
if (calcResult.feeAmount > 0)
_tokenTransferFromUser(
address(mToken),
feeReceiver,
calcResult.feeAmount,
18
);
_tokenTransferFromUser(
address(mToken),
address(this),
calcResult.amountMTokenWithoutFee,
18 // mToken always have 18 decimals
);

requestId = currentRequestId.current();
currentRequestId.increment();
Expand Down
2 changes: 1 addition & 1 deletion contracts/RedemptionVaultWithAave.sol
Original file line number Diff line number Diff line change
Expand Up @@ -129,14 +129,14 @@ contract RedemptionVaultWithAave is RedemptionVault {

_requireAndUpdateAllowance(tokenOutCopy, amountTokenOut);

mToken.burn(user, calcResult.amountMTokenWithoutFee);
if (calcResult.feeAmount > 0)
_tokenTransferFromUser(
address(mToken),
feeReceiver,
calcResult.feeAmount,
18
);
mToken.burn(user, calcResult.amountMTokenWithoutFee);

uint256 amountTokenOutWithoutFeeFrom18 = ((calcResult
.amountMTokenWithoutFee * mTokenRate) / tokenOutRate)
Expand Down
2 changes: 1 addition & 1 deletion contracts/RedemptionVaultWithBUIDL.sol
Original file line number Diff line number Diff line change
Expand Up @@ -164,14 +164,14 @@ contract RedemptionVaultWIthBUIDL is RedemptionVault {

_requireAndUpdateAllowance(tokenOutCopy, amountTokenOut);

mToken.burn(user, calcResult.amountMTokenWithoutFee);
if (calcResult.feeAmount > 0)
_tokenTransferFromUser(
address(mToken),
feeReceiver,
calcResult.feeAmount,
18
);
mToken.burn(user, calcResult.amountMTokenWithoutFee);

amountTokenOutWithoutFee =
(calcResult.amountMTokenWithoutFee * mTokenRate) /
Expand Down
2 changes: 1 addition & 1 deletion contracts/RedemptionVaultWithMToken.sol
Original file line number Diff line number Diff line change
Expand Up @@ -162,14 +162,14 @@ contract RedemptionVaultWithMToken is RedemptionVault {

_requireAndUpdateAllowance(tokenOutCopy, amountTokenOut);

mToken.burn(user, calcResult.amountMTokenWithoutFee);
if (calcResult.feeAmount > 0)
_tokenTransferFromUser(
address(mToken),
feeReceiver,
calcResult.feeAmount,
18
);
mToken.burn(user, calcResult.amountMTokenWithoutFee);

uint256 amountTokenOutWithoutFeeFrom18 = ((calcResult
.amountMTokenWithoutFee * mTokenRate) / tokenOutRate)
Expand Down
2 changes: 1 addition & 1 deletion contracts/RedemptionVaultWithMorpho.sol
Original file line number Diff line number Diff line change
Expand Up @@ -133,14 +133,14 @@ contract RedemptionVaultWithMorpho is RedemptionVault {

_requireAndUpdateAllowance(tokenOutCopy, amountTokenOut);

mToken.burn(user, calcResult.amountMTokenWithoutFee);
if (calcResult.feeAmount > 0)
_tokenTransferFromUser(
address(mToken),
feeReceiver,
calcResult.feeAmount,
18
);
mToken.burn(user, calcResult.amountMTokenWithoutFee);

uint256 amountTokenOutWithoutFeeFrom18 = ((calcResult
.amountMTokenWithoutFee * mTokenRate) / tokenOutRate)
Expand Down
2 changes: 1 addition & 1 deletion contracts/RedemptionVaultWithUSTB.sol
Original file line number Diff line number Diff line change
Expand Up @@ -130,14 +130,14 @@ contract RedemptionVaultWithUSTB is RedemptionVault {

_requireAndUpdateAllowance(tokenOutCopy, amountTokenOut);

mToken.burn(user, calcResult.amountMTokenWithoutFee);
if (calcResult.feeAmount > 0)
_tokenTransferFromUser(
address(mToken),
feeReceiver,
calcResult.feeAmount,
18
);
mToken.burn(user, calcResult.amountMTokenWithoutFee);

uint256 amountTokenOutWithoutFeeFrom18 = ((calcResult
.amountMTokenWithoutFee * mTokenRate) / tokenOutRate)
Expand Down
62 changes: 62 additions & 0 deletions contracts/mTokenMinBalance.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.9;

import "./mToken.sol";

/**
* @title mTokenMinBalance
* @notice mToken with minimum balance checks
* @author RedDuck Software
*/
//solhint-disable contract-name-camelcase
abstract contract mTokenMinBalance is mToken {
/**
* @dev leaving a storage gap for futures updates
*/
uint256[50] private __gap;

/**
* @dev overrides _afterTokenTransfer function to check if the recipient has a minimum balance
*/
function _afterTokenTransfer(
address from,
address to,
uint256 amount
) internal virtual override {
if (from != address(0) && !_isMinBalanceExempt(from)) {
_validateMinBalance(from);
}

if (to != address(0) && !_isMinBalanceExempt(to)) {
_validateMinBalance(to);
}

super._afterTokenTransfer(from, to, amount);
}

/**
* @dev returns the role holder of which is exempt from min balance checks
*/
function _minBalanceExemptRole() internal view virtual returns (bytes32);

/**
* @dev checks if a user is exempt from min balance checks
* @param user address of the user
* @return bool true if the user is exempt from min balance checks
*/
function _isMinBalanceExempt(address user) private view returns (bool) {
return accessControl.hasRole(_minBalanceExemptRole(), user);
}

/**
* @dev validates the minimum balance of a user
* @param user address of the user
*/
function _validateMinBalance(address user) private view {
uint256 balance = balanceOf(user);
require(
balance == 0 || balance >= 1 ether,
"MTMB: min balance not met"
);
}
Comment on lines +55 to +61

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Hardcoding "1 ether" (which is "10**18") as the minimum balance assumes that all tokens using this contract will have 18 decimals and require exactly a 1-token minimum balance. If a token has fewer decimals (e.g., 6 decimals for USDC-pegged tokens or 8 decimals for BTC-pegged tokens), "1 ether" translates to an extremely large minimum balance (e.g., "10^12" or "10^10" tokens), making the token practically unusable.

To support tokens with different decimals and allow customization of the minimum balance, consider defining a virtual function "_minBalance()" that defaults to "10 ** decimals()" (or a customizable value) and can be overridden by specific token implementations.

    function _validateMinBalance(address user) private view {
        uint256 balance = balanceOf(user);
        require(
            balance == 0 || balance >= _minBalance(),
            "MTMB: min balance not met"
        );
    }

    /**
     * @dev returns the minimum balance required for non-exempt users
     */
    function _minBalance() internal view virtual returns (uint256) {
        return 10 ** decimals();
    }

}
41 changes: 41 additions & 0 deletions contracts/mTokenPermissionedMinBalance.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.9;

import "./mTokenPermissioned.sol";
import "./mTokenMinBalance.sol";

/**
* @title mTokenPermissionedMinBalance
* @notice mToken with permissioned transfers and minimum balance checks
* @author RedDuck Software
*/
//solhint-disable contract-name-camelcase
abstract contract mTokenPermissionedMinBalance is
mTokenMinBalance,
mTokenPermissioned
{
// no gap as we would upgrade some of the deployments
// to mTokenMinBalance

/**
* @dev overrides _beforeTokenTransfer function to call the parent hooks
*/
function _beforeTokenTransfer(
address from,
address to,
uint256 amount
) internal virtual override(mTokenPermissioned, mToken) {
mTokenPermissioned._beforeTokenTransfer(from, to, amount);
}

/**
* @dev overrides _afterTokenTransfer function to call the parent hooks
*/
function _afterTokenTransfer(
Comment on lines +31 to +34

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The docstring/comment for "_afterTokenTransfer" incorrectly states that it overrides "_beforeTokenTransfer". This is a copy-paste typo from the function above it.

    /**
     * @dev overrides _afterTokenTransfer function to call the parent hooks
     */
    function _afterTokenTransfer(

address from,
address to,
uint256 amount
) internal virtual override(mTokenMinBalance, ERC20Upgradeable) {
mTokenMinBalance._afterTokenTransfer(from, to, amount);
}
}
28 changes: 28 additions & 0 deletions contracts/products/mGLOeuro/MGloEuroCustomAggregatorFeed.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.9;

import "../../feeds/CustomAggregatorV3CompatibleFeed.sol";
import "./MGloEuroMidasAccessControlRoles.sol";

/**
* @title MGloEuroCustomAggregatorFeed
* @notice AggregatorV3 compatible feed for mGLOeuro,
* where price is submitted manually by feed admins
* @author RedDuck Software
*/
contract MGloEuroCustomAggregatorFeed is
CustomAggregatorV3CompatibleFeed,
MGloEuroMidasAccessControlRoles
{
/**
* @dev leaving a storage gap for futures updates
*/
uint256[50] private __gap;

/**
* @inheritdoc CustomAggregatorV3CompatibleFeed
*/
function feedAdminRole() public pure override returns (bytes32) {
return M_GLO_EURO_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE;
}
}
24 changes: 24 additions & 0 deletions contracts/products/mGLOeuro/MGloEuroDataFeed.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.9;

import "../../feeds/DataFeed.sol";
import "./MGloEuroMidasAccessControlRoles.sol";

/**
* @title MGloEuroDataFeed
* @notice DataFeed for mGLOeuro product
* @author RedDuck Software
*/
contract MGloEuroDataFeed is DataFeed, MGloEuroMidasAccessControlRoles {
/**
* @dev leaving a storage gap for futures updates
*/
uint256[50] private __gap;

/**
* @inheritdoc DataFeed
*/
function feedAdminRole() public pure override returns (bytes32) {
return M_GLO_EURO_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE;
}
}
Loading
Loading