From 2ee695e72967bf08d71d2116f82e9fda145cd350 Mon Sep 17 00:00:00 2001 From: kostyamospan Date: Tue, 14 Jul 2026 13:33:51 +0300 Subject: [PATCH 01/19] feat: mtoken min balance contract --- contracts/mTokenMinBalance.sol | 81 + contracts/mTokenPermissionedMinBalance.sol | 43 + contracts/testers/mTokenMinBalanceTest.sol | 39 + .../mTokenPermissionedMinBalanceTest.sol | 49 + test/common/fixtures.ts | 73 + test/common/mTBILL.helpers.ts | 15 +- test/unit/mtoken.test.ts | 1466 ++++++++++++++++- 7 files changed, 1763 insertions(+), 3 deletions(-) create mode 100644 contracts/mTokenMinBalance.sol create mode 100644 contracts/mTokenPermissionedMinBalance.sol create mode 100644 contracts/testers/mTokenMinBalanceTest.sol create mode 100644 contracts/testers/mTokenPermissionedMinBalanceTest.sol diff --git a/contracts/mTokenMinBalance.sol b/contracts/mTokenMinBalance.sol new file mode 100644 index 00000000..629f0b38 --- /dev/null +++ b/contracts/mTokenMinBalance.sol @@ -0,0 +1,81 @@ +// 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 { + /** + * @param user address of the user + * @param isFree bool if the user is free from min balance checks + */ + event SetIsFreeFromMinBalance(address indexed user, bool isFree); + + /** + * @notice mapping, user address => is free from min balance checks + */ + mapping(address => bool) public isFreeFromMinBalance; + + /** + * @dev leaving a storage gap for futures updates + */ + uint256[50] private __gap; + + /** + * @notice set if a user is free from min balance checks + * @param user address of the user + * @param isFree bool if the user is free from min balance checks + */ + function setIsFreeFromMinBalance(address user, bool isFree) + external + onlyRole(DEFAULT_ADMIN_ROLE, msg.sender) + { + if (isFreeFromMinBalance[user] == isFree) { + return; + } + + isFreeFromMinBalance[user] = isFree; + emit SetIsFreeFromMinBalance(user, isFree); + } + + /** + * @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) && !isFreeFromMinBalance[from]) { + _validateMinBalance(from, true); + } + + if (to != address(0) && !isFreeFromMinBalance[to]) { + _validateMinBalance(to, from != address(0)); + } + + super._afterTokenTransfer(from, to, amount); + } + + /** + * @dev validates the minimum balance of a user + * @param user address of the user + * @param canBeZero bool if the user can have a balance of 0 + */ + function _validateMinBalance(address user, bool canBeZero) internal view { + uint256 balance = balanceOf(user); + + bool isMinBalanceMet = balance >= 1 ether; + + if (canBeZero) { + isMinBalanceMet = balance == 0 || isMinBalanceMet; + } + + require(isMinBalanceMet, "MTMB: min balance not met"); + } +} diff --git a/contracts/mTokenPermissionedMinBalance.sol b/contracts/mTokenPermissionedMinBalance.sol new file mode 100644 index 00000000..9a22d767 --- /dev/null +++ b/contracts/mTokenPermissionedMinBalance.sol @@ -0,0 +1,43 @@ +// 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 + mTokenPermissioned, + mTokenMinBalance +{ + /** + * @dev leaving a storage gap for futures updates + */ + uint256[50] private __gap; + + /** + * @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 _beforeTokenTransfer function to call the parent hooks + */ + function _afterTokenTransfer( + address from, + address to, + uint256 amount + ) internal virtual override(mTokenMinBalance, ERC20Upgradeable) { + mTokenMinBalance._afterTokenTransfer(from, to, amount); + } +} diff --git a/contracts/testers/mTokenMinBalanceTest.sol b/contracts/testers/mTokenMinBalanceTest.sol new file mode 100644 index 00000000..e67c8516 --- /dev/null +++ b/contracts/testers/mTokenMinBalanceTest.sol @@ -0,0 +1,39 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.9; + +import "../mTokenMinBalance.sol"; + +//solhint-disable contract-name-camelcase +contract mTokenMinBalanceTest is mTokenMinBalance { + bytes32 public constant M_TOKEN_TEST_MINT_OPERATOR_ROLE = + keccak256("M_TOKEN_TEST_MINT_OPERATOR_ROLE"); + + bytes32 public constant M_TOKEN_TEST_BURN_OPERATOR_ROLE = + keccak256("M_TOKEN_TEST_BURN_OPERATOR_ROLE"); + + bytes32 public constant M_TOKEN_TEST_PAUSE_OPERATOR_ROLE = + keccak256("M_TOKEN_TEST_PAUSE_OPERATOR_ROLE"); + + function _disableInitializers() internal override {} + + function _getNameSymbol() + internal + pure + override + returns (string memory, string memory) + { + return ("mTokenMinBalanceTest", "mTokenMinBalanceTest"); + } + + function _minterRole() internal pure override returns (bytes32) { + return M_TOKEN_TEST_MINT_OPERATOR_ROLE; + } + + function _burnerRole() internal pure override returns (bytes32) { + return M_TOKEN_TEST_BURN_OPERATOR_ROLE; + } + + function _pauserRole() internal pure override returns (bytes32) { + return M_TOKEN_TEST_PAUSE_OPERATOR_ROLE; + } +} diff --git a/contracts/testers/mTokenPermissionedMinBalanceTest.sol b/contracts/testers/mTokenPermissionedMinBalanceTest.sol new file mode 100644 index 00000000..826b0284 --- /dev/null +++ b/contracts/testers/mTokenPermissionedMinBalanceTest.sol @@ -0,0 +1,49 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.9; + +import "../mTokenPermissionedMinBalance.sol"; + +//solhint-disable contract-name-camelcase +contract mTokenPermissionedMinBalanceTest is mTokenPermissionedMinBalance { + bytes32 public constant M_TOKEN_TEST_MINT_OPERATOR_ROLE = + keccak256("M_TOKEN_TEST_MINT_OPERATOR_ROLE"); + + bytes32 public constant M_TOKEN_TEST_BURN_OPERATOR_ROLE = + keccak256("M_TOKEN_TEST_BURN_OPERATOR_ROLE"); + + bytes32 public constant M_TOKEN_TEST_PAUSE_OPERATOR_ROLE = + keccak256("M_TOKEN_TEST_PAUSE_OPERATOR_ROLE"); + + bytes32 public constant M_TOKEN_TEST_GREENLISTED_ROLE = + keccak256("M_TOKEN_TEST_GREENLISTED_ROLE"); + + function _disableInitializers() internal override {} + + function _getNameSymbol() + internal + pure + override + returns (string memory, string memory) + { + return ( + "mTokenPermissionedMinBalanceTest", + "mTokenPermissionedMinBalanceTest" + ); + } + + function _minterRole() internal pure override returns (bytes32) { + return M_TOKEN_TEST_MINT_OPERATOR_ROLE; + } + + function _burnerRole() internal pure override returns (bytes32) { + return M_TOKEN_TEST_BURN_OPERATOR_ROLE; + } + + function _pauserRole() internal pure override returns (bytes32) { + return M_TOKEN_TEST_PAUSE_OPERATOR_ROLE; + } + + function _greenlistedRole() internal pure override returns (bytes32) { + return M_TOKEN_TEST_GREENLISTED_ROLE; + } +} diff --git a/test/common/fixtures.ts b/test/common/fixtures.ts index 70e0ab6e..f4b99982 100644 --- a/test/common/fixtures.ts +++ b/test/common/fixtures.ts @@ -57,6 +57,8 @@ import { MidasLzOFTAdapter__factory, MidasLzVaultComposerSyncTester, MTokenPermissionedTest__factory, + MTokenMinBalanceTest__factory, + MTokenPermissionedMinBalanceTest__factory, AxelarInterchainTokenServiceMock__factory, MidasAxelarVaultExecutableTester, LzEndpointV2Mock__factory, @@ -985,6 +987,77 @@ export const mTokenPermissionedFixture = async ( }; }; +/** + * mTokenMinBalanceTest fixture for min-balance unit tests. + */ +export const mTokenMinBalanceFixture = async ( + baseFixture?: Awaited>, +) => { + const fx = baseFixture ?? (await defaultDeploy()); + const { owner, accessControl } = fx; + + const mTokenMinBalance = await new MTokenMinBalanceTest__factory( + owner, + ).deploy(); + await mTokenMinBalance.initialize(accessControl.address); + + const mintRole = await mTokenMinBalance.M_TOKEN_TEST_MINT_OPERATOR_ROLE(); + const burnRole = await mTokenMinBalance.M_TOKEN_TEST_BURN_OPERATOR_ROLE(); + const pauseRole = await mTokenMinBalance.M_TOKEN_TEST_PAUSE_OPERATOR_ROLE(); + + await accessControl.grantRole(mintRole, owner.address); + await accessControl.grantRole(burnRole, owner.address); + await accessControl.grantRole(pauseRole, owner.address); + + return { + ...fx, + mTokenMinBalance, + mTokenMinBalanceRoles: { + mint: mintRole, + burn: burnRole, + pause: pauseRole, + }, + }; +}; + +/** + * mTokenPermissionedMinBalanceTest fixture for permissioned + min-balance unit tests. + */ +export const mTokenPermissionedMinBalanceFixture = async ( + baseFixture?: Awaited>, +) => { + const fx = baseFixture ?? (await defaultDeploy()); + const { owner, accessControl } = fx; + + const mTokenPermissionedMinBalance = + await new MTokenPermissionedMinBalanceTest__factory(owner).deploy(); + await mTokenPermissionedMinBalance.initialize(accessControl.address); + + const mintRole = + await mTokenPermissionedMinBalance.M_TOKEN_TEST_MINT_OPERATOR_ROLE(); + const burnRole = + await mTokenPermissionedMinBalance.M_TOKEN_TEST_BURN_OPERATOR_ROLE(); + const pauseRole = + await mTokenPermissionedMinBalance.M_TOKEN_TEST_PAUSE_OPERATOR_ROLE(); + const greenlistedRole = + await mTokenPermissionedMinBalance.M_TOKEN_TEST_GREENLISTED_ROLE(); + + await accessControl.grantRole(mintRole, owner.address); + await accessControl.grantRole(burnRole, owner.address); + await accessControl.grantRole(pauseRole, owner.address); + + return { + ...fx, + mTokenPermissionedMinBalance, + mTokenPermissionedMinBalanceRoles: { + mint: mintRole, + burn: burnRole, + pause: pauseRole, + greenlisted: greenlistedRole, + }, + }; +}; + export const acreAdapterFixture = async () => { const defaultFixture = await defaultDeploy(); diff --git a/test/common/mTBILL.helpers.ts b/test/common/mTBILL.helpers.ts index 8613a8b3..a3346d73 100644 --- a/test/common/mTBILL.helpers.ts +++ b/test/common/mTBILL.helpers.ts @@ -5,10 +5,21 @@ import { defaultAbiCoder, solidityKeccak256 } from 'ethers/lib/utils'; import { Account, OptionalCommonParams, getAccount } from './common.helpers'; -import { MTBILL, MToken, MTokenPermissioned } from '../../typechain-types'; +import { + MTBILL, + MToken, + MTokenMinBalance, + MTokenPermissioned, + MTokenPermissionedMinBalance, +} from '../../typechain-types'; type CommonParams = { - tokenContract: MToken | MTBILL | MTokenPermissioned; + tokenContract: + | MToken + | MTBILL + | MTokenPermissioned + | MTokenMinBalance + | MTokenPermissionedMinBalance; owner: SignerWithAddress; }; diff --git a/test/unit/mtoken.test.ts b/test/unit/mtoken.test.ts index 76807887..64bb3e53 100644 --- a/test/unit/mtoken.test.ts +++ b/test/unit/mtoken.test.ts @@ -5,7 +5,12 @@ import { parseUnits } from 'ethers/lib/utils'; import { MTokenNameEnum } from '../../config'; import { getRolesForToken, getRolesNamesForToken } from '../../helpers/roles'; import { acErrors, blackList } from '../common/ac.helpers'; -import { defaultDeploy, mTokenPermissionedFixture } from '../common/fixtures'; +import { + defaultDeploy, + mTokenMinBalanceFixture, + mTokenPermissionedFixture, + mTokenPermissionedMinBalanceFixture, +} from '../common/fixtures'; import { burn, mint } from '../common/mTBILL.helpers'; import { tokenContractsTests } from '../common/token.tests'; @@ -447,6 +452,1465 @@ describe('Token contracts', () => { }); }); }); + + describe('mTokenMinBalance (mTokenMinBalanceTest)', () => { + describe('setIsFreeFromMinBalance()', () => { + it('should fail: call from address without DEFAULT_ADMIN_ROLE', async () => { + const baseFixture = await defaultDeploy(); + const { regularAccounts, mTokenMinBalance } = await loadFixture( + mTokenMinBalanceFixture.bind(this, baseFixture), + ); + + await expect( + mTokenMinBalance + .connect(regularAccounts[0]) + .setIsFreeFromMinBalance(regularAccounts[1].address, true), + ).revertedWith(acErrors.WMAC_HASNT_ROLE); + }); + + it('set isFreeFromMinBalance to true', async () => { + const baseFixture = await defaultDeploy(); + const { owner, regularAccounts, mTokenMinBalance } = await loadFixture( + mTokenMinBalanceFixture.bind(this, baseFixture), + ); + + const user = regularAccounts[0]; + await expect( + mTokenMinBalance + .connect(owner) + .setIsFreeFromMinBalance(user.address, true), + ) + .to.emit(mTokenMinBalance, 'SetIsFreeFromMinBalance') + .withArgs(user.address, true); + + expect(await mTokenMinBalance.isFreeFromMinBalance(user.address)).eq( + true, + ); + }); + + it('set isFreeFromMinBalance to false', async () => { + const baseFixture = await defaultDeploy(); + const { owner, regularAccounts, mTokenMinBalance } = await loadFixture( + mTokenMinBalanceFixture.bind(this, baseFixture), + ); + + const user = regularAccounts[0]; + await mTokenMinBalance + .connect(owner) + .setIsFreeFromMinBalance(user.address, true); + + await expect( + mTokenMinBalance + .connect(owner) + .setIsFreeFromMinBalance(user.address, false), + ) + .to.emit(mTokenMinBalance, 'SetIsFreeFromMinBalance') + .withArgs(user.address, false); + + expect(await mTokenMinBalance.isFreeFromMinBalance(user.address)).eq( + false, + ); + }); + + it('no-op when value is unchanged', async () => { + const baseFixture = await defaultDeploy(); + const { owner, regularAccounts, mTokenMinBalance } = await loadFixture( + mTokenMinBalanceFixture.bind(this, baseFixture), + ); + + const user = regularAccounts[0]; + await expect( + mTokenMinBalance + .connect(owner) + .setIsFreeFromMinBalance(user.address, false), + ).to.not.emit(mTokenMinBalance, 'SetIsFreeFromMinBalance'); + }); + }); + + describe('transfer()', () => { + it('transfer when both parties hold above min balance after transfer', async () => { + const baseFixture = await defaultDeploy(); + const { owner, regularAccounts, mTokenMinBalance } = await loadFixture( + mTokenMinBalanceFixture.bind(this, baseFixture), + ); + + const from = regularAccounts[0]; + const to = regularAccounts[1]; + const amount = parseUnits('0.1'); + + await mint( + { tokenContract: mTokenMinBalance, owner }, + from, + parseUnits('3'), + ); + await mint( + { tokenContract: mTokenMinBalance, owner }, + to, + parseUnits('3'), + ); + + await expect( + mTokenMinBalance.connect(from).transfer(to.address, amount), + ).not.reverted; + expect(await mTokenMinBalance.balanceOf(from.address)).eq( + parseUnits('2.9'), + ); + expect(await mTokenMinBalance.balanceOf(to.address)).eq( + parseUnits('3.1'), + ); + }); + + it('should fail: transfer dust to empty recipient', async () => { + const baseFixture = await defaultDeploy(); + const { owner, regularAccounts, mTokenMinBalance } = await loadFixture( + mTokenMinBalanceFixture.bind(this, baseFixture), + ); + + const from = regularAccounts[0]; + const to = regularAccounts[1]; + + await mint( + { tokenContract: mTokenMinBalance, owner }, + from, + parseUnits('3'), + ); + + await expect( + mTokenMinBalance + .connect(from) + .transfer(to.address, parseUnits('0.1')), + ).revertedWith('MTMB: min balance not met'); + }); + + it('transfer dust to empty recipient when recipient is free from min balance', async () => { + const baseFixture = await defaultDeploy(); + const { owner, regularAccounts, mTokenMinBalance } = await loadFixture( + mTokenMinBalanceFixture.bind(this, baseFixture), + ); + + const from = regularAccounts[0]; + const to = regularAccounts[1]; + const amount = parseUnits('0.1'); + + await mint( + { tokenContract: mTokenMinBalance, owner }, + from, + parseUnits('3'), + ); + await mTokenMinBalance + .connect(owner) + .setIsFreeFromMinBalance(to.address, true); + + await expect( + mTokenMinBalance.connect(from).transfer(to.address, amount), + ).not.reverted; + expect(await mTokenMinBalance.balanceOf(to.address)).eq(amount); + }); + + it('should fail: transfer dust from waived sender to empty non-waived recipient', async () => { + const baseFixture = await defaultDeploy(); + const { owner, regularAccounts, mTokenMinBalance } = await loadFixture( + mTokenMinBalanceFixture.bind(this, baseFixture), + ); + + const from = regularAccounts[0]; + const to = regularAccounts[1]; + + await mTokenMinBalance + .connect(owner) + .setIsFreeFromMinBalance(from.address, true); + await mint( + { tokenContract: mTokenMinBalance, owner }, + from, + parseUnits('0.3'), + ); + + await expect( + mTokenMinBalance + .connect(from) + .transfer(to.address, parseUnits('0.1')), + ).revertedWith('MTMB: min balance not met'); + }); + + it('transfer entire balance leaving sender at zero', async () => { + const baseFixture = await defaultDeploy(); + const { owner, regularAccounts, mTokenMinBalance } = await loadFixture( + mTokenMinBalanceFixture.bind(this, baseFixture), + ); + + const from = regularAccounts[0]; + const to = regularAccounts[1]; + const amount = parseUnits('3'); + + await mint({ tokenContract: mTokenMinBalance, owner }, from, amount); + + await expect( + mTokenMinBalance.connect(from).transfer(to.address, amount), + ).not.reverted; + expect(await mTokenMinBalance.balanceOf(from.address)).eq(0); + expect(await mTokenMinBalance.balanceOf(to.address)).eq(amount); + }); + + it('should fail: transfer leaving sender below min balance', async () => { + const baseFixture = await defaultDeploy(); + const { owner, regularAccounts, mTokenMinBalance } = await loadFixture( + mTokenMinBalanceFixture.bind(this, baseFixture), + ); + + const from = regularAccounts[0]; + const to = regularAccounts[1]; + + await mint( + { tokenContract: mTokenMinBalance, owner }, + from, + parseUnits('3'), + ); + await mint( + { tokenContract: mTokenMinBalance, owner }, + to, + parseUnits('1'), + ); + + await expect( + mTokenMinBalance + .connect(from) + .transfer(to.address, parseUnits('2.5')), + ).revertedWith('MTMB: min balance not met'); + }); + + it('transfer leaving sender below min balance when sender is free from min balance', async () => { + const baseFixture = await defaultDeploy(); + const { owner, regularAccounts, mTokenMinBalance } = await loadFixture( + mTokenMinBalanceFixture.bind(this, baseFixture), + ); + + const from = regularAccounts[0]; + const to = regularAccounts[1]; + + await mint( + { tokenContract: mTokenMinBalance, owner }, + from, + parseUnits('3'), + ); + await mint( + { tokenContract: mTokenMinBalance, owner }, + to, + parseUnits('1'), + ); + await mTokenMinBalance + .connect(owner) + .setIsFreeFromMinBalance(from.address, true); + + await expect( + mTokenMinBalance + .connect(from) + .transfer(to.address, parseUnits('2.5')), + ).not.reverted; + expect(await mTokenMinBalance.balanceOf(from.address)).eq( + parseUnits('0.5'), + ); + }); + + it('transfer leaving both parties at exactly min balance', async () => { + const baseFixture = await defaultDeploy(); + const { owner, regularAccounts, mTokenMinBalance } = await loadFixture( + mTokenMinBalanceFixture.bind(this, baseFixture), + ); + + const from = regularAccounts[0]; + const to = regularAccounts[1]; + + await mint( + { tokenContract: mTokenMinBalance, owner }, + from, + parseUnits('2'), + ); + await mint( + { tokenContract: mTokenMinBalance, owner }, + to, + parseUnits('1'), + ); + + await expect( + mTokenMinBalance.connect(from).transfer(to.address, parseUnits('1')), + ).not.reverted; + expect(await mTokenMinBalance.balanceOf(from.address)).eq( + parseUnits('1'), + ); + expect(await mTokenMinBalance.balanceOf(to.address)).eq( + parseUnits('2'), + ); + }); + + it('should fail: transfer when from is blacklisted', async () => { + const baseFixture = await defaultDeploy(); + const { owner, accessControl, regularAccounts, mTokenMinBalance } = + await loadFixture(mTokenMinBalanceFixture.bind(this, baseFixture)); + + const from = regularAccounts[0]; + const to = regularAccounts[1]; + + await mint( + { tokenContract: mTokenMinBalance, owner }, + from, + parseUnits('3'), + ); + await mint( + { tokenContract: mTokenMinBalance, owner }, + to, + parseUnits('3'), + ); + await blackList( + { blacklistable: mTokenMinBalance, accessControl, owner }, + from, + ); + + await expect( + mTokenMinBalance + .connect(from) + .transfer(to.address, parseUnits('0.1')), + ).revertedWith(acErrors.WMAC_HAS_ROLE); + }); + + it('should fail: transfer when to is blacklisted', async () => { + const baseFixture = await defaultDeploy(); + const { owner, accessControl, regularAccounts, mTokenMinBalance } = + await loadFixture(mTokenMinBalanceFixture.bind(this, baseFixture)); + + const from = regularAccounts[0]; + const to = regularAccounts[1]; + + await mint( + { tokenContract: mTokenMinBalance, owner }, + from, + parseUnits('3'), + ); + await mint( + { tokenContract: mTokenMinBalance, owner }, + to, + parseUnits('3'), + ); + await blackList( + { blacklistable: mTokenMinBalance, accessControl, owner }, + to, + ); + + await expect( + mTokenMinBalance + .connect(from) + .transfer(to.address, parseUnits('0.1')), + ).revertedWith(acErrors.WMAC_HAS_ROLE); + }); + + it('should fail: transfer when token is paused', async () => { + const baseFixture = await defaultDeploy(); + const { owner, regularAccounts, mTokenMinBalance } = await loadFixture( + mTokenMinBalanceFixture.bind(this, baseFixture), + ); + + const from = regularAccounts[0]; + const to = regularAccounts[1]; + + await mint( + { tokenContract: mTokenMinBalance, owner }, + from, + parseUnits('3'), + ); + await mint( + { tokenContract: mTokenMinBalance, owner }, + to, + parseUnits('3'), + ); + await mTokenMinBalance.connect(owner).pause(); + + await expect( + mTokenMinBalance + .connect(from) + .transfer(to.address, parseUnits('0.1')), + ).revertedWith('ERC20Pausable: token transfer while paused'); + }); + }); + + describe('transferFrom()', () => { + it('transferFrom when both parties hold above min balance after transfer', async () => { + const baseFixture = await defaultDeploy(); + const { owner, regularAccounts, mTokenMinBalance } = await loadFixture( + mTokenMinBalanceFixture.bind(this, baseFixture), + ); + + const from = regularAccounts[0]; + const spender = regularAccounts[1]; + const to = regularAccounts[2]; + const amount = parseUnits('0.1'); + + await mint( + { tokenContract: mTokenMinBalance, owner }, + from, + parseUnits('3'), + ); + await mint( + { tokenContract: mTokenMinBalance, owner }, + to, + parseUnits('3'), + ); + await mTokenMinBalance.connect(from).approve(spender.address, amount); + + await expect( + mTokenMinBalance + .connect(spender) + .transferFrom(from.address, to.address, amount), + ).not.reverted; + }); + + it('should fail: transferFrom dust to empty recipient', async () => { + const baseFixture = await defaultDeploy(); + const { owner, regularAccounts, mTokenMinBalance } = await loadFixture( + mTokenMinBalanceFixture.bind(this, baseFixture), + ); + + const from = regularAccounts[0]; + const spender = regularAccounts[1]; + const to = regularAccounts[2]; + const amount = parseUnits('0.1'); + + await mint( + { tokenContract: mTokenMinBalance, owner }, + from, + parseUnits('3'), + ); + await mTokenMinBalance.connect(from).approve(spender.address, amount); + + await expect( + mTokenMinBalance + .connect(spender) + .transferFrom(from.address, to.address, amount), + ).revertedWith('MTMB: min balance not met'); + }); + + it('should fail: transferFrom when from is blacklisted', async () => { + const baseFixture = await defaultDeploy(); + const { owner, accessControl, regularAccounts, mTokenMinBalance } = + await loadFixture(mTokenMinBalanceFixture.bind(this, baseFixture)); + + const from = regularAccounts[0]; + const spender = regularAccounts[1]; + const to = regularAccounts[2]; + const amount = parseUnits('0.1'); + + await mint( + { tokenContract: mTokenMinBalance, owner }, + from, + parseUnits('3'), + ); + await mint( + { tokenContract: mTokenMinBalance, owner }, + to, + parseUnits('3'), + ); + await mTokenMinBalance.connect(from).approve(spender.address, amount); + await blackList( + { blacklistable: mTokenMinBalance, accessControl, owner }, + from, + ); + + await expect( + mTokenMinBalance + .connect(spender) + .transferFrom(from.address, to.address, amount), + ).revertedWith(acErrors.WMAC_HAS_ROLE); + }); + + it('should fail: transferFrom when to is blacklisted', async () => { + const baseFixture = await defaultDeploy(); + const { owner, accessControl, regularAccounts, mTokenMinBalance } = + await loadFixture(mTokenMinBalanceFixture.bind(this, baseFixture)); + + const from = regularAccounts[0]; + const spender = regularAccounts[1]; + const to = regularAccounts[2]; + const amount = parseUnits('0.1'); + + await mint( + { tokenContract: mTokenMinBalance, owner }, + from, + parseUnits('3'), + ); + await mint( + { tokenContract: mTokenMinBalance, owner }, + to, + parseUnits('3'), + ); + await mTokenMinBalance.connect(from).approve(spender.address, amount); + await blackList( + { blacklistable: mTokenMinBalance, accessControl, owner }, + to, + ); + + await expect( + mTokenMinBalance + .connect(spender) + .transferFrom(from.address, to.address, amount), + ).revertedWith(acErrors.WMAC_HAS_ROLE); + }); + }); + + describe('mint()', () => { + it('mint at least 1 token to empty recipient', async () => { + const baseFixture = await defaultDeploy(); + const { owner, regularAccounts, mTokenMinBalance } = await loadFixture( + mTokenMinBalanceFixture.bind(this, baseFixture), + ); + + await mint( + { tokenContract: mTokenMinBalance, owner }, + regularAccounts[0], + parseUnits('1'), + ); + }); + + it('should fail: mint less than 1 token to empty recipient', async () => { + const baseFixture = await defaultDeploy(); + const { owner, regularAccounts, mTokenMinBalance } = await loadFixture( + mTokenMinBalanceFixture.bind(this, baseFixture), + ); + + await mint( + { tokenContract: mTokenMinBalance, owner }, + regularAccounts[0], + parseUnits('0.5'), + { revertMessage: 'MTMB: min balance not met' }, + ); + }); + + it('mint less than 1 token to empty recipient when free from min balance', async () => { + const baseFixture = await defaultDeploy(); + const { owner, regularAccounts, mTokenMinBalance } = await loadFixture( + mTokenMinBalanceFixture.bind(this, baseFixture), + ); + + const to = regularAccounts[0]; + await mTokenMinBalance + .connect(owner) + .setIsFreeFromMinBalance(to.address, true); + + await mint( + { tokenContract: mTokenMinBalance, owner }, + to, + parseUnits('0.5'), + ); + }); + + it('mint dust to recipient that already holds min balance', async () => { + const baseFixture = await defaultDeploy(); + const { owner, regularAccounts, mTokenMinBalance } = await loadFixture( + mTokenMinBalanceFixture.bind(this, baseFixture), + ); + + const to = regularAccounts[0]; + await mint( + { tokenContract: mTokenMinBalance, owner }, + to, + parseUnits('1'), + ); + await mint( + { tokenContract: mTokenMinBalance, owner }, + to, + parseUnits('0.1'), + ); + }); + + it('should fail: mint when recipient is blacklisted', async () => { + const baseFixture = await defaultDeploy(); + const { owner, accessControl, regularAccounts, mTokenMinBalance } = + await loadFixture(mTokenMinBalanceFixture.bind(this, baseFixture)); + + const to = regularAccounts[0]; + await blackList( + { blacklistable: mTokenMinBalance, accessControl, owner }, + to, + ); + + await mint( + { tokenContract: mTokenMinBalance, owner }, + to, + parseUnits('1'), + { revertMessage: acErrors.WMAC_HAS_ROLE }, + ); + }); + }); + + describe('burn()', () => { + it('burn leaving holder at zero', async () => { + const baseFixture = await defaultDeploy(); + const { owner, regularAccounts, mTokenMinBalance } = await loadFixture( + mTokenMinBalanceFixture.bind(this, baseFixture), + ); + + const holder = regularAccounts[0]; + await mint( + { tokenContract: mTokenMinBalance, owner }, + holder, + parseUnits('1'), + ); + await burn( + { tokenContract: mTokenMinBalance, owner }, + holder, + parseUnits('1'), + ); + }); + + it('burn leaving holder at or above min balance', async () => { + const baseFixture = await defaultDeploy(); + const { owner, regularAccounts, mTokenMinBalance } = await loadFixture( + mTokenMinBalanceFixture.bind(this, baseFixture), + ); + + const holder = regularAccounts[0]; + await mint( + { tokenContract: mTokenMinBalance, owner }, + holder, + parseUnits('3'), + ); + await burn( + { tokenContract: mTokenMinBalance, owner }, + holder, + parseUnits('1'), + ); + expect(await mTokenMinBalance.balanceOf(holder.address)).eq( + parseUnits('2'), + ); + }); + + it('should fail: burn leaving holder below min balance', async () => { + const baseFixture = await defaultDeploy(); + const { owner, regularAccounts, mTokenMinBalance } = await loadFixture( + mTokenMinBalanceFixture.bind(this, baseFixture), + ); + + const holder = regularAccounts[0]; + await mint( + { tokenContract: mTokenMinBalance, owner }, + holder, + parseUnits('3'), + ); + + await burn( + { tokenContract: mTokenMinBalance, owner }, + holder, + parseUnits('2.5'), + { revertMessage: 'MTMB: min balance not met' }, + ); + }); + + it('burn leaving holder below min balance when free from min balance', async () => { + const baseFixture = await defaultDeploy(); + const { owner, regularAccounts, mTokenMinBalance } = await loadFixture( + mTokenMinBalanceFixture.bind(this, baseFixture), + ); + + const holder = regularAccounts[0]; + await mint( + { tokenContract: mTokenMinBalance, owner }, + holder, + parseUnits('3'), + ); + await mTokenMinBalance + .connect(owner) + .setIsFreeFromMinBalance(holder.address, true); + + await burn( + { tokenContract: mTokenMinBalance, owner }, + holder, + parseUnits('2.5'), + ); + expect(await mTokenMinBalance.balanceOf(holder.address)).eq( + parseUnits('0.5'), + ); + }); + + it('should fail: burn when holder is blacklisted', async () => { + const baseFixture = await defaultDeploy(); + const { owner, accessControl, regularAccounts, mTokenMinBalance } = + await loadFixture(mTokenMinBalanceFixture.bind(this, baseFixture)); + + const holder = regularAccounts[0]; + await mint( + { tokenContract: mTokenMinBalance, owner }, + holder, + parseUnits('1'), + ); + await blackList( + { blacklistable: mTokenMinBalance, accessControl, owner }, + holder, + ); + + await burn( + { tokenContract: mTokenMinBalance, owner }, + holder, + parseUnits('1'), + { revertMessage: acErrors.WMAC_HAS_ROLE }, + ); + }); + + it('burnGoverned when holder is blacklisted', async () => { + const baseFixture = await defaultDeploy(); + const { owner, accessControl, regularAccounts, mTokenMinBalance } = + await loadFixture(mTokenMinBalanceFixture.bind(this, baseFixture)); + + const holder = regularAccounts[0]; + const amount = parseUnits('1'); + await mint({ tokenContract: mTokenMinBalance, owner }, holder, amount); + await blackList( + { blacklistable: mTokenMinBalance, accessControl, owner }, + holder, + ); + + await expect( + mTokenMinBalance.connect(owner).burnGoverned(holder.address, amount), + ).not.reverted; + expect(await mTokenMinBalance.balanceOf(holder.address)).eq(0); + }); + + it('should fail: burnGoverned leaving holder below min balance', async () => { + const baseFixture = await defaultDeploy(); + const { owner, regularAccounts, mTokenMinBalance } = await loadFixture( + mTokenMinBalanceFixture.bind(this, baseFixture), + ); + + const holder = regularAccounts[0]; + await mint( + { tokenContract: mTokenMinBalance, owner }, + holder, + parseUnits('3'), + ); + + await expect( + mTokenMinBalance + .connect(owner) + .burnGoverned(holder.address, parseUnits('2.5')), + ).revertedWith('MTMB: min balance not met'); + }); + }); + }); + + describe('mTokenPermissionedMinBalance (mTokenPermissionedMinBalanceTest)', () => { + describe('transfer()', () => { + it('should fail: transfer when sender is not greenlisted', async () => { + const baseFixture = await defaultDeploy(); + const { + owner, + accessControl, + regularAccounts, + mTokenPermissionedMinBalance, + mTokenPermissionedMinBalanceRoles, + } = await loadFixture( + mTokenPermissionedMinBalanceFixture.bind(this, baseFixture), + ); + + const from = regularAccounts[0]; + const to = regularAccounts[1]; + const { greenlisted } = mTokenPermissionedMinBalanceRoles; + + await accessControl.grantRole(greenlisted, from.address); + await accessControl.grantRole(greenlisted, to.address); + await mint( + { tokenContract: mTokenPermissionedMinBalance, owner }, + from, + parseUnits('3'), + ); + await mint( + { tokenContract: mTokenPermissionedMinBalance, owner }, + to, + parseUnits('3'), + ); + await accessControl.revokeRole(greenlisted, from.address); + + await expect( + mTokenPermissionedMinBalance + .connect(from) + .transfer(to.address, parseUnits('0.1')), + ).revertedWith(acErrors.WMAC_HASNT_ROLE); + }); + + it('should fail: transfer when recipient is not greenlisted', async () => { + const baseFixture = await defaultDeploy(); + const { + owner, + accessControl, + regularAccounts, + mTokenPermissionedMinBalance, + mTokenPermissionedMinBalanceRoles, + } = await loadFixture( + mTokenPermissionedMinBalanceFixture.bind(this, baseFixture), + ); + + const from = regularAccounts[0]; + const to = regularAccounts[1]; + const { greenlisted } = mTokenPermissionedMinBalanceRoles; + + await accessControl.grantRole(greenlisted, from.address); + await mint( + { tokenContract: mTokenPermissionedMinBalance, owner }, + from, + parseUnits('3'), + ); + + await expect( + mTokenPermissionedMinBalance + .connect(from) + .transfer(to.address, parseUnits('0.1')), + ).revertedWith(acErrors.WMAC_HASNT_ROLE); + }); + + it('should fail: transfer dust to empty recipient when both greenlisted', async () => { + const baseFixture = await defaultDeploy(); + const { + owner, + accessControl, + regularAccounts, + mTokenPermissionedMinBalance, + mTokenPermissionedMinBalanceRoles, + } = await loadFixture( + mTokenPermissionedMinBalanceFixture.bind(this, baseFixture), + ); + + const from = regularAccounts[0]; + const to = regularAccounts[1]; + const { greenlisted } = mTokenPermissionedMinBalanceRoles; + + await accessControl.grantRole(greenlisted, from.address); + await accessControl.grantRole(greenlisted, to.address); + await mint( + { tokenContract: mTokenPermissionedMinBalance, owner }, + from, + parseUnits('3'), + ); + + await expect( + mTokenPermissionedMinBalance + .connect(from) + .transfer(to.address, parseUnits('0.1')), + ).revertedWith('MTMB: min balance not met'); + }); + + it('transfer dust to empty recipient when recipient is free from min balance', async () => { + const baseFixture = await defaultDeploy(); + const { + owner, + accessControl, + regularAccounts, + mTokenPermissionedMinBalance, + mTokenPermissionedMinBalanceRoles, + } = await loadFixture( + mTokenPermissionedMinBalanceFixture.bind(this, baseFixture), + ); + + const from = regularAccounts[0]; + const to = regularAccounts[1]; + const { greenlisted } = mTokenPermissionedMinBalanceRoles; + const amount = parseUnits('0.1'); + + await accessControl.grantRole(greenlisted, from.address); + await accessControl.grantRole(greenlisted, to.address); + await mint( + { tokenContract: mTokenPermissionedMinBalance, owner }, + from, + parseUnits('3'), + ); + await mTokenPermissionedMinBalance + .connect(owner) + .setIsFreeFromMinBalance(to.address, true); + + await expect( + mTokenPermissionedMinBalance + .connect(from) + .transfer(to.address, amount), + ).not.reverted; + expect(await mTokenPermissionedMinBalance.balanceOf(to.address)).eq( + amount, + ); + }); + + it('should fail: transfer dust from waived sender to empty non-waived recipient', async () => { + const baseFixture = await defaultDeploy(); + const { + owner, + accessControl, + regularAccounts, + mTokenPermissionedMinBalance, + mTokenPermissionedMinBalanceRoles, + } = await loadFixture( + mTokenPermissionedMinBalanceFixture.bind(this, baseFixture), + ); + + const from = regularAccounts[0]; + const to = regularAccounts[1]; + const { greenlisted } = mTokenPermissionedMinBalanceRoles; + + await accessControl.grantRole(greenlisted, from.address); + await accessControl.grantRole(greenlisted, to.address); + await mTokenPermissionedMinBalance + .connect(owner) + .setIsFreeFromMinBalance(from.address, true); + await mint( + { tokenContract: mTokenPermissionedMinBalance, owner }, + from, + parseUnits('0.3'), + ); + + await expect( + mTokenPermissionedMinBalance + .connect(from) + .transfer(to.address, parseUnits('0.1')), + ).revertedWith('MTMB: min balance not met'); + }); + + it('transfer when both greenlisted and above min balance', async () => { + const baseFixture = await defaultDeploy(); + const { + owner, + accessControl, + regularAccounts, + mTokenPermissionedMinBalance, + mTokenPermissionedMinBalanceRoles, + } = await loadFixture( + mTokenPermissionedMinBalanceFixture.bind(this, baseFixture), + ); + + const from = regularAccounts[0]; + const to = regularAccounts[1]; + const { greenlisted } = mTokenPermissionedMinBalanceRoles; + const amount = parseUnits('0.1'); + + await accessControl.grantRole(greenlisted, from.address); + await accessControl.grantRole(greenlisted, to.address); + await mint( + { tokenContract: mTokenPermissionedMinBalance, owner }, + from, + parseUnits('3'), + ); + await mint( + { tokenContract: mTokenPermissionedMinBalance, owner }, + to, + parseUnits('3'), + ); + + await expect( + mTokenPermissionedMinBalance + .connect(from) + .transfer(to.address, amount), + ).not.reverted; + }); + + it('should fail: transfer when from is blacklisted', async () => { + const baseFixture = await defaultDeploy(); + const { + owner, + accessControl, + regularAccounts, + mTokenPermissionedMinBalance, + mTokenPermissionedMinBalanceRoles, + } = await loadFixture( + mTokenPermissionedMinBalanceFixture.bind(this, baseFixture), + ); + + const from = regularAccounts[0]; + const to = regularAccounts[1]; + const { greenlisted } = mTokenPermissionedMinBalanceRoles; + + await accessControl.grantRole(greenlisted, from.address); + await accessControl.grantRole(greenlisted, to.address); + await mint( + { tokenContract: mTokenPermissionedMinBalance, owner }, + from, + parseUnits('3'), + ); + await mint( + { tokenContract: mTokenPermissionedMinBalance, owner }, + to, + parseUnits('3'), + ); + await blackList( + { + blacklistable: mTokenPermissionedMinBalance, + accessControl, + owner, + }, + from, + ); + + await expect( + mTokenPermissionedMinBalance + .connect(from) + .transfer(to.address, parseUnits('0.1')), + ).revertedWith(acErrors.WMAC_HAS_ROLE); + }); + + it('should fail: transfer when to is blacklisted', async () => { + const baseFixture = await defaultDeploy(); + const { + owner, + accessControl, + regularAccounts, + mTokenPermissionedMinBalance, + mTokenPermissionedMinBalanceRoles, + } = await loadFixture( + mTokenPermissionedMinBalanceFixture.bind(this, baseFixture), + ); + + const from = regularAccounts[0]; + const to = regularAccounts[1]; + const { greenlisted } = mTokenPermissionedMinBalanceRoles; + + await accessControl.grantRole(greenlisted, from.address); + await accessControl.grantRole(greenlisted, to.address); + await mint( + { tokenContract: mTokenPermissionedMinBalance, owner }, + from, + parseUnits('3'), + ); + await mint( + { tokenContract: mTokenPermissionedMinBalance, owner }, + to, + parseUnits('3'), + ); + await blackList( + { + blacklistable: mTokenPermissionedMinBalance, + accessControl, + owner, + }, + to, + ); + + await expect( + mTokenPermissionedMinBalance + .connect(from) + .transfer(to.address, parseUnits('0.1')), + ).revertedWith(acErrors.WMAC_HAS_ROLE); + }); + }); + + describe('mint()', () => { + it('should fail: mint when receiver is not greenlisted', async () => { + const baseFixture = await defaultDeploy(); + const { owner, regularAccounts, mTokenPermissionedMinBalance } = + await loadFixture( + mTokenPermissionedMinBalanceFixture.bind(this, baseFixture), + ); + + await mint( + { tokenContract: mTokenPermissionedMinBalance, owner }, + regularAccounts[0], + parseUnits('1'), + { revertMessage: acErrors.WMAC_HASNT_ROLE }, + ); + }); + + it('should fail: mint less than 1 token to empty greenlisted recipient', async () => { + const baseFixture = await defaultDeploy(); + const { + owner, + accessControl, + regularAccounts, + mTokenPermissionedMinBalance, + mTokenPermissionedMinBalanceRoles, + } = await loadFixture( + mTokenPermissionedMinBalanceFixture.bind(this, baseFixture), + ); + + const to = regularAccounts[0]; + await accessControl.grantRole( + mTokenPermissionedMinBalanceRoles.greenlisted, + to.address, + ); + + await mint( + { tokenContract: mTokenPermissionedMinBalance, owner }, + to, + parseUnits('0.5'), + { revertMessage: 'MTMB: min balance not met' }, + ); + }); + + it('mint when receiver is greenlisted and amount meets min balance', async () => { + const baseFixture = await defaultDeploy(); + const { + owner, + accessControl, + regularAccounts, + mTokenPermissionedMinBalance, + mTokenPermissionedMinBalanceRoles, + } = await loadFixture( + mTokenPermissionedMinBalanceFixture.bind(this, baseFixture), + ); + + const to = regularAccounts[0]; + await accessControl.grantRole( + mTokenPermissionedMinBalanceRoles.greenlisted, + to.address, + ); + + await mint( + { tokenContract: mTokenPermissionedMinBalance, owner }, + to, + parseUnits('1'), + ); + }); + + it('should fail: mint when recipient is blacklisted', async () => { + const baseFixture = await defaultDeploy(); + const { + owner, + accessControl, + regularAccounts, + mTokenPermissionedMinBalance, + mTokenPermissionedMinBalanceRoles, + } = await loadFixture( + mTokenPermissionedMinBalanceFixture.bind(this, baseFixture), + ); + + const to = regularAccounts[0]; + await accessControl.grantRole( + mTokenPermissionedMinBalanceRoles.greenlisted, + to.address, + ); + await blackList( + { + blacklistable: mTokenPermissionedMinBalance, + accessControl, + owner, + }, + to, + ); + + await mint( + { tokenContract: mTokenPermissionedMinBalance, owner }, + to, + parseUnits('1'), + { revertMessage: acErrors.WMAC_HAS_ROLE }, + ); + }); + }); + + describe('burn()', () => { + it('burn without greenlist on holder', async () => { + const baseFixture = await defaultDeploy(); + const { + owner, + accessControl, + regularAccounts, + mTokenPermissionedMinBalance, + mTokenPermissionedMinBalanceRoles, + } = await loadFixture( + mTokenPermissionedMinBalanceFixture.bind(this, baseFixture), + ); + + const holder = regularAccounts[0]; + await accessControl.grantRole( + mTokenPermissionedMinBalanceRoles.greenlisted, + holder.address, + ); + await mint( + { tokenContract: mTokenPermissionedMinBalance, owner }, + holder, + parseUnits('1'), + ); + await accessControl.revokeRole( + mTokenPermissionedMinBalanceRoles.greenlisted, + holder.address, + ); + + await burn( + { tokenContract: mTokenPermissionedMinBalance, owner }, + holder, + parseUnits('1'), + ); + }); + + it('should fail: burn leaving holder below min balance', async () => { + const baseFixture = await defaultDeploy(); + const { + owner, + accessControl, + regularAccounts, + mTokenPermissionedMinBalance, + mTokenPermissionedMinBalanceRoles, + } = await loadFixture( + mTokenPermissionedMinBalanceFixture.bind(this, baseFixture), + ); + + const holder = regularAccounts[0]; + await accessControl.grantRole( + mTokenPermissionedMinBalanceRoles.greenlisted, + holder.address, + ); + await mint( + { tokenContract: mTokenPermissionedMinBalance, owner }, + holder, + parseUnits('3'), + ); + + await burn( + { tokenContract: mTokenPermissionedMinBalance, owner }, + holder, + parseUnits('2.5'), + { revertMessage: 'MTMB: min balance not met' }, + ); + }); + + it('should fail: burn when holder is blacklisted', async () => { + const baseFixture = await defaultDeploy(); + const { + owner, + accessControl, + regularAccounts, + mTokenPermissionedMinBalance, + mTokenPermissionedMinBalanceRoles, + } = await loadFixture( + mTokenPermissionedMinBalanceFixture.bind(this, baseFixture), + ); + + const holder = regularAccounts[0]; + await accessControl.grantRole( + mTokenPermissionedMinBalanceRoles.greenlisted, + holder.address, + ); + await mint( + { tokenContract: mTokenPermissionedMinBalance, owner }, + holder, + parseUnits('1'), + ); + await blackList( + { + blacklistable: mTokenPermissionedMinBalance, + accessControl, + owner, + }, + holder, + ); + + await burn( + { tokenContract: mTokenPermissionedMinBalance, owner }, + holder, + parseUnits('1'), + { revertMessage: acErrors.WMAC_HAS_ROLE }, + ); + }); + + it('burnGoverned when holder is blacklisted', async () => { + const baseFixture = await defaultDeploy(); + const { + owner, + accessControl, + regularAccounts, + mTokenPermissionedMinBalance, + mTokenPermissionedMinBalanceRoles, + } = await loadFixture( + mTokenPermissionedMinBalanceFixture.bind(this, baseFixture), + ); + + const holder = regularAccounts[0]; + const amount = parseUnits('1'); + await accessControl.grantRole( + mTokenPermissionedMinBalanceRoles.greenlisted, + holder.address, + ); + await mint( + { tokenContract: mTokenPermissionedMinBalance, owner }, + holder, + amount, + ); + await blackList( + { + blacklistable: mTokenPermissionedMinBalance, + accessControl, + owner, + }, + holder, + ); + + await expect( + mTokenPermissionedMinBalance + .connect(owner) + .burnGoverned(holder.address, amount), + ).not.reverted; + expect(await mTokenPermissionedMinBalance.balanceOf(holder.address)).eq( + 0, + ); + }); + }); + + describe('transferFrom()', () => { + it('should fail: transferFrom when from is not greenlisted', async () => { + const baseFixture = await defaultDeploy(); + const { + owner, + accessControl, + regularAccounts, + mTokenPermissionedMinBalance, + mTokenPermissionedMinBalanceRoles, + } = await loadFixture( + mTokenPermissionedMinBalanceFixture.bind(this, baseFixture), + ); + + const from = regularAccounts[0]; + const spender = regularAccounts[1]; + const to = regularAccounts[2]; + const { greenlisted } = mTokenPermissionedMinBalanceRoles; + const amount = parseUnits('0.1'); + + await accessControl.grantRole(greenlisted, from.address); + await accessControl.grantRole(greenlisted, to.address); + await mint( + { tokenContract: mTokenPermissionedMinBalance, owner }, + from, + parseUnits('3'), + ); + await mint( + { tokenContract: mTokenPermissionedMinBalance, owner }, + to, + parseUnits('3'), + ); + await mTokenPermissionedMinBalance + .connect(from) + .approve(spender.address, amount); + await accessControl.revokeRole(greenlisted, from.address); + + await expect( + mTokenPermissionedMinBalance + .connect(spender) + .transferFrom(from.address, to.address, amount), + ).revertedWith(acErrors.WMAC_HASNT_ROLE); + }); + + it('should fail: transferFrom dust to empty recipient when both greenlisted', async () => { + const baseFixture = await defaultDeploy(); + const { + owner, + accessControl, + regularAccounts, + mTokenPermissionedMinBalance, + mTokenPermissionedMinBalanceRoles, + } = await loadFixture( + mTokenPermissionedMinBalanceFixture.bind(this, baseFixture), + ); + + const from = regularAccounts[0]; + const spender = regularAccounts[1]; + const to = regularAccounts[2]; + const { greenlisted } = mTokenPermissionedMinBalanceRoles; + const amount = parseUnits('0.1'); + + await accessControl.grantRole(greenlisted, from.address); + await accessControl.grantRole(greenlisted, to.address); + await mint( + { tokenContract: mTokenPermissionedMinBalance, owner }, + from, + parseUnits('3'), + ); + await mTokenPermissionedMinBalance + .connect(from) + .approve(spender.address, amount); + + await expect( + mTokenPermissionedMinBalance + .connect(spender) + .transferFrom(from.address, to.address, amount), + ).revertedWith('MTMB: min balance not met'); + }); + + it('transferFrom when both greenlisted and above min balance', async () => { + const baseFixture = await defaultDeploy(); + const { + owner, + accessControl, + regularAccounts, + mTokenPermissionedMinBalance, + mTokenPermissionedMinBalanceRoles, + } = await loadFixture( + mTokenPermissionedMinBalanceFixture.bind(this, baseFixture), + ); + + const from = regularAccounts[0]; + const spender = regularAccounts[1]; + const to = regularAccounts[2]; + const { greenlisted } = mTokenPermissionedMinBalanceRoles; + const amount = parseUnits('0.1'); + + await accessControl.grantRole(greenlisted, from.address); + await accessControl.grantRole(greenlisted, to.address); + await mint( + { tokenContract: mTokenPermissionedMinBalance, owner }, + from, + parseUnits('3'), + ); + await mint( + { tokenContract: mTokenPermissionedMinBalance, owner }, + to, + parseUnits('3'), + ); + await mTokenPermissionedMinBalance + .connect(from) + .approve(spender.address, amount); + + await expect( + mTokenPermissionedMinBalance + .connect(spender) + .transferFrom(from.address, to.address, amount), + ).not.reverted; + }); + + it('should fail: transferFrom when from is blacklisted', async () => { + const baseFixture = await defaultDeploy(); + const { + owner, + accessControl, + regularAccounts, + mTokenPermissionedMinBalance, + mTokenPermissionedMinBalanceRoles, + } = await loadFixture( + mTokenPermissionedMinBalanceFixture.bind(this, baseFixture), + ); + + const from = regularAccounts[0]; + const spender = regularAccounts[1]; + const to = regularAccounts[2]; + const { greenlisted } = mTokenPermissionedMinBalanceRoles; + const amount = parseUnits('0.1'); + + await accessControl.grantRole(greenlisted, from.address); + await accessControl.grantRole(greenlisted, to.address); + await mint( + { tokenContract: mTokenPermissionedMinBalance, owner }, + from, + parseUnits('3'), + ); + await mint( + { tokenContract: mTokenPermissionedMinBalance, owner }, + to, + parseUnits('3'), + ); + await mTokenPermissionedMinBalance + .connect(from) + .approve(spender.address, amount); + await blackList( + { + blacklistable: mTokenPermissionedMinBalance, + accessControl, + owner, + }, + from, + ); + + await expect( + mTokenPermissionedMinBalance + .connect(spender) + .transferFrom(from.address, to.address, amount), + ).revertedWith(acErrors.WMAC_HAS_ROLE); + }); + }); + }); }); describe('Shared greenlist role (mGLO -> mGLOBAL)', () => { From 8b90792d94449a99987576f8dbe039772c1057c7 Mon Sep 17 00:00:00 2001 From: kostyamospan Date: Tue, 14 Jul 2026 13:59:44 +0300 Subject: [PATCH 02/19] chore: generator scripts min balance --- helpers/mtokens-metadata.ts | 7 +- scripts/deploy/codegen/common/index.ts | 24 +++++-- .../common/templates/mtoken.template.ts | 13 ++-- .../common/templates/token-roles.template.ts | 8 ++- .../codegen/common/ui/deployment-contracts.ts | 7 ++ test/common/token.tests.ts | 64 +++++++++++-------- 6 files changed, 84 insertions(+), 39 deletions(-) diff --git a/helpers/mtokens-metadata.ts b/helpers/mtokens-metadata.ts index 247b2319..00b5e62d 100644 --- a/helpers/mtokens-metadata.ts +++ b/helpers/mtokens-metadata.ts @@ -2,7 +2,12 @@ import { MTokenName } from '../config'; export const mTokensMetadata: Record< MTokenName, - { name: string; symbol: string; isPermissioned?: boolean } + { + name: string; + symbol: string; + isPermissioned?: boolean; + isMinBalance?: boolean; + } > = { mTBILL: { name: 'Midas US Treasury Bill Token', diff --git a/scripts/deploy/codegen/common/index.ts b/scripts/deploy/codegen/common/index.ts index 2160829f..0e5ba7a5 100644 --- a/scripts/deploy/codegen/common/index.ts +++ b/scripts/deploy/codegen/common/index.ts @@ -46,6 +46,7 @@ import { getGenerationModeFromUser, getGreenlistRoleSourceFromUser, getShouldUseTokenLevelGreenListFromUser, + getShouldUseTokenMinBalanceFromUser, getShouldUseTokenPermissionedFromUser, getTokenContractNameFromUser, } from './ui/deployment-contracts'; @@ -110,6 +111,7 @@ export const updateConfigFiles = ( symbol, mToken, isPermissioned, + isMinBalance, useTokenLevelGreenList, greenlistRoleSource, }: { @@ -119,6 +121,7 @@ export const updateConfigFiles = ( symbol: string; mToken: string; isPermissioned?: true; + isMinBalance?: true; useTokenLevelGreenList?: boolean; greenlistRoleSource?: string; }, @@ -233,17 +236,20 @@ export const updateConfigFiles = ( ); if (!objLiteral.getProperty(mToken)) { + const flags = [ + isPermissioned ? 'isPermissioned: true' : undefined, + isMinBalance ? 'isMinBalance: true' : undefined, + ] + .filter(Boolean) + .map((flag) => `,\n ${flag}`) + .join(''); + objLiteral.addPropertyAssignment({ name: mToken, initializer: (writer) => writer.write(`{ name: '${name}', - symbol: '${symbol}'${ - isPermissioned - ? `, - isPermissioned: true` - : '' - } + symbol: '${symbol}'${flags} }`), }); } @@ -775,6 +781,7 @@ export const generateContracts = async (hre: HardhatRuntimeEnvironment) => { rolesPrefix: string; }; let isPermissionedFromMetadata = false; + let isMinBalanceFromMetadata = false; if (mode === 'create') { config = await getConfigFromUser(mToken); @@ -800,12 +807,14 @@ export const generateContracts = async (hre: HardhatRuntimeEnvironment) => { rolesPrefix, }; isPermissionedFromMetadata = !!metadata.isPermissioned; + isMinBalanceFromMetadata = !!metadata.isMinBalance; } const contractsToGenerate = await getContractsToGenerateFromUser(); let shouldUseTokenLevelGreenList = false; let shouldUseTokenPermissioned = isPermissionedFromMetadata; + let shouldUseTokenMinBalance = isMinBalanceFromMetadata; let greenlistRoleSource: string | undefined; if ( @@ -835,6 +844,7 @@ export const generateContracts = async (hre: HardhatRuntimeEnvironment) => { if (contractsToGenerate.includes('token') && mode === 'create') { shouldUseTokenPermissioned = await getShouldUseTokenPermissionedFromUser(); + shouldUseTokenMinBalance = await getShouldUseTokenMinBalanceFromUser(); } await tasks([ @@ -848,6 +858,7 @@ export const generateContracts = async (hre: HardhatRuntimeEnvironment) => { symbol: config.tokenSymbol, mToken: config.tokenContractName, isPermissioned: shouldUseTokenPermissioned ? true : undefined, + isMinBalance: shouldUseTokenMinBalance ? true : undefined, useTokenLevelGreenList: shouldUseTokenLevelGreenList, greenlistRoleSource, }); @@ -883,6 +894,7 @@ export const generateContracts = async (hre: HardhatRuntimeEnvironment) => { generator(mToken, { vaultUseTokenLevelGreenList: shouldUseTokenLevelGreenList, isPermissionedMToken: shouldUseTokenPermissioned, + isMinBalanceMToken: shouldUseTokenMinBalance, }), ), ); diff --git a/scripts/deploy/codegen/common/templates/mtoken.template.ts b/scripts/deploy/codegen/common/templates/mtoken.template.ts index c27ce66b..aea3c22c 100644 --- a/scripts/deploy/codegen/common/templates/mtoken.template.ts +++ b/scripts/deploy/codegen/common/templates/mtoken.template.ts @@ -5,7 +5,8 @@ export const getTokenContractFromTemplate = async ( mToken: MTokenName, optionalParams?: Record, ) => { - const { isPermissionedMToken = false } = optionalParams || {}; + const { isPermissionedMToken = false, isMinBalanceMToken = false } = + optionalParams || {}; const { getTokenContractNames } = await importWithoutCache( require.resolve('../../../../../helpers/contracts'), ); @@ -23,13 +24,17 @@ export const getTokenContractFromTemplate = async ( const contractNames = getTokenContractNames(mToken); const roles = getRolesNamesForToken(mToken); + const mTokenBase = `mToken${isPermissionedMToken ? 'Permissioned' : ''}${ + isMinBalanceMToken ? 'MinBalance' : '' + }`; + return { name: contractNames.token, content: ` // SPDX-License-Identifier: MIT pragma solidity 0.8.9; - import "../../mToken${isPermissionedMToken ? 'Permissioned' : ''}.sol"; + import "../../${mTokenBase}.sol"; ${isPermissionedMToken ? `import "./${contractNames.roles}.sol";` : ''} /** @@ -37,8 +42,8 @@ export const getTokenContractFromTemplate = async ( * @author RedDuck Software */ //solhint-disable contract-name-camelcase - contract ${contractNames.token} is mToken${ - isPermissionedMToken ? `Permissioned, ${contractNames.roles}` : '' + contract ${contractNames.token} is ${mTokenBase}${ + isPermissionedMToken ? `, ${contractNames.roles}` : '' } { /** * @notice actor that can mint ${contractNames.token} diff --git a/scripts/deploy/codegen/common/templates/token-roles.template.ts b/scripts/deploy/codegen/common/templates/token-roles.template.ts index 1120bb96..63f47b82 100644 --- a/scripts/deploy/codegen/common/templates/token-roles.template.ts +++ b/scripts/deploy/codegen/common/templates/token-roles.template.ts @@ -5,7 +5,8 @@ export const getTokenRolesContractFromTemplate = async ( mToken: MTokenName, optionalParams?: Record, ) => { - const { vaultUseTokenLevelGreenList = false } = optionalParams || {}; + const { vaultUseTokenLevelGreenList = false, isPermissionedMToken = false } = + optionalParams || {}; const { getTokenContractNames } = await importWithoutCache( require.resolve('../../../../../helpers/contracts'), @@ -18,6 +19,9 @@ export const getTokenRolesContractFromTemplate = async ( const contractNames = getTokenContractNames(mToken); const roles = getRolesNamesForToken(mToken); + const includeGreenlistedRole = + vaultUseTokenLevelGreenList || isPermissionedMToken; + return { name: contractNames.roles, content: ` @@ -53,7 +57,7 @@ export const getTokenRolesContractFromTemplate = async ( keccak256("${roles.customFeedAdmin}"); ${ - vaultUseTokenLevelGreenList + includeGreenlistedRole ? ` /** * @notice greenlist role for ${contractNames.token} diff --git a/scripts/deploy/codegen/common/ui/deployment-contracts.ts b/scripts/deploy/codegen/common/ui/deployment-contracts.ts index d8b97ea5..4d378d14 100644 --- a/scripts/deploy/codegen/common/ui/deployment-contracts.ts +++ b/scripts/deploy/codegen/common/ui/deployment-contracts.ts @@ -174,6 +174,13 @@ export const getShouldUseTokenPermissionedFromUser = async () => { }).then(requireNotCancelled); }; +export const getShouldUseTokenMinBalanceFromUser = async () => { + return confirm({ + message: 'Should use min-balance mToken variant?', + initialValue: false, + }).then(requireNotCancelled); +}; + /** * Optionally reuse an existing product's greenlist role instead of minting a * token-specific one (e.g. mGLO reuses mGLOBAL's M_GLOBAL_GREENLISTED_ROLE). diff --git a/test/common/token.tests.ts b/test/common/token.tests.ts index 5c9df789..3902bf14 100644 --- a/test/common/token.tests.ts +++ b/test/common/token.tests.ts @@ -109,6 +109,9 @@ export const tokenContractsTests = (token: MTokenName) => { return factory.attach(proxy.address) as TContract; }; + const isPermissioned = !!mTokensMetadata[token]?.isPermissioned; + const unitAmount = mTokensMetadata[token]?.isMinBalance ? parseUnits('1') : 1; + const deployMTokenWithFixture = async () => { const fixture = await loadFixture(defaultDeploy); @@ -118,7 +121,7 @@ export const tokenContractsTests = (token: MTokenName) => { fixture.accessControl.address, )) as MTBILL; - if (mTokensMetadata[token]?.isPermissioned) { + if (isPermissioned) { const greenlistedRole = tokenRoles.greenlisted; for (const account of fixture.regularAccounts) { await fixture.accessControl @@ -533,7 +536,7 @@ export const tokenContractsTests = (token: MTokenName) => { { blacklistable: tokenContract, accessControl, owner }, blacklisted, ); - await mint({ tokenContract, owner }, blacklisted, 1, { + await mint({ tokenContract, owner }, blacklisted, unitAmount, { revertMessage: acErrors.WMAC_HAS_ROLE, }); }); @@ -545,14 +548,14 @@ export const tokenContractsTests = (token: MTokenName) => { const blacklisted = regularAccounts[0]; const to = regularAccounts[1]; - await mint({ tokenContract, owner }, blacklisted, 1); + await mint({ tokenContract, owner }, blacklisted, unitAmount); await blackList( { blacklistable: tokenContract, accessControl, owner }, blacklisted, ); await expect( - tokenContract.connect(blacklisted).transfer(to.address, 1), + tokenContract.connect(blacklisted).transfer(to.address, unitAmount), ).revertedWith(acErrors.WMAC_HAS_ROLE); }); @@ -563,14 +566,14 @@ export const tokenContractsTests = (token: MTokenName) => { const blacklisted = regularAccounts[0]; const from = regularAccounts[1]; - await mint({ tokenContract, owner }, from, 1); + await mint({ tokenContract, owner }, from, unitAmount); await blackList( { blacklistable: tokenContract, accessControl, owner }, blacklisted, ); await expect( - tokenContract.connect(from).transfer(blacklisted.address, 1), + tokenContract.connect(from).transfer(blacklisted.address, unitAmount), ).revertedWith(acErrors.WMAC_HAS_ROLE); }); @@ -581,18 +584,20 @@ export const tokenContractsTests = (token: MTokenName) => { const blacklisted = regularAccounts[0]; const to = regularAccounts[1]; - await mint({ tokenContract, owner }, blacklisted, 1); + await mint({ tokenContract, owner }, blacklisted, unitAmount); await blackList( { blacklistable: tokenContract, accessControl, owner }, blacklisted, ); - await tokenContract.connect(blacklisted).approve(to.address, 1); + await tokenContract + .connect(blacklisted) + .approve(to.address, unitAmount); await expect( tokenContract .connect(to) - .transferFrom(blacklisted.address, to.address, 1), + .transferFrom(blacklisted.address, to.address, unitAmount), ).revertedWith(acErrors.WMAC_HAS_ROLE); }); @@ -604,18 +609,18 @@ export const tokenContractsTests = (token: MTokenName) => { const from = regularAccounts[1]; const caller = regularAccounts[2]; - await mint({ tokenContract, owner }, from, 1); + await mint({ tokenContract, owner }, from, unitAmount); await blackList( { blacklistable: tokenContract, accessControl, owner }, blacklisted, ); - await tokenContract.connect(from).approve(caller.address, 1); + await tokenContract.connect(from).approve(caller.address, unitAmount); await expect( tokenContract .connect(caller) - .transferFrom(from.address, blacklisted.address, 1), + .transferFrom(from.address, blacklisted.address, unitAmount), ).revertedWith(acErrors.WMAC_HAS_ROLE); }); @@ -625,12 +630,12 @@ export const tokenContractsTests = (token: MTokenName) => { const blacklisted = regularAccounts[0]; - await mint({ tokenContract, owner }, blacklisted, 1); + await mint({ tokenContract, owner }, blacklisted, unitAmount); await blackList( { blacklistable: tokenContract, accessControl, owner }, blacklisted, ); - await burn({ tokenContract, owner }, blacklisted, 1, { + await burn({ tokenContract, owner }, blacklisted, unitAmount, { revertMessage: acErrors.WMAC_HAS_ROLE, }); }); @@ -641,7 +646,7 @@ export const tokenContractsTests = (token: MTokenName) => { const blacklisted = regularAccounts[0]; - await mint({ tokenContract, owner }, blacklisted, 1); + await mint({ tokenContract, owner }, blacklisted, unitAmount); await blackList( { blacklistable: tokenContract, accessControl, owner }, blacklisted, @@ -651,10 +656,12 @@ export const tokenContractsTests = (token: MTokenName) => { blacklisted.address, ); await expect( - tokenContract.connect(owner).burnGoverned(blacklisted.address, 1), + tokenContract + .connect(owner) + .burnGoverned(blacklisted.address, unitAmount), ).to.not.reverted; const balanceAfter = await tokenContract.balanceOf(blacklisted.address); - expect(balanceBefore.sub(balanceAfter)).eq(1); + expect(balanceBefore.sub(balanceAfter)).eq(unitAmount); }); it('should fail: burnGoverned(...) when caller lacks burner role', async () => { @@ -664,10 +671,12 @@ export const tokenContractsTests = (token: MTokenName) => { const unauthorized = regularAccounts[0]; const target = regularAccounts[1]; - await mint({ tokenContract, owner }, target, 1); + await mint({ tokenContract, owner }, target, unitAmount); await expect( - tokenContract.connect(unauthorized).burnGoverned(target.address, 1), + tokenContract + .connect(unauthorized) + .burnGoverned(target.address, unitAmount), ).revertedWith(acErrors.WMAC_HASNT_ROLE); }); @@ -679,18 +688,20 @@ export const tokenContractsTests = (token: MTokenName) => { const from = regularAccounts[1]; const to = regularAccounts[2]; - await mint({ tokenContract, owner }, from, 1); + await mint({ tokenContract, owner }, from, unitAmount); await blackList( { blacklistable: tokenContract, accessControl, owner }, blacklisted, ); - await tokenContract.connect(from).approve(blacklisted.address, 1); + await tokenContract + .connect(from) + .approve(blacklisted.address, unitAmount); await expect( tokenContract .connect(blacklisted) - .transferFrom(from.address, to.address, 1), + .transferFrom(from.address, to.address, unitAmount), ).not.reverted; }); @@ -701,14 +712,14 @@ export const tokenContractsTests = (token: MTokenName) => { const blacklisted = regularAccounts[0]; const to = regularAccounts[2]; - await mint({ tokenContract, owner }, blacklisted, 1); + await mint({ tokenContract, owner }, blacklisted, unitAmount); await blackList( { blacklistable: tokenContract, accessControl, owner }, blacklisted, ); await expect( - tokenContract.connect(blacklisted).transfer(to.address, 1), + tokenContract.connect(blacklisted).transfer(to.address, unitAmount), ).revertedWith(acErrors.WMAC_HAS_ROLE); await unBlackList( @@ -716,8 +727,9 @@ export const tokenContractsTests = (token: MTokenName) => { blacklisted, ); - await expect(tokenContract.connect(blacklisted).transfer(to.address, 1)) - .not.reverted; + await expect( + tokenContract.connect(blacklisted).transfer(to.address, unitAmount), + ).not.reverted; }); }); }); From 4257ca4fe82e7de7950a9195f8455f8fa96f1a9f Mon Sep 17 00:00:00 2001 From: kostyamospan Date: Tue, 14 Jul 2026 17:28:55 +0300 Subject: [PATCH 03/19] fix: skills, refactor min balance contract --- .claude/skills/feynman-auditor/SKILL.md | 972 +++++++++++++++ .claude/skills/nemesis-auditor/SKILL.md | 1047 +++++++++++++++++ .../state-inconsistency-auditor/SKILL.md | 517 ++++++++ contracts/mTokenMinBalance.sol | 24 +- test/unit/mtoken.test.ts | 40 +- 5 files changed, 2568 insertions(+), 32 deletions(-) create mode 100644 .claude/skills/feynman-auditor/SKILL.md create mode 100644 .claude/skills/nemesis-auditor/SKILL.md create mode 100644 .claude/skills/state-inconsistency-auditor/SKILL.md diff --git a/.claude/skills/feynman-auditor/SKILL.md b/.claude/skills/feynman-auditor/SKILL.md new file mode 100644 index 00000000..a8c8aee2 --- /dev/null +++ b/.claude/skills/feynman-auditor/SKILL.md @@ -0,0 +1,972 @@ +--- +name: feynman-auditor +description: Deep business logic bug finder using the Feynman technique. Language-agnostic — works on Solidity, Move, Rust, Go, C++, or any codebase. Questions every line, every ordering choice, every guard presence/absence, and every implicit assumption to surface logic bugs that pattern-matching misses. Triggers on /feynman, feynman audit, or deep logic review. +--- + +# Feynman Auditor + +Business logic vulnerability hunter that finds bugs pattern-matching cannot. Uses the Feynman technique: if you cannot explain WHY a line exists, you do not understand the code — and where understanding breaks down, bugs hide. + +**Language-agnostic by design.** Logic bugs live in the reasoning, not the syntax. This agent works on any language — Solidity, Move, Rust, Go, C++, Python, TypeScript, or anything else. The questions are universal; only the examples change. + +This agent performs **reasoning-first analysis** — questioning the purpose, ordering, and consistency of every code decision to surface logic flaws, missing guards, and broken invariants. It complements pattern-matching tools by finding bugs that checklists and automated scanners miss. + +## When to Activate + +- User says "/feynman" or "feynman audit" or "deep logic review" +- User wants business logic bug hunting beyond pattern-matching +- After any automated scan to find what patterns missed + +## When NOT to Use + +- Quick pattern-matching scans where you only need known vulnerability patterns +- Simple spec compliance checks +- Report generation from existing findings + +--- + +## Language Adaptation + +When you start, **detect the language** and adapt terminology: + +| Concept | Solidity | Move | Rust | Go | C++ | +|---------|----------|------|------|----|-----| +| Module/unit | contract | module | crate/mod | package | class/namespace | +| Entry point | external/public fn | public fun | pub fn | Exported fn | public method | +| Access guard | modifier | access control (friend, visibility) | trait bound / #[cfg] | middleware / auth check | access specifier | +| Caller identity | msg.sender | &signer | caller param / Context | ctx / request.User | this / session | +| Error/abort | revert / require | abort / assert! | panic! / Result::Err | error / panic | throw / exception | +| State storage | storage variables | global storage / resources | struct fields / state | struct fields / DB | member variables | +| Checked math | SafeMath / checked | built-in overflow abort | checked_add / saturating | math/big / overflow check | safe int libs | +| Test framework | Foundry / Hardhat | Move Prover / aptos move test | cargo test | go test | gtest / catch2 | +| Value/assets | ETH, ERC-20, NFTs | APT, Coin\, tokens | SOL, SPL tokens, funds | any value type | any value type | + +**IMPORTANT:** Do NOT force Solidity terminology onto non-Solidity code. Use the language's native concepts. The questions stay the same — the vocabulary adapts. + +--- + +## Core Philosophy + +``` +"What I cannot create, I do not understand." — Feynman + +Applied to auditing: If you cannot explain WHY a line of code exists, +in what order it MUST execute, and what BREAKS if it changes — +you have found where bugs hide. +``` + +Pattern matchers find KNOWN bug classes. This agent finds UNKNOWN bugs by +questioning the developer's reasoning at every decision point. + +--- + +## Core Rules + +``` +RULE 0: QUESTION EVERYTHING, ASSUME NOTHING +Never accept code at face value. Every line exists because a developer +made a decision. Your job is to question that decision. + +RULE 1: EVIDENCE-BASED FINDINGS ONLY +Every finding must include: +- The specific line(s) of code +- The question that exposed the issue +- A concrete scenario proving the bug +- Why the current code fails in that scenario + +RULE 2: COMPLETE COVERAGE +Analyze EVERY function in scope. Do not skip "simple" functions. +Business logic bugs hide in the code everyone assumes is correct. + +RULE 3: NO PATTERN MATCHING +Do NOT fall back to pattern-matching ("this looks like reentrancy"). +Reason from first principles about what this specific code does. + +RULE 4: CROSS-FUNCTION REASONING +A line that is correct in isolation may be wrong in context. +Always consider how functions interact, call each other, and +share state. +``` + +--- + +## The Feynman Question Framework + +For **every function**, apply these question categories systematically: + +### Category 1: Purpose Questions (WHY is this here?) + +For each line or block of code, ask: + +``` +Q1.1: Why does this line exist? What invariant does it protect? + → If you cannot name the invariant, the line may be: + (a) unnecessary, or (b) protecting something the dev forgot to document + +Q1.2: What happens if I DELETE this line entirely? + → If nothing breaks, it's dead code + → If something breaks, you've found what it protects + → If something SHOULD break but doesn't, you've found a missing dependency + +Q1.3: What SPECIFIC attack or edge case motivated this check? + → If the dev added a guard like `assert(amount > 0)`, what goes + wrong at amount=0? Trace the zero/empty/max value through + the entire function. + → Language examples: + Solidity: require(amount > 0) + Move: assert!(amount > 0, ERROR_ZERO) + Rust: ensure!(amount > 0, Error::Zero) + Go: if amount <= 0 { return ErrZero } + +Q1.4: Is this check SUFFICIENT for what it's trying to prevent? + → A check for `amount > 0` doesn't prevent dust/minimum-value griefing + → A check for `caller == owner` doesn't prevent owner key compromise + → A bounds check doesn't prevent off-by-one within the bounds +``` + +### Category 2: Ordering Questions (WHAT IF I MOVE THIS?) + +For each state-changing operation, ask: + +``` +Q2.1: What if this line executes BEFORE the line above it? + → Would a different ordering allow state manipulation? + → Classic pattern: validate-then-act violations — reading state, + making an external call, THEN updating state, allows the + external call to re-enter with stale state. + +Q2.2: What if this line executes AFTER the line below it? + → Does delaying this operation create a window of inconsistent state? + → Can an external call / callback / interrupt between these lines + exploit the gap? + +Q2.3: What is the FIRST line that changes state? What is the LAST line + that reads state? Is there a gap between them? + → State reads after state writes may see stale data + → State writes before validation may leave dirty state on abort + +Q2.4: If this function ABORTS HALFWAY through, what state is left behind? + → Are there side effects that persist despite the abort? + (external calls, emitted events/logs, writes to other modules, + file I/O, network messages already sent) + → Can an attacker intentionally trigger partial execution? + +Q2.5: Can the ORDER in which users call this function matter? + → Front-running / race conditions: does calling first give advantage? + → Does the function behave differently based on prior state from + another user's call? + → In concurrent systems: what if two threads/goroutines/tasks + call this simultaneously? +``` + +### Category 3: Consistency Questions (WHY does A have it but B doesn't?) + +Compare functions that SHOULD be symmetric: + +``` +Q3.1: If functionA has an access guard and functionB doesn't, WHY? + → Is functionB intentionally unrestricted, or did the dev forget? + → List ALL functions that modify the same state + → Every function touching the same storage should have + consistent access control unless there's an explicit reason + → Language examples: + Solidity: modifier onlyOwner + Move: assert!(signer::address_of(account) == @admin) + Rust: #[access_control(ctx.accounts.authority)] + Go: if !isAuthorized(ctx) { return ErrUnauthorized } + +Q3.2: If deposit() checks X, does withdraw() also check X? + → Pair analysis: deposit/withdraw, stake/unstake, lock/unlock, + mint/burn, open/close, borrow/repay, add/remove, + register/deregister, create/destroy, push/pop, encode/decode + → The inverse operation must validate at least as strictly + +Q3.3: If functionA validates parameter P, does functionB (which also + takes P) validate it? + → Same parameter, different validation = one of them is wrong + +Q3.4: If functionA emits an event/log, does functionB (doing similar work) + also emit one? + → Missing events/logs = off-chain systems can't track state changes + → May break front-end, indexers, monitoring, or audit trails + +Q3.5: If functionA uses overflow-safe arithmetic, does functionB? + → Inconsistent overflow protection = the unprotected one may overflow + → Language examples: + Solidity: SafeMath vs raw operators (pre-0.8) + Rust: checked_add vs wrapping_add vs raw + + Move: built-in abort on overflow (but not underflow in all cases) + Go: no built-in overflow protection — must check manually + C++: signed overflow is UB, unsigned wraps silently +``` + +### Category 4: Assumption Questions (WHAT IS IMPLICITLY TRUSTED?) + +Expose hidden assumptions: + +``` +Q4.1: What does this function assume about THE CALLER? + → Who can call this? Is that enforced or just assumed? + → Could the caller be a different type than expected? + Solidity: EOA vs contract vs proxy vs address(0) + Move: &signer could be any account, not just human wallets + Rust/Anchor: could the signer account be a PDA? + Go: could the HTTP caller be unauthenticated / spoofed? + C++: could this be called from a different thread? + → What if the caller IS the system itself? (self-calls, recursion) + +Q4.2: What does this function assume about EXTERNAL DATA it receives? + → For tokens/coins: standard behavior? Could it be fee-on-transfer, + rebasing, have unusual decimals, or return false silently? + → For API responses: always well-formed? What if malformed, empty, + or adversarially crafted? + → For user input: sanitized? What about injection, encoding tricks, + or type confusion? + → For deserialized data: trusted format? What if the schema changed + or the data was tampered with? + +Q4.3: What does this function assume about the current state? + → "This will never be called when paused/locked" — but IS it enforced? + → "Balance will always be sufficient" — but who guarantees that? + → "This map/vector will never be empty" — but what if it is? + → "This was already initialized" — but what if it wasn't? + +Q4.4: What does this function assume about TIME or ORDERING? + → Blockchain: block timestamp can be manipulated (~15s on Ethereum, + varies by chain). Move: epoch-based timing. Solana: slot-based. + → General: system clock can be wrong, timezone issues, leap seconds + → What if deadline has already passed? What if time = 0? + → What if events arrive out of order? (network, async, concurrent) + +Q4.5: What does this function assume about PRICES, RATES, or EXTERNAL VALUES? + → Can the value be manipulated within the same transaction/call? + → Is the data source fresh? What if the oracle/API is stale or dead? + → What if the value is 0? What if it's MAX_VALUE for the type? + → What if precision differs between source and consumer? + +Q4.6: What does this function assume about INPUT AMOUNTS or SIZES? + → What if amount/size = 0? What if it's the maximum representable value? + → What if amount = 1 (dust / minimum unit)? + → What if amount exceeds what's available? + → What if a collection is empty? What if it has millions of entries? +``` + +### Category 5: Boundary & Edge Case Questions (WHAT BREAKS AT THE EDGES?) + +``` +Q5.1: What happens on the FIRST call to this function? (Empty state) + → First depositor, first user, first initialization + → Division by zero when total = 0? + → Share/ratio inflation when pool/collection is empty? + → Uninitialized state treated as valid? + +Q5.2: What happens on the LAST call? (Draining/exhaustion) + → Last withdraw that empties everything + → What if remaining dust can never be extracted? + → Does rounding trap value permanently? + → What if the last element removal breaks an invariant? + +Q5.3: What if this function is called TWICE in rapid succession? + → Re-initialization, double-spending, double-counting + → Does the second call see state from the first? + → In concurrent systems: race condition between the two calls? + → Blockchain: two calls in the same block/transaction + +Q5.4: What if two DIFFERENT functions are called in the same context? + → Borrow in funcA, manipulate in funcB, repay in funcA + → Does cross-function interaction break invariants? + → What about callback patterns where control flow is non-linear? + +Q5.5: What if this function is called with THE SYSTEM ITSELF as a parameter? + → Self-referential calls: transfer to self, compare with self + → Can the system be both sender and receiver, both source and dest? + → What about circular references or recursive structures? +``` + +### Category 6: Return Value & Error Path Questions + +``` +Q6.1: What does this function return? Who consumes the return value? + → If the caller ignores the return value, what's lost? + → If the return value is wrong, what downstream logic breaks? + → Language-specific: Does the language even FORCE you to check? + Rust: Result must be used. Go: error can be silently ignored with _. + Solidity: low-level call returns bool that's often unchecked. + C++: [[nodiscard]] is opt-in. Move: values must be consumed. + +Q6.2: What happens on the ERROR/ABORT path? + → Are there side effects before the error? + → Does the error message leak sensitive information? + → Can an attacker cause targeted errors (griefing / DoS)? + → In languages with exceptions: is cleanup code (finally/defer/ + Drop) correct? Are resources leaked on the error path? + +Q6.3: What if an EXTERNAL CALL in this function fails silently? + → Does the language/runtime guarantee failure propagation? + → Is the error checked, or can it be swallowed? + → Language examples: + Solidity: low-level call returns (bool, bytes) — often unchecked + Go: err is a normal return value — easy to ignore with _ + Rust: .unwrap() can panic; ? propagates but hides the error + C++: exception might be caught too broadly + Move: abort is always propagated (safer by design) + +Q6.4: Is there a code path where NO return and NO error happens? + → Functions falling through without explicit return + → Default/zero values used when they shouldn't be + → Missing match/switch arms or else branches + → Language-specific: + Rust: compiler catches this. Go/C++: does not always. + Solidity: functions can fall through returning zero values. +``` + +### Category 7: External Call Reordering & Multi-Transaction State Analysis + +This category catches bugs that live in the TIMING and SEQUENCING of operations — both within a single transaction and across multiple transactions over time. + +#### Part A: External Call Reordering (within a single transaction) + +``` +Q7.1: If the function performs an external call BEFORE a state update, + what happens if I SWAP them — state update first, external call second? + → If the swap causes a revert: the ORIGINAL ordering may be exploitable + (the external call might re-enter or manipulate state before it's updated) + → If the swap works cleanly: the original ordering is likely safe, + OR the swap reveals the intended safe ordering was never enforced + → KEY: Try both directions. The one that reverts tells you which + ordering the code DEPENDS on. The one that doesn't revert tells you + which ordering an attacker can exploit. + +Q7.2: If the function performs an external call AFTER a state update, + what happens if I SWAP them — external call first, state update second? + → If the swap causes a revert: the current code is CORRECTLY ordered + (state must be updated before the external call can proceed) + → If the swap works cleanly: the ordering doesn't matter, OR + the external call could be exploited before state is finalized + → FINDING: If moving the external call BEFORE the state update + allows an attacker to observe/act on stale state, this is a bug. + +Q7.3: For EVERY external call in the function, ask: + "What can the CALLEE do with the current state at THIS exact moment?" + → At the point of the external call, what state is committed vs pending? + → Can the callee re-enter this contract/module and see inconsistent state? + → Can the callee call a DIFFERENT function that reads the not-yet-updated state? + → This applies beyond reentrancy: callbacks, hooks, oracle calls, + cross-contract reads — ANY outbound call is an opportunity for + the callee to act on intermediate state. + → Language examples: + Solidity: .call(), .transfer(), IERC20.safeTransfer(), callback hooks + Move: cross-module function calls during resource manipulation + Rust/Anchor: CPI (Cross-Program Invocation) in Solana + Go: outbound HTTP/RPC calls, goroutine spawning mid-operation + C++: virtual method calls, callback invocations, signal handlers + +Q7.4: What is the MINIMAL set of state that MUST be updated before each + external call to prevent exploitation? + → List every state variable the external callee could read or depend on + → If ANY of those variables are updated AFTER the external call, + flag it as a potential ordering vulnerability + → The fix is often: move the state update above the external call + (checks-effects-interactions pattern generalized to any language) +``` + +#### Part B: Multi-Transaction State Corruption (across time) + +``` +Q7.5: If a user calls this function with value X, and then calls it AGAIN + later with value Y — does the second call behave correctly given the + state changes from the first call? + → The first call changes state. Does the second call's logic ACCOUNT + for that changed state, or does it assume fresh/initial state? + → Example: deposit(100), then deposit(50). Does the second deposit + correctly handle shares/accounting when totalSupply is no longer 0? + → Example: borrow(1000), then borrow(500). Does the second borrow + check against the UPDATED debt, or does it re-read stale collateral? + +Q7.6: After transaction T1 changes state, does transaction T2 (same function, + different parameters) REVERT when it shouldn't, or SUCCEED when it shouldn't? + → Unexpected revert: T1's state change made a condition impossible for T2 + (e.g., T1 drains a pool below a minimum, T2 can't withdraw dust) + → Unexpected success: T1's state change should have blocked T2 but didn't + (e.g., T1 uses all collateral, T2 still borrows against phantom collateral) + → DEEP CHECK: Don't just test T2 immediately after T1. Test T2 after: + - Many T1s have accumulated (state drift over time) + - T1 with extreme values (max, min, dust) + - T1 from a different user (cross-user state pollution) + - T1 that was partially reverted (try-catch leaving dirty state) + +Q7.7: Does the accumulated state from MULTIPLE calls create a condition that + a SINGLE call can never reach? + → Rounding errors that compound: each call loses 1 wei of precision, + after 1000 calls the accounting is off by 1000 wei + → Monotonically growing state: counters, nonces, array lengths that + grow but never shrink — do they hit a ceiling or overflow? + → Reward/rate staleness: if updateReward() is called infrequently, + do accumulated rewards become incorrect? + → State fragmentation: many small operations leaving dust/remnants + that block future operations (e.g., can't close position because + of 1 wei of remaining debt) + + WORKED EXAMPLE — Partial Swap Fee Distribution Bug: + ───────────────────────────────────────────────────── + Consider an AMM pool with a swap() function that: + 1. Calculates amountOut based on reserves + 2. Updates accumulatedFees (used for LP fee distribution) + 3. Updates reserves + + Scenario — partial swap with wrong fee accounting: + State: reserveA=10000, reserveB=10000, accFees=0, totalLP=100 + + TX1: Alice swaps 1000 tokenA → tokenB (partial fill, 0.3% fee) + - fee = 3 tokenA → accFees updated to 3 + - BUT: the fee is added to accFees BEFORE reserves update + - reserveA becomes 11000, reserveB becomes ~9091 + - feePerLP = 3/100 = 0.03 per LP token ✓ (looks correct) + + TX2: Bob swaps 500 tokenA → tokenB (different amount, same function) + - fee = 1.5 tokenA → accFees updated to 4.5 + - BUT: feePerLP is now calculated as 4.5/100 = 0.045 + - The problem: the fee rate was computed using STALE reserve + ratios from before TX1 changed the pool composition + - After TX1, the pool is imbalanced — 1 tokenA is worth less + than before. But the fee accounting still values TX2's fee + at the OLD rate. + + TX3: Charlie claims LP fees + - Gets paid based on accFees = 4.5 at OLD token valuation + - But the pool's ACTUAL composition has shifted — the fees + are denominated in a token that's now worth less in the pool + - Result: fee distribution is skewed. Early LPs get overpaid, + late LPs get underpaid. Over hundreds of swaps, the + accounting diverges significantly from reality. + + The root cause: accFees is updated per-swap without rebasing + against the current reserve ratio. Each swap changes what "1 unit + of fee" is worth, but the accumulator treats all units as equal. + + → GENERALIZE THIS PATTERN to any system where: + - A global accumulator (fees, rewards, interest) is updated per-tx + - The VALUE of what's being accumulated changes between txs + - The accumulator doesn't rebase/normalize against current state + - Examples: LP fee distributors, staking reward accumulators, + interest rate models, rebasing token accounting, yield vaults + with variable share prices + + → CHECK SPECIFICALLY: + - Is the fee/reward denominated in a token whose relative value + changes with each operation? + - Does the accumulator use a snapshot of rates/prices that goes + stale after the state-changing operation? + - Are fees calculated BEFORE or AFTER the reserves/balances update? + (before = stale rate, after = correct rate, but BOTH must be checked) + - When multiple fee tiers or partial fills exist, does each partial + chunk use the UPDATED state from the previous chunk, or do they + all use the ORIGINAL state? (batch vs iterative accounting) + - After N swaps with varying sizes, does SUM(individual fees) equal + the fee you'd compute on the AGGREGATE swap? If not, the + accumulator is path-dependent and exploitable. + +Q7.8: Can an attacker craft a SEQUENCE of transactions to reach a state + that no single "normal" transaction path would produce? + → Deposit-borrow-withdraw-liquidate sequences that leave bad debt + → Stake-unstake-restake sequences that compound rounding errors + → Create-transfer-destroy sequences that orphan child state + → The attacker's advantage: they CHOOSE the order, amounts, and + timing. Test adversarial sequences, not just happy-path sequences. + → For each function, ask: "After calling THIS, what state is the + system in? What functions become newly available or newly dangerous + to call from that state?" +``` + +--- + +## Execution Process + +### Phase 0: Attacker Mindset (BEFORE reading a single line of code) + +``` +The bugs are in the answers to these 4 questions. +Ask them FIRST — they tell you WHERE to spend your time. + +Q0.1: What's the WORST thing an attacker can do here? + → Think attacker, NOT user. Users follow happy paths. + Attackers find the one path the dev never imagined. + → List the top 3-5 catastrophic outcomes: + drain all funds, brick the system, steal admin privileges, + manipulate prices/data, grief other users permanently, + corrupt state irreversibly, exfiltrate sensitive data. + → These become your ATTACK GOALS for the entire audit. + Every function you read, ask: "Does this help an + attacker achieve any of these goals?" + +Q0.2: What parts of the project are NOVEL? + → First-time code = first-time bugs. Period. + → Identify code that is NOT a fork/copy of battle-tested + libraries or frameworks. + Solidity: OpenZeppelin, Uniswap, Aave forks + Move: Aptos Framework, Sui Framework stdlib + Rust: well-known crates (tokio, serde, anchor) + Go: standard library, well-maintained packages + C++: STL, Boost, established frameworks + → Custom math, custom state machines, novel incentive + structures, unusual callback/hook patterns — + THIS is where your time pays off most. + → Standard library imports are unlikely to have bugs. + The glue code connecting them is where things break. + +Q0.3: Where does VALUE actually sit? + → Follow the money. Every expensive mistake involves + value moving somewhere it shouldn't. + → Map every module/component that holds: + - Funds (native tokens, coins, balances, account credits) + - Assets (tokens, NFTs, resources, inventory) + - Sensitive data (keys, credentials, PII) + - Accounting state (shares, debt, rewards, balances) + → For each value store, ask: "What code path moves + value OUT? What authorizes it? What validates the amount?" + → The functions touching these stores get 10x more scrutiny. + +Q0.4: What's the most COMPLEX interaction path? + → Complexity kills. The most complex path through the + system is the most likely to contain bugs. + → Map paths that: cross multiple modules/contracts/services, + involve callbacks or hooks, mix user input with external data, + have multiple branching conditions, or chain state changes. + → If a path touches 4+ modules or has 3+ external calls, + it's a prime candidate for state inconsistency bugs. + → Cross-module interaction + value movement = audit gold. +``` + +**Output of Phase 0:** A prioritized hit list. + +``` +┌─────────────────────────────────────────────────────┐ +│ PHASE 0 — ATTACKER'S HIT LIST │ +├─────────────────────────────────────────────────────┤ +│ │ +│ LANGUAGE: [detected language/framework] │ +│ │ +│ ATTACK GOALS (from Q0.1): │ +│ 1. [worst outcome] │ +│ 2. [second worst] │ +│ 3. [third worst] │ +│ │ +│ NOVEL CODE — highest bug density (from Q0.2): │ +│ - [module/file] — [why it's novel] │ +│ - [module/file] — [why it's novel] │ +│ │ +│ VALUE STORES — follow the money (from Q0.3): │ +│ - [module] holds [asset] — [outflow functions] │ +│ - [module] holds [asset] — [outflow functions] │ +│ │ +│ COMPLEX PATHS — complexity kills (from Q0.4): │ +│ - [path description] — [modules involved] │ +│ - [path description] — [modules involved] │ +│ │ +│ PRIORITY ORDER (spend time here first): │ +│ 1. [highest priority target + why] │ +│ 2. [second priority target + why] │ +│ 3. [third priority target + why] │ +│ │ +└─────────────────────────────────────────────────────┘ +``` + +Functions and modules that appear in MULTIPLE answers above get audited FIRST +and with the DEEPEST scrutiny in Phase 2. Everything else is secondary. + +--- + +### Phase 1: Scope & Inventory + +``` +1. Identify ALL modules/contracts/packages in scope +2. For each module, list: + - ALL entry points (public/exported/external functions — the attack surface) + - ALL state they read/write (storage, globals, struct fields, DB) + - ALL access guards applied (modifiers, auth checks, visibility) + - ALL internal functions they call +3. Build a FUNCTION-STATE MATRIX: + | Function | Reads | Writes | Guards | Calls | + |----------|-------|--------|--------|-------| + This matrix is your map for consistency analysis (Category 3) +``` + +### Phase 2: Individual Function Deep Dive + +For EACH function, perform the Feynman interrogation: + +``` +┌─────────────────────────────────────────────────────┐ +│ FUNCTION: [module.functionName] │ +│ Visibility: [public/private/internal/exported] │ +│ Guards: [access control, auth checks, decorators] │ +│ State reads: [variables/fields/storage] │ +│ State writes: [variables/fields/storage] │ +│ External calls: [targets] │ +├─────────────────────────────────────────────────────┤ +│ │ +│ LINE-BY-LINE INTERROGATION: │ +│ │ +│ L[N]: [code line] │ +│ Q1.1 → WHY: [explanation or "CANNOT EXPLAIN" flag] │ +│ Q2.1 → ORDER: [what if moved up?] │ +│ Q2.2 → ORDER: [what if moved down?] │ +│ Q4.x → ASSUMES: [hidden assumption found] │ +│ Q5.x → EDGE: [boundary behavior] │ +│ → VERDICT: SOUND | SUSPECT | VULNERABLE │ +│ → If SUSPECT/VULNERABLE: [specific scenario] │ +│ │ +│ CROSS-FUNCTION CHECK: │ +│ Q3.1 → [guard consistency with sibling functions] │ +│ Q3.2 → [inverse operation parity] │ +│ Q3.3 → [parameter validation consistency] │ +│ │ +│ FUNCTION VERDICT: SOUND | HAS_CONCERNS | VULNERABLE │ +└─────────────────────────────────────────────────────┘ +``` + +**IMPORTANT**: You do NOT need to ask ALL questions for ALL lines. Use judgment: +- State-changing lines → heavy on Q2 (ordering) and Q4 (assumptions) +- Validation/guard lines → heavy on Q1 (purpose) and Q3 (consistency) +- External calls / cross-module calls → heavy on Q4 (assumptions), Q5 (edges), Q6 (returns) +- Math operations → heavy on Q5 (boundaries) and Q4.6 (amount assumptions) + +### Phase 3: Cross-Function Analysis + +``` +Using the Function-State Matrix from Phase 1: + +1. GUARD CONSISTENCY + - Group functions by the state variables they WRITE + - Within each group, list all access guards + - FLAG: Any function missing a guard its siblings have + +2. INVERSE OPERATION PARITY + - Pair up: deposit/withdraw, mint/burn, stake/unstake, + create/destroy, add/remove, open/close, encode/decode, etc. + - For each pair, compare: + - Parameter validation (Q3.2) + - State changes (are they truly inverse?) + - Access control (should both require same auth?) + - Event/log emission (are both tracked?) + +3. STATE TRANSITION INTEGRITY + - Map all valid state transitions + - For each transition, verify: + - Can it be triggered out of expected order? + - Can it be skipped entirely? + - Can it be triggered by an unauthorized actor? + - What if it's triggered when the system is in an unexpected state? + +4. VALUE FLOW TRACKING + - Trace value/asset flows across function boundaries + - Verify: value in == value out (conservation) + - FLAG: Any path where value can be created or destroyed unexpectedly +``` + +### Phase 4: Synthesize Raw Findings + +``` +For each SUSPECT or VULNERABLE verdict: + +1. Write the QUESTION that exposed it +2. Describe the SCENARIO (step-by-step) +3. Show the AFFECTED CODE (exact lines) +4. Explain WHY the current code fails +5. Assess IMPACT (what can an attacker gain/break?) +6. Classify severity: CRITICAL / HIGH / MEDIUM / LOW +7. Suggest a FIX (minimal, targeted) +``` + +Save raw (unverified) findings to: `.audit/findings/feynman-analysis-raw.md` + +**IMPORTANT: Do NOT report raw findings to the user as final results.** +These are HYPOTHESES that must be verified in Phase 5 before inclusion in the final report. + +--- + +### Phase 5: Verification Gate (MANDATORY before final report) + +**Every CRITICAL, HIGH, and MEDIUM finding from Phase 4 MUST be verified before +being included in the final report.** Feynman reasoning surfaces many hypotheses, +but code-level reasoning alone produces false positives (wrong mechanism assumed, +mitigating code missed, incorrect severity assessment). Verification eliminates +these before they reach the user. + +``` +VERIFICATION RULE: No C/H/M finding goes into the final report unverified. +Raw findings are HYPOTHESES. Verified findings are RESULTS. +``` + +#### Verification Methods (use whichever is most appropriate per finding): + +**Method A: Deep Code Trace Verification** +For findings about missing checks, wrong parameters, or inconsistent validation: +1. Read the EXACT lines cited in the finding +2. Trace the complete call chain (caller → callee → downstream effects) +3. Check for mitigating code elsewhere (guards in called functions, validation in callers) +4. Confirm the scenario is reachable end-to-end +5. Verdict: TRUE POSITIVE / FALSE POSITIVE / DOWNGRADE + +**Method B: PoC Test Verification** +For findings about math errors, rounding drift, resource limits, or state accounting: +1. Write a test using the project's native test framework: + - Solidity: Foundry test → `forge test --match-path "test/audit/[file]" -vvv` + - Move: `aptos move test --filter [test_name]` or `sui move test` + - Rust: `cargo test [test_name] -- --nocapture` + - Go: `go test -run TestName -v` + - C++: gtest/catch2 equivalent +2. The PoC must demonstrate the EXACT scenario described in the finding +3. If the test passes and output confirms the issue: TRUE POSITIVE +4. If the test fails or output disproves the claim: FALSE POSITIVE or DOWNGRADE + +**Method C: Hybrid (Code Trace + PoC)** +For complex findings spanning multiple modules: +1. First do a code trace to confirm the mechanism is plausible +2. Then write a PoC to confirm with concrete values and runtime behavior + +#### What to verify for each severity: + +| Severity | Verification Required | Method | +|----------|----------------------|--------| +| CRITICAL | MANDATORY — PoC required (Method B or C) | Must demonstrate value loss or permanent DoS with concrete numbers | +| HIGH | MANDATORY — Code trace + PoC recommended (Method A or C) | Must confirm the broken invariant is reachable | +| MEDIUM | MANDATORY — Code trace minimum (Method A) | Must confirm the mechanism is correct and not mitigated elsewhere | +| LOW | Optional — Code inspection sufficient | Quick sanity check: is the line/function real? | + +#### Verification Checklist (per finding): + +``` +[] 1. Does the cited code actually exist at the stated line numbers? +[] 2. Is the described mechanism correct? (trace the actual math/logic) +[] 3. Are there mitigating factors the finding missed? + - Called functions that add validation + - Access guards on calling functions + - Upstream checks that prevent the scenario + - Downstream checks that catch the error + - Language-level safety (borrow checker, type system, Move verifier) +[] 4. Is the severity accurate given the ACTUAL impact? + - Does "value loss" actually mean "revert/abort with confusing error"? + - Does "permanent DoS" actually mean "self-griefing only"? + - Is the "missing check" actually handled by a different code path? +[] 5. For PoC-verified findings: does the test output match the claim? +``` + +#### Common False Positive Patterns from Feynman Analysis: + +These patterns frequently produce hypotheses that fail verification: + +1. **"Missing authorization" that exists in a different layer:** + Finding says auth is missing, but the caller/router/middleware already + enforces it before this function is reachable. + +2. **"Rounding drift" that's cleaned by downstream code:** + Finding identifies `scale_up(scale_down(x)) < x` but misses cleanup + applied upstream that ensures x is always a clean multiple. + +3. **"No validation" that errors downstream:** + Finding says a parameter isn't validated, but the called function has its own + validation that catches invalid inputs (just with a confusing error message). + +4. **"Unbounded loop" bounded by design or economics:** + Finding says a loop has no cap, but the data structure is bounded by design, + or the economic cost of creating the DoS condition exceeds the benefit. + +5. **"Severity inflation":** + Finding claims CRITICAL (value loss) but actual impact is MEDIUM (error/DoS) + because a safety check catches the issue before value is affected. + +6. **"Language safety ignored":** + Finding claims overflow/underflow but the language aborts on overflow by + default (Move, Rust in debug, Solidity >=0.8). Or finding claims memory + unsafety in a memory-safe language. + +#### Phase 5 Output: + +After verification, produce the VERIFIED findings file: + +Save to: `.audit/findings/feynman-verified.md` + +```markdown +# Feynman Audit — Verified Findings + +## Verification Summary +| ID | Original Severity | Verdict | Final Severity | +|----|-------------------|---------|----------------| +| FF-001 | CRITICAL | TRUE POSITIVE — DOWNGRADE | LOW | +| FF-002 | HIGH | TRUE POSITIVE | HIGH | +| FF-003 | MEDIUM | FALSE POSITIVE | — | +| ... | ... | ... | ... | + +## Verified TRUE POSITIVE Findings +[Only findings that passed verification, with final severity] + +## False Positives Eliminated +[Findings that failed verification, with explanation of why] + +## Downgraded Findings +[Findings where severity was reduced, with justification] +``` + +**Only the verified findings file should be presented to the user as the final report.** + +--- + +## Severity Classification + +| Severity | Criteria | +|----------|----------| +| **CRITICAL** | Direct value/fund loss, permanent DoS, or system insolvency | +| **HIGH** | Conditional value loss, privilege escalation, or broken core invariant | +| **MEDIUM** | Value leakage, griefing with cost, or degraded functionality | +| **LOW** | Informational, inefficiency, or cosmetic inconsistency with no exploit | + +--- + +## Output Format + +Two files are produced during the audit: + +### 1. Raw Findings (intermediate — NOT the final deliverable) + +Save to: `.audit/findings/feynman-analysis-raw.md` + +This contains ALL hypotheses from Phases 1-4 before verification. Include the +Function-State Matrix, Guard Consistency Analysis, Inverse Operation Parity, +and all raw findings with their initial severity classification. + +### 2. Verified Findings (FINAL deliverable — present this to the user) + +Save to: `.audit/findings/feynman-verified.md` + +```markdown +# Feynman Audit — Verified Findings + +## Scope +- Language: [detected language] +- Modules analyzed: [list] +- Functions analyzed: [count] +- Lines interrogated: [count] + +## Verification Summary +| ID | Original Severity | Verdict | Final Severity | +|----|-------------------|---------|----------------| + +## Function-State Matrix +[The matrix from Phase 1] + +## Guard Consistency Analysis +[Results from Phase 3.1 — which functions are missing expected guards] + +## Inverse Operation Parity +[Results from Phase 3.2 — asymmetries between paired operations] + +## Verified Findings (TRUE POSITIVES only) + +### Finding FF-001: [Title] +**Severity:** CRITICAL | HIGH | MEDIUM | LOW +**Module:** [name] +**Function:** [name] +**Lines:** [L:start-end] +**Verification:** [Code trace / PoC / Hybrid] — [test file if PoC] + +**Feynman Question that exposed this:** +> [The exact question from the framework] + +**The code:** +```[language] +// [affected code block] +``` + +**Why this is wrong:** +[First-principles explanation — no jargon, no pattern names. +Explain like you're teaching someone who has never seen this bug class.] + +**Verification evidence:** +[For code trace: the exact mitigating/confirming code paths traced] +[For PoC: test name, key log output, concrete numbers] + +**Attack scenario:** +1. [Step-by-step exploitation] + +**Impact:** +[What an attacker gains or what breaks] + +**Suggested fix:** +```[language] +// [minimal fix] +``` + +--- + +## False Positives Eliminated +[Findings that failed verification, with explanation of WHY they are false] + +## Downgraded Findings +[Findings where severity was reduced, with justification] + +## LOW Findings (verified by inspection) +[Table of LOW findings with brief verdict] + +## Summary +- Total functions analyzed: [N] +- Raw findings (pre-verification): [N] CRITICAL | [N] HIGH | [N] MEDIUM | [N] LOW +- After verification: [N] TRUE POSITIVE | [N] FALSE POSITIVE | [N] DOWNGRADED +- Final: [N] HIGH | [N] MEDIUM | [N] LOW +``` + +--- + +## Post-Audit Actions + +| Scenario | Action | +|----------|--------| +| Need deeper context on a function | Re-read the function and its callers line-by-line | +| Finding confirmed as true positive | Write up with severity, trigger sequence, PoC, and fix | +| Need exploit validation | Write a Foundry/Hardhat PoC test to confirm | +| Uncertain about design intent | Check NatSpec, comments, and project documentation | + +--- + +## Anti-Hallucination Protocol + +``` +NEVER: +- Invent code that doesn't exist in the codebase +- Assume a function has an access guard without verifying +- Claim a variable is uninitialized without checking constructors/initializers +- Report a finding without showing the exact vulnerable code +- Use phrases like "could potentially" or "might be vulnerable" +- Apply language-specific assumptions to a different language + (e.g., don't assume Rust code has Solidity's reentrancy model) + +ALWAYS: +- Read the actual code before questioning it +- Verify your assumptions by reading called functions +- Check constructors, initializers, and default values +- Confirm guard/access-control behavior by reading the actual implementation +- Show exact file paths and line numbers for all references +- Use the correct language terminology (not Solidity terms for Rust code) +``` + +--- + +## Quick-Start Checklist + +When starting a Feynman audit: + +- [ ] **Phase 0:** Detect the language and adapt terminology +- [ ] **Phase 0:** Answer the 4 attacker mindset questions BEFORE reading code +- [ ] **Phase 0:** Build the Attacker's Hit List (attack goals, novel code, value stores, complex paths) +- [ ] **Phase 0:** Prioritize targets — functions appearing in multiple answers get audited first +- [ ] **Phase 1:** List all modules/contracts/packages in scope +- [ ] **Phase 1:** Build the Function-State Matrix +- [ ] **Phase 1:** Identify all function pairs (deposit/withdraw, etc.) +- [ ] **Phase 2:** Run Feynman interrogation on every entry point (priority order from Phase 0) +- [ ] **Phase 3:** Run cross-function analysis (guard consistency, inverse parity, value flow) +- [ ] **Phase 4:** Document all SUSPECT and VULNERABLE verdicts as raw findings +- [ ] **Phase 4:** Save raw findings to `.audit/findings/feynman-analysis-raw.md` +- [ ] **Phase 5:** Verify ALL C/H/M findings (code trace + PoC where needed) +- [ ] **Phase 5:** Eliminate false positives, downgrade inflated severities +- [ ] **Phase 5:** Save verified findings to `.audit/findings/feynman-verified.md` +- [ ] **Phase 5:** Present ONLY the verified report to the user diff --git a/.claude/skills/nemesis-auditor/SKILL.md b/.claude/skills/nemesis-auditor/SKILL.md new file mode 100644 index 00000000..978d0233 --- /dev/null +++ b/.claude/skills/nemesis-auditor/SKILL.md @@ -0,0 +1,1047 @@ +--- +name: nemesis-auditor +description: "The Inescapable Auditor. Runs the full Feynman Auditor (Stage 1) and full State Inconsistency Auditor (Stage 2) as primary steps, then fuses their outputs in a feedback loop (Stage 3) to find bugs at the intersection that neither alone would catch. Language-agnostic. Triggers on /nemesis or nemesis audit." +--- + +# N E M E S I S +### The Inescapable Auditor + +``` + ╔═══════════════════════════════════════════════════════════════╗ + ║ ║ + ║ "Nemesis — the goddess of divine retribution against ║ + ║ those who succumb to hubris." ║ + ║ ║ + ║ Your code was written with confidence. ║ + ║ Nemesis questions that confidence. ║ + ║ Then maps what your confidence forgot to protect. ║ + ║ Then questions it again. ║ + ║ ║ + ║ Nothing survives both passes. ║ + ║ ║ + ╚═══════════════════════════════════════════════════════════════╝ +``` + +Not three sequential stages. An **iterative back-and-forth loop** where Feynman and State Inconsistency run alternating passes — each pass informed by the previous pass's findings — until no new bugs surface. + +**Pass 1 (Feynman)** — Run the **complete Feynman Auditor** (`.claude/skills/feynman-auditor/SKILL.md`). Every line questioned. Every ordering challenged. Every assumption exposed. Collect findings + suspects. + +**Pass 2 (State)** — Run the **complete State Inconsistency Auditor** (`.claude/skills/state-inconsistency-auditor/SKILL.md`), **enriched by Pass 1's findings**. Feynman suspects become extra audit targets. Feynman's exposed assumptions reveal new coupled pairs to map. Collect findings + gaps. + +**Pass 3 (Feynman)** — Re-run Feynman **only on functions/state touched by Pass 2's new findings**. State gaps become new Feynman interrogation targets. Ask: "WHY is this sync missing? What assumption led to the gap? What breaks downstream?" Collect new findings. + +**Pass 4 (State)** — Re-run State Mapper **only on new coupled pairs and mutation paths exposed by Pass 3**. Check if Feynman's new findings reveal additional state desync. Collect new findings. + +**...continue alternating until convergence (no new findings in a pass).** + +**Language-agnostic.** Works on Solidity, Move, Rust, Go, C++, or anything else. + +--- + +## When to Activate + +- User says `/nemesis` or `nemesis audit` or `deep combined audit` +- User wants maximum-depth business logic + state inconsistency coverage +- When the codebase is complex enough that either auditor alone would miss cross-cutting bugs + +## When NOT to Use + +- Quick pattern-matching scans where you only need known vulnerability patterns +- Simple spec compliance checks +- Report generation from existing findings + +--- + +## The Nemesis Execution Model: Iterative Back-and-Forth + +``` +┌──────────────────────────────────────────────────────────────────────┐ +│ N E M E S I S — I T E R A T I V E L O O P │ +├──────────────────────────────────────────────────────────────────────┤ +│ │ +│ PHASE 0: RECON │ +│ ─────────────── │ +│ Attacker mindset (Q0.1-Q0.5) + Initial coupling hypothesis │ +│ Output: Hit List + Priority Targets │ +│ │ +│ ════════════════════════════════════════════════════════════════ │ +│ ║ ITERATIVE PASS LOOP BEGINS ║ │ +│ ════════════════════════════════════════════════════════════════ │ +│ │ +│ ┌────────────────────────────────────────────────────────────┐ │ +│ │ PASS 1 — FEYNMAN (full skill, first run) │ │ +│ │ │ │ +│ │ Load: .claude/skills/feynman-auditor/SKILL.md │ │ +│ │ Execute complete pipeline: Phase 0→1→2→3→4→5 │ │ +│ │ Input: Raw codebase + Phase 0 hit list │ │ +│ │ Output: │ │ +│ │ • Verified findings (.audit/findings/feynman-pass1.md) │ │ +│ │ • SUSPECT verdicts (functions + state vars flagged) │ │ +│ │ • Exposed assumptions (implicit trusts about state) │ │ +│ │ • Ordering concerns (external call timing issues) │ │ +│ │ • Multi-tx state corruption candidates │ │ +│ │ • Function-State Matrix │ │ +│ └────────────────────────────────────────────────────────────┘ │ +│ ↓ feed forward │ +│ ┌────────────────────────────────────────────────────────────┐ │ +│ │ PASS 2 — STATE INCONSISTENCY (full skill, enriched) │ │ +│ │ │ │ +│ │ Load: .claude/skills/state-inconsistency-auditor/SKILL.md │ │ +│ │ Execute complete pipeline: Phase 1→2→3→4→5→6→7→8 │ │ +│ │ Input: Raw codebase + ALL of Pass 1's output │ │ +│ │ │ │ +│ │ ENRICHMENT from Pass 1: │ │ +│ │ • Feynman SUSPECTS → add as extra state audit targets │ │ +│ │ • Exposed assumptions → reveal NEW coupled pairs │ │ +│ │ ("dev assumes X stays in sync" → map X as coupled) │ │ +│ │ • Ordering concerns → check if state gap exists at │ │ +│ │ the flagged ordering point │ │ +│ │ • Function-State Matrix → use as base for Mutation Matrix │ │ +│ │ │ │ +│ │ Output: │ │ +│ │ • Verified findings (.audit/findings/state-pass2.md) │ │ +│ │ • State GAPS (functions missing coupled updates) │ │ +│ │ • New coupled pairs discovered via Feynman enrichment │ │ +│ │ • Masking code flagged (ternary clamps, min caps) │ │ +│ │ • Parallel path mismatches │ │ +│ │ • Coupled State Dependency Map │ │ +│ │ • Mutation Matrix │ │ +│ └────────────────────────────────────────────────────────────┘ │ +│ ↓ feed back │ +│ ┌────────────────────────────────────────────────────────────┐ │ +│ │ PASS 3 — FEYNMAN RE-INTERROGATION (targeted, not full) │ │ +│ │ │ │ +│ │ Scope: ONLY functions/state touched by Pass 2's NEW output │ │ +│ │ DO NOT re-audit what Pass 1 already cleared. │ │ +│ │ │ │ +│ │ For each State GAP from Pass 2: │ │ +│ │ Q: "WHY doesn't [function] update [coupled state B]?" │ │ +│ │ Q: "What ASSUMPTION led to this gap?" │ │ +│ │ Q: "What DOWNSTREAM function reads B and breaks?" │ │ +│ │ Q: "Can an attacker CHOOSE a sequence to exploit this?" │ │ +│ │ │ │ +│ │ For each MASKING CODE from Pass 2: │ │ +│ │ Q: "WHY would this ever underflow/overflow?" │ │ +│ │ Q: "What invariant is ACTUALLY broken underneath?" │ │ +│ │ → Trace the broken invariant to its root cause mutation │ │ +│ │ │ │ +│ │ For each NEW COUPLED PAIR from Pass 2: │ │ +│ │ Q: "Is this coupling intentional or accidental?" │ │ +│ │ Q: "What ordering constraints exist between the pair?" │ │ +│ │ Q: "What happens across multiple txs as both drift?" │ │ +│ │ │ │ +│ │ Output: │ │ +│ │ • New findings (.audit/findings/feynman-pass3.md) │ │ +│ │ • New suspects (if any) │ │ +│ │ • Deeper root cause analysis on Pass 2 gaps │ │ +│ │ • Multi-tx adversarial sequences for confirmed bugs │ │ +│ └────────────────────────────────────────────────────────────┘ │ +│ ↓ feed back │ +│ ┌────────────────────────────────────────────────────────────┐ │ +│ │ PASS 4 — STATE RE-ANALYSIS (targeted, not full) │ │ +│ │ │ │ +│ │ Scope: ONLY new coupled pairs + mutation paths from Pass 3 │ │ +│ │ DO NOT re-audit what Pass 2 already cleared. │ │ +│ │ │ │ +│ │ For each NEW SUSPECT from Pass 3: │ │ +│ │ → Is this suspect state part of a coupled pair? │ │ +│ │ → Does the suspect function update all counterparts? │ │ +│ │ → Does the root cause analysis reveal additional gaps? │ │ +│ │ │ │ +│ │ For each ROOT CAUSE from Pass 3: │ │ +│ │ → Trace the root cause mutation through ALL code paths │ │ +│ │ → Check parallel paths for the same root cause │ │ +│ │ → Check if the root cause affects other coupled pairs │ │ +│ │ │ │ +│ │ Output: │ │ +│ │ • New findings (.audit/findings/state-pass4.md) │ │ +│ │ • Any remaining gaps or suspects │ │ +│ └────────────────────────────────────────────────────────────┘ │ +│ ↓ │ +│ ┌────────────────────────────────────────────────────────────┐ │ +│ │ CONVERGENCE CHECK │ │ +│ │ │ │ +│ │ Did the last pass produce ANY new: │ │ +│ │ - Findings not in previous passes? │ │ +│ │ - Coupled pairs not previously mapped? │ │ +│ │ - Suspects not previously flagged? │ │ +│ │ - Root causes not previously traced? │ │ +│ │ │ │ +│ │ IF YES → Continue: Run Pass N+1 (alternate Feynman/State) │ │ +│ │ Scope: ONLY new items from the previous pass │ │ +│ │ │ │ +│ │ IF NO → Converged. Proceed to Final Phase. │ │ +│ │ │ │ +│ │ SAFETY: Maximum 6 total passes (3 Feynman + 3 State) │ │ +│ │ to prevent infinite loops. │ │ +│ └────────────────────────────────────────────────────────────┘ │ +│ │ +│ ════════════════════════════════════════════════════════════════ │ +│ ║ ITERATIVE LOOP ENDS ║ │ +│ ════════════════════════════════════════════════════════════════ │ +│ │ +│ FINAL PHASE: CONSOLIDATION │ +│ ─────────────────────────── │ +│ 1. Merge all pass outputs into unified finding set │ +│ 2. Deduplicate (same root cause found from both sides) │ +│ 3. Multi-Tx adversarial sequence tracing on ALL confirmed bugs │ +│ 4. Final Verification Gate (code trace + PoC for all C/H/M) │ +│ 5. Tag each finding with discovery path: │ +│ • "Feynman-only" — found in Pass 1, never enriched by State │ +│ • "State-only" — found in Pass 2, never enriched by Feynman │ +│ • "Cross-feed P[N]→P[M]" — found via back-and-forth interaction │ +│ 6. Output: .audit/findings/nemesis-verified.md │ +│ │ +└──────────────────────────────────────────────────────────────────────┘ +``` + +**KEY RULES FOR THE ITERATIVE LOOP:** + +``` +1. Pass 1 (Feynman) and Pass 2 (State) are FULL skill runs — complete pipelines. + They establish the baseline. + +2. Pass 3+ are TARGETED — only audit new items surfaced by the previous pass. + Do NOT re-audit what was already cleared. This prevents redundant work + while ensuring every new discovery gets deep analysis from both perspectives. + +3. Each pass MUST produce a delta — what's NEW compared to all previous passes. + The delta is what feeds the next pass. No delta = convergence. + +4. Alternate strictly: Feynman → State → Feynman → State → ... + Never run the same auditor twice in a row. + +5. Maximum 6 total passes (3 Feynman + 3 State). In practice, most audits + converge in 3-4 passes (Pass 1 + Pass 2 + 1-2 targeted re-passes). + +6. Track the DISCOVERY PATH for every finding. Findings that emerged from + cross-feed (e.g., "State gap in Pass 2 → Feynman root cause in Pass 3") + are the highest-value discoveries — they prove the loop's worth. +``` + +--- + +## Core Philosophy + +``` +Feynman alone finds logic bugs but may miss state coupling gaps. +State Mapper alone finds desync bugs but may miss WHY the state was designed that way. + +NEMESIS runs them BACK AND FORTH — each pass feeds the next. + +The iterative loop: +┌─────────────────────────────────────────────────────────────┐ +│ │ +│ PASS 1 — FEYNMAN (full run): │ +│ "WHY does this state update exist?" │ +│ → Finds: ordering bugs, assumption violations, suspects │ +│ → Exposes: "This line maintains invariant X with State B" │ +│ │ +│ ↓ feed suspects + assumptions + matrix forward │ +│ │ +│ PASS 2 — STATE (full run, enriched by Pass 1): │ +│ "Do ALL paths that touch A also touch B?" │ +│ → Uses Feynman suspects as extra audit targets │ +│ → Uses exposed assumptions to discover NEW coupled pairs │ +│ → Finds: gaps, masking code, parallel path mismatches │ +│ │ +│ ↓ feed gaps + masking code + new pairs back │ +│ │ +│ PASS 3 — FEYNMAN (targeted re-interrogation): │ +│ "WHY doesn't liquidate() update B?" │ +│ "What assumption led to this gap?" │ +│ "What breaks downstream after N transactions?" │ +│ → Root cause analysis on State's gaps │ +│ → NEW suspects emerge from deeper questioning │ +│ │ +│ ↓ feed new suspects + root causes back │ +│ │ +│ PASS 4 — STATE (targeted re-analysis): │ +│ "Does this root cause affect OTHER coupled pairs?" │ +│ "Do parallel paths share the same root cause?" │ +│ → Finds ADDITIONAL gaps via root cause propagation │ +│ │ +│ ↓ ... continue until convergence ... │ +│ │ +│ CONVERGED — No new findings in the last pass. │ +│ Consolidate + verify + deliver. │ +│ │ +└─────────────────────────────────────────────────────────────┘ +``` + +--- + +## Core Rules + +``` +RULE 0: THE ITERATIVE LOOP IS MANDATORY +Never run Feynman and State Mapper as isolated one-shot passes. +They MUST alternate back and forth. Each pass feeds the next. +The loop runs until no new findings emerge. + +RULE 1: FULL FIRST, TARGETED AFTER +Pass 1 (Feynman) and Pass 2 (State) are FULL skill runs. +Pass 3+ are TARGETED — only audit the delta from the previous pass. +Never re-audit what was already cleared. Always go deeper on what's new. + +RULE 2: EVERY COUPLED PAIR GETS INTERROGATED +The State Mapper finds pairs. Feynman interrogates each one: +"Why are these coupled? What invariant links them? Is the +invariant ACTUALLY maintained by every mutation path?" + +RULE 3: EVERY FEYNMAN SUSPECT GETS STATE-TRACED +When Feynman flags a line as SUSPECT, the State Mapper traces +every state variable that line touches, maps all their coupled +dependencies, and checks if the suspicion propagates. + +RULE 4: PARTIAL OPERATIONS + ORDERING = GOLD +The intersection of "partial state change" (State Mapper's +specialty) and "operation ordering" (Feynman's Category 2 & 7) +is where the highest-value bugs live. + +RULE 5: DEFENSIVE CODE IS A SIGNAL, NOT A SOLUTION +When the State Mapper finds masking code (ternary clamps, min caps), +Feynman interrogates WHY it exists. The mask reveals the invariant +that's actually broken underneath. + +RULE 6: EVIDENCE OR SILENCE +No finding without: coupled pair, breaking operation, trigger +sequence, downstream consequence, and verification. +``` + +--- + +## Language Adaptation + +Detect the language and adapt. The questions and methodology are universal. + +| Concept | Solidity | Move | Rust | Go | C++ | +|---------|----------|------|------|----|-----| +| Module/unit | contract | module | crate/mod | package | class/namespace | +| Entry point | external/public fn | public fun | pub fn | Exported fn | public method | +| State storage | storage variables | global storage / resources | struct fields / state | struct fields / DB | member variables | +| Access guard | modifier | access control / friend | trait bound / #[cfg] | middleware / auth | access specifier | +| Mapping | mapping(k => v) | Table\ | HashMap / BTreeMap | map[K]V | std::map | +| Delete | delete mapping[key] | table::remove | map.remove(&key) | delete(map, key) | map.erase(key) | +| Caller identity | msg.sender | &signer | caller / Context | ctx / request.User | this / session | +| Error/abort | revert / require | abort / assert! | panic! / Result::Err | error / panic | throw / exception | +| Checked math | 0.8+ auto / SafeMath | built-in overflow abort | checked_add | math/big | safe int libs | +| External call | .call() / interface | cross-module call | CPI (Solana) | RPC / HTTP | virtual call | +| Test framework | Foundry / Hardhat | Move Prover / aptos test | cargo test | go test | gtest / catch2 | + +--- + +## The Nemesis Execution Pipeline + +``` +┌───────────────────────────────────────────────────────────────────┐ +│ N E M E S I S P I P E L I N E │ +├───────────────────────────────────────────────────────────────────┤ +│ │ +│ RECON Phase 0: Attacker Mindset + Hit List │ +│ ───── (Feynman Q0.1-Q0.4 + State value store mapping) │ +│ │ +│ FOUNDATION Phase 1: Dual Mapping │ +│ ────────── ├─ Feynman: Function-State Matrix │ +│ └─ State: Coupled State Dependency Map │ +│ │ +│ HUNT PASS 1 Phase 2: Feynman Interrogation (all 7 categories) │ +│ ─────────── Each SUSPECT verdict → fed to Phase 3 │ +│ │ +│ HUNT PASS 2 Phase 3: State Cross-Check │ +│ ─────────── Mutation Matrix + Parallel Path Comparison │ +│ + Feynman suspects as extra audit targets │ +│ │ +│ FEEDBACK Phase 4: The Nemesis Loop │ +│ ──────── ├─ State gaps → Feynman re-interrogation │ +│ ├─ Feynman findings → State dependency expansion │ +│ ├─ Masking code → Feynman "WHY" questioning │ +│ └─ Loop until convergence (no new findings) │ +│ │ +│ SEQUENCES Phase 5: Multi-Transaction Journey Tracing │ +│ ───────── Adversarial sequences across both dimensions │ +│ │ +│ VERIFY Phase 6: Verification Gate │ +│ ────── Code trace + PoC for all C/H/M findings │ +│ │ +│ DELIVER Phase 7: Final Report │ +│ ─────── Only TRUE POSITIVES. Zero noise. │ +│ │ +└───────────────────────────────────────────────────────────────────┘ +``` + +--- + +### Phase 0: Attacker Recon (BEFORE reading code) + +Combine Feynman's attacker mindset with State Mapper's value tracking: + +``` +Q0.1: ATTACK GOALS — What's the WORST an attacker can achieve? + List top 3-5 catastrophic outcomes. These drive the entire audit. + +Q0.2: NOVEL CODE — What's NOT a fork of battle-tested code? + Custom math, novel mechanisms, unique state machines = highest bug density. + +Q0.3: VALUE STORES — Where does value actually sit? + Map every module that holds funds, assets, accounting state. + For each: what code path moves value OUT? What authorizes it? + +Q0.4: COMPLEX PATHS — What's the most complex interaction path? + Paths crossing 4+ modules with 3+ external calls = prime targets. + +Q0.5: COUPLED VALUE — Which value stores have DEPENDENT accounting? + (NEW — State Mapper contribution to recon) + For each value store from Q0.3, ask: "What other storage must + stay in sync with this?" Build the initial coupling hypothesis + BEFORE reading code. The code will confirm or reveal more. +``` + +**Output:** Attacker's Hit List + Initial Coupling Hypothesis + +``` +┌─────────────────────────────────────────────────────────────┐ +│ PHASE 0 — NEMESIS RECON │ +├─────────────────────────────────────────────────────────────┤ +│ │ +│ LANGUAGE: [detected] │ +│ │ +│ ATTACK GOALS: │ +│ 1. [worst outcome] │ +│ 2. [second worst] │ +│ 3. [third worst] │ +│ │ +│ NOVEL CODE (highest bug density): │ +│ - [module] — [why novel] │ +│ │ +│ VALUE STORES + INITIAL COUPLING HYPOTHESIS: │ +│ - [module] holds [asset] │ +│ Outflows: [functions] │ +│ Suspected coupled state: [what must sync] │ +│ │ +│ COMPLEX PATHS: │ +│ - [path] — [modules involved] │ +│ │ +│ PRIORITY ORDER: │ +│ 1. [target] — appears in [N] answers above │ +│ 2. [target] — appears in [N] answers above │ +│ │ +└─────────────────────────────────────────────────────────────┘ +``` + +--- + +### Phase 1: Dual Mapping (Foundation for both auditors) + +Run both mapping operations simultaneously. They share the same codebase scan. + +#### 1A: Function-State Matrix (Feynman foundation) + +``` +For each module, list: +- ALL entry points (public/exported/external functions) +- ALL state they read/write +- ALL access guards applied +- ALL internal functions they call +- ALL external calls they make + +| Function | Reads | Writes | Guards | Internal Calls | External Calls | +|----------|-------|--------|--------|----------------|----------------| +``` + +#### 1B: Coupled State Dependency Map (State Mapper foundation) + +``` +For every storage variable, ask: +"What other storage values MUST change when this one changes?" + +Build the dependency graph: + State A changes → State B MUST change (invariant: [relationship]) + State C changes → State D AND State E MUST change + +Look for: +- per-user balance ↔ per-user accumulator/tracker/checkpoint +- numerator ↔ denominator +- position size ↔ position-derived values (health, rewards, shares) +- total/aggregate ↔ sum of individual components +- any cached computation ↔ inputs it was derived from +- any index/accumulator ↔ last-snapshot of that index per user +``` + +#### 1C: Cross-Reference (THE NEMESIS DIFFERENCE) + +``` +Overlay the two maps: + +For each COUPLED PAIR from 1B: + → Find ALL functions from 1A that WRITE to either side + → Mark which functions update BOTH sides vs only ONE side + → Functions that update only ONE side = PRIMARY AUDIT TARGETS + +For each FUNCTION from 1A: + → List ALL state variables it writes + → For each written variable, check 1B: is it part of a coupled pair? + → If yes: does this function ALSO write the coupled counterpart? + → If no: mark as STATE GAP — feed to Phase 3 AND Phase 4 +``` + +**Output:** Unified Nemesis Map — functions × state × couplings × gaps + +``` +┌────────────────────────────────────────────────────────────────────┐ +│ NEMESIS MAP — Phase 1 Cross-Reference │ +├───────────────┬──────────┬──────────┬──────────┬──────────────────┤ +│ Function │ Writes A │ Writes B │ A↔B Pair │ Sync Status │ +├───────────────┼──────────┼──────────┼──────────┼──────────────────┤ +│ deposit() │ ✓ │ ✓ │ bal↔chk │ ✓ SYNCED │ +│ withdraw() │ ✓ │ ✓ │ bal↔chk │ ✓ SYNCED │ +│ transfer() │ ✓ │ ✗ │ bal↔chk │ ✗ GAP → Phase 4 │ +│ liquidate() │ ✓ │ ✗ │ bal↔chk │ ✗ GAP → Phase 4 │ +│ emergencyW() │ ✓ │ ✗ │ bal↔chk │ ✗ GAP → Phase 4 │ +└───────────────┴──────────┴──────────┴──────────┴──────────────────┘ +``` + +--- + +### Phase 2: Feynman Interrogation (Hunt Pass 1) + +Apply ALL 7 Feynman Question Categories to every function, in priority order from Phase 0. + +**Categories (28+ core questions):** + +``` +Category 1: Purpose — WHY is this line here? What breaks if deleted? +Category 2: Ordering — What if this line moves up/down? State gap window? +Category 3: Consistency — WHY does funcA have this guard but funcB doesn't? +Category 4: Assumptions — What is implicitly trusted about caller/data/state/time? +Category 5: Boundaries — First call, last call, double call, self-reference? +Category 6: Return/Error — Ignored returns, silent failures, fallthrough paths? +Category 7: Call Reorder — Swap external call before/after state update? + + Multi-Tx — Same function, different values, across time? +``` + +For each function: +``` +┌─────────────────────────────────────────────────────────────┐ +│ FUNCTION: [module.functionName] │ +│ Priority: [from Phase 0 hit list] │ +│ │ +│ LINE-BY-LINE INTERROGATION: │ +│ │ +│ L[N]: [code line] │ +│ Q[x.y] → [answer] │ +│ → VERDICT: SOUND | SUSPECT | VULNERABLE │ +│ → If SUSPECT: [specific scenario] │ +│ → STATE FEED: [state variables touched — feed to Phase 3] │ +│ │ +│ FUNCTION VERDICT: SOUND | HAS_CONCERNS | VULNERABLE │ +│ │ +│ SUSPECTS FOR STATE MAPPER (feed to Phase 3): │ +│ - [state var] — [why suspicious from Feynman questioning] │ +│ - [ordering concern] — [which states are in the gap] │ +│ │ +└─────────────────────────────────────────────────────────────┘ +``` + +**Critical — Category 7 deep checks:** + +For every external call in every function: +1. **Swap test**: Move the external call before/after state updates. Does it revert? If not, the original ordering may be exploitable. +2. **Callee power audit**: At the moment of the external call, what state is committed vs pending? What can the callee observe or manipulate? +3. **Multi-tx state corruption**: Call the function with value X, then again with value Y. Does the second call use stale state from the first? Does accumulated state from many calls create unreachable conditions? + +**Feed forward**: Every SUSPECT verdict and every state variable touched by suspect code is passed to Phase 3 as an additional audit target. + +--- + +### Phase 3: State Cross-Check (Hunt Pass 2) + +The State Mapper now runs its full analysis, ENRICHED by Feynman's Phase 2 output. + +#### 3A: Mutation Matrix + +For EACH state variable (including new ones Feynman flagged): +``` +List every function that modifies it: +- Direct writes, increments, decrements, deletions +- Indirect mutations (internal calls, hooks, callbacks) +- Implicit changes (burns, rebases, external triggers) + +┌──────────────────┬───────────────────┬───────────────────────────┐ +│ State Variable │ Mutating Function │ Updates Coupled State? │ +├──────────────────┼───────────────────┼───────────────────────────┤ +│ [var] │ [function] │ ✓ / ✗ GAP / ??? CHECK │ +└──────────────────┴───────────────────┴───────────────────────────┘ +``` + +#### 3B: Parallel Path Comparison + +``` +Group functions that achieve similar outcomes: +- transfer() vs burn() — both reduce sender balance +- withdraw() vs liquidate() — both reduce position +- partial vs full removal +- direct vs wrapper call +- normal vs emergency/admin path +- single vs batch operation + +For each group: do ALL paths update the SAME coupled state? + +┌─────────────────┬──────────────┬──────────────┬────────────┐ +│ Coupled State │ Path A │ Path B │ Path C │ +├─────────────────┼──────────────┼──────────────┼────────────┤ +│ [state pair] │ ✓/✗ │ ✓/✗ │ ✓/✗ │ +└─────────────────┴──────────────┴──────────────┴────────────┘ +``` + +#### 3C: Operation Ordering Within Functions + +``` +Trace the exact order of state changes in each function: + +step 1: reads A and B → computes result +step 2: modifies B based on result +step 3: modifies A +// B is now stale relative to new A — gap between step 2 and step 3 + +At each step ask: +- Are ALL coupled pairs still consistent RIGHT HERE? +- Does step N use a value that step N-1 already invalidated? +- If an external call happens between steps, can the callee see + inconsistent state? +``` + +#### 3D: Feynman-Enriched Targets + +``` +For each SUSPECT from Phase 2: + → The State Mapper now specifically checks: + 1. Is the suspect state variable part of a coupled pair? + 2. Does the suspect function update all coupled counterparts? + 3. Does the ordering concern from Feynman create a state gap + that the State Mapper can now measure? + +This is where the FEEDBACK LOOP produces findings that NEITHER +auditor would find alone. +``` + +**Feed forward**: Every GAP from Phase 3 is passed to Phase 4 for Feynman re-interrogation. + +--- + +### Phase 4: The Nemesis Loop (FEEDBACK — the core innovation) + +This is what makes Nemesis more than the sum of its parts. The two auditors now interrogate EACH OTHER'S findings. + +``` +LOOP { + ┌─────────────────────────────────────────────────────────┐ + │ STEP A: State Mapper gaps → Feynman re-interrogation │ + │ │ + │ For each GAP found in Phase 3: │ + │ Feynman asks: │ + │ Q: "WHY doesn't [function] update [coupled state B] │ + │ when it modifies [state A]?" │ + │ Q: "What ASSUMPTION is the developer making about │ + │ when [coupled state B] gets updated?" │ + │ Q: "What DOWNSTREAM function reads [state B] and │ + │ would produce a wrong result from the stale value?"│ + │ Q: "Can an attacker CHOOSE a sequence that exploits │ + │ this gap before [state B] gets reconciled?" │ + │ │ + │ → If Feynman finds the gap is real: FINDING │ + │ → If Feynman finds lazy reconciliation: FALSE POSITIVE │ + │ → If Feynman finds a NEW coupled pair: feed back to 3 │ + └─────────────────────────────────────────────────────────┘ + │ + ↓ + ┌─────────────────────────────────────────────────────────┐ + │ STEP B: Feynman findings → State dependency expansion │ + │ │ + │ For each Feynman SUSPECT/VULNERABLE verdict: │ + │ State Mapper asks: │ + │ Q: "Does this suspicious line WRITE to a state that │ + │ is part of a coupled pair I haven't mapped yet?" │ + │ Q: "Does the ordering concern create a WINDOW where │ + │ coupled state is inconsistent?" │ + │ Q: "Does the assumption violation mean a coupled │ + │ state's invariant is based on a false premise?" │ + │ │ + │ → If State Mapper finds new coupling: add to map, │ + │ re-run 3A-3C for the new pair │ + │ → If no new coupling: Feynman finding stands alone │ + └─────────────────────────────────────────────────────────┘ + │ + ↓ + ┌─────────────────────────────────────────────────────────┐ + │ STEP C: Masking code → Joint interrogation │ + │ │ + │ For each defensive/masking pattern found: │ + │ (ternary clamps, min caps, try/catch, early returns) │ + │ │ + │ Feynman asks: "WHY would this ever underflow/overflow? │ + │ What invariant is ACTUALLY broken underneath?" │ + │ │ + │ State Mapper asks: "Which coupled pair's desync is │ + │ this mask hiding? Trace the pair to find the root │ + │ mutation that broke the invariant." │ + │ │ + │ → Combined answer: the mask, the broken invariant, │ + │ the root cause mutation, and the downstream impact │ + └─────────────────────────────────────────────────────────┘ + │ + ↓ + ┌─────────────────────────────────────────────────────────┐ + │ STEP D: Convergence check │ + │ │ + │ Did Steps A-C produce ANY new: │ + │ - Coupled pairs not in the Phase 1 map? │ + │ - Mutation paths not in the Phase 3 matrix? │ + │ - Feynman suspects not in the Phase 2 output? │ + │ - Masking patterns not previously flagged? │ + │ │ + │ IF YES → loop back to STEP A with expanded scope │ + │ IF NO → converged. Proceed to Phase 5. │ + │ │ + │ SAFETY: Maximum 3 loop iterations to prevent runaway. │ + └─────────────────────────────────────────────────────────┘ +} +``` + +--- + +### Phase 5: Multi-Transaction Journey Tracing + +Now that both auditors have converged, trace adversarial sequences that exploit findings from BOTH dimensions. + +``` +For each finding from Phases 2-4, construct a MINIMAL trigger sequence: + +SEQUENCE TEMPLATE: + 1. Initial state (clean) + 2. Operation that modifies State A (coupled to B) + 3. [Optional: time passes / external state evolves] + 4. Operation that SHOULD update B but DOESN'T (the gap) + 5. [Optional: repeat steps 2-4 to compound the error] + 6. Operation that reads BOTH A and B → produces wrong result + +ADVERSARIAL SEQUENCES TO ALWAYS TEST: + - Deposit → partial withdraw → claim rewards + (rewards computed on which balance? old or new?) + + - Stake → unstake half → restake → unstake all + (reward debt accumulated correctly through each step?) + + - Open position → add collateral → partial close → health check + (cached health factor updated at each step?) + + - Provide liquidity → swaps happen → remove liquidity + (fee tracking correct through reserve changes?) + + - Delegate votes → transfer tokens → vote + (voting power reflects current balance?) + + - Borrow → partial repay → borrow again → check debt + (interest accumulator rebased at each step?) + + - Swap with value X → swap with value Y → claim fees + (fee accumulator path-dependent? See worked example below) + +MULTI-TX STATE CORRUPTION — WORKED EXAMPLE: + ───────────────────────────────────────── + AMM pool with swap() that: + 1. Calculates amountOut based on reserves + 2. Updates accumulatedFees (for LP fee distribution) + 3. Updates reserves + + TX1: Alice swaps 1000 tokenA → tokenB (0.3% fee) + - fee = 3 tokenA added to accFees BEFORE reserves update + - reserves shift: reserveA=11000, reserveB≈9091 + + TX2: Bob swaps 500 tokenA → tokenB + - fee = 1.5 tokenA added to accFees + - feePerLP calculated using STALE reserve ratio from pre-TX1 + - 1 tokenA is now worth LESS in the pool, but fee accounting + doesn't know that + + TX3: Charlie claims LP fees + - Gets paid based on accFees=4.5 at OLD token valuation + - Pool composition has shifted — fees are denominated in a + token whose relative value changed + - Result: early LPs overpaid, late LPs underpaid + + Root cause: accFees accumulator doesn't rebase against current + reserve ratio. Each swap changes what "1 unit of fee" means, + but the accumulator treats all units as equal. + + GENERALIZE: Any global accumulator (fees, rewards, interest) + updated per-tx where the VALUE of what's accumulated changes + between txs, and the accumulator doesn't normalize. + + CHECK: After N operations with varying sizes, does + SUM(individual fees) == fee on AGGREGATE operation? + If not → path-dependent accumulator → exploitable. +``` + +--- + +### Phase 6: Verification Gate (MANDATORY) + +**Every CRITICAL, HIGH, and MEDIUM finding MUST be verified.** + +#### Methods: + +**Method A: Deep Code Trace** +1. Read exact lines cited +2. Trace complete call chain (caller → callee → downstream) +3. Check for mitigating code elsewhere (guards, hooks, lazy reconciliation) +4. Confirm scenario is reachable end-to-end +5. Verdict: TRUE POSITIVE / FALSE POSITIVE / DOWNGRADE + +**Method B: PoC Test** +1. Write test in project's native framework +2. Execute the exact trigger sequence from the finding +3. Assert state inconsistency after the breaking operation +4. Assert incorrect result in the downstream operation +5. Verdict: TRUE POSITIVE / FALSE POSITIVE + +**Method C: Hybrid** (trace + PoC) for complex multi-module findings. + +#### Common False Positive Patterns (from BOTH auditors): + +``` +1. HIDDEN RECONCILIATION: Coupled state IS updated, but through an + internal call chain you missed (_beforeTokenTransfer hook, modifier + that runs _updateReward before every function). + +2. LAZY EVALUATION: Coupled state is intentionally stale and reconciled + on next READ, not on every WRITE. The desync is by design. + +3. IMMUTABLE AFTER INIT: The coupled state is set once and never needs + updating because both sides are frozen after initialization. + +4. DESIGNED ASYMMETRY: The states are intentionally NOT coupled the way + you assumed. Read docs/comments before reporting. + +5. LANGUAGE SAFETY: Finding claims overflow but the language aborts on + overflow by default (Solidity >=0.8, Move, Rust debug). + +6. SEVERITY INFLATION: Finding claims "value loss" but actual impact is + "confusing error message" because a downstream check catches it. + +7. ECONOMIC INFEASIBILITY: The attack costs more than it gains. + Flash loans don't make everything free — compute the actual profit. +``` + +#### Verification Output per Finding: + +``` +Finding NM-XXX: [Title] +├─ Verification method: [A / B / C] +├─ Code trace: [paths traced, mitigations checked] +├─ PoC result: [test name, pass/fail, key output] +├─ Mitigating factors found: [none / list] +└─ VERDICT: TRUE POSITIVE [severity] / FALSE POSITIVE [reason] / DOWNGRADE [from→to] +``` + +--- + +### Phase 7: Final Report + +Save to: `.audit/findings/nemesis-verified.md` + +```markdown +# N E M E S I S — Verified Findings + +## Scope +- Language: [detected] +- Modules analyzed: [list] +- Functions analyzed: [count] +- Coupled state pairs mapped: [count] +- Mutation paths traced: [count] +- Nemesis loop iterations: [count] + +## Nemesis Map (Phase 1 Cross-Reference) +[Unified map: functions × state × couplings × gaps] + +## Verification Summary +| ID | Source | Coupled Pair | Breaking Op | Severity | Verdict | +|----|--------|-------------|-------------|----------|---------| +| NM-001 | Feynman→State | A↔B | func() | HIGH | TRUE POS | +| NM-002 | State→Feynman | C↔D | func2() | MEDIUM | TRUE POS | +| NM-003 | Loop Step C | E↔F | func3() | HIGH | DOWNGRADE→MED | +| NM-004 | Feynman only | — | func4() | MEDIUM | FALSE POS | + +## Verified Findings (TRUE POSITIVES only) + +### Finding NM-001: [Title] +**Severity:** CRITICAL | HIGH | MEDIUM | LOW +**Source:** [Which auditor found it, or "Feedback Loop Step X"] +**Verification:** [Code trace / PoC / Hybrid] + +**Coupled Pair:** State A ↔ State B +**Invariant:** [What relationship must hold] + +**Feynman Question that exposed it:** +> [The exact question] + +**State Mapper gap that confirmed it:** +> [The mutation matrix entry showing the missing update] + +**Breaking Operation:** `functionName()` at `File.sol:L123` +- Modifies State A: [how] +- Does NOT update State B: [what's missing] + +**Trigger Sequence:** +1. [Step-by-step] +2. [Minimal adversarial sequence] + +**Consequence:** +- [What goes wrong downstream] +- [Concrete impact with numbers] + +**Masking Code** (if present): +```[language] +// This defensive code hides the broken invariant: +[code] +``` + +**Verification Evidence:** +[Code trace paths / PoC test output / concrete numbers] + +**Fix:** +```[language] +// Add the missing state synchronization: +[minimal fix] +``` + +--- + +## Feedback Loop Discoveries +[Findings that ONLY emerged from the cross-feed between auditors — +bugs that neither Feynman alone nor State Mapper alone would have found] + +## False Positives Eliminated +[Findings that failed verification, with explanation] + +## Downgraded Findings +[Findings where severity was reduced, with justification] + +## Summary +- Total functions analyzed: [N] +- Coupled state pairs mapped: [N] +- Nemesis loop iterations: [N] +- Raw findings (pre-verification): [N] C | [N] H | [N] M | [N] L +- Feedback loop discoveries: [N] (found ONLY via cross-feed) +- After verification: [N] TRUE POSITIVE | [N] FALSE POSITIVE | [N] DOWNGRADED +- Final: [N] CRITICAL | [N] HIGH | [N] MEDIUM | [N] LOW +``` + +Also save intermediate work to: `.audit/findings/nemesis-raw.md` + +--- + +## Red Flags Checklist (Combined) + +``` +FROM FEYNMAN: +- [ ] A line of code whose PURPOSE you cannot explain +- [ ] An ordering choice with no clear justification +- [ ] A guard on funcA that's missing from funcB (same state) +- [ ] An implicit trust assumption about caller/data/state/time +- [ ] External call with state updates AFTER it (stale state window) +- [ ] Function behaves differently on 2nd call due to 1st call's state change + +FROM STATE MAPPER: +- [ ] Function modifies State A but has no writes to coupled State B +- [ ] Two similar operations handle coupled state differently +- [ ] Claim/collect runs before reduce/remove with no reconciliation +- [ ] Partial operation exists but only full operation resets coupled state +- [ ] Defensive ternary/min() between two coupled values (WHY underflow?) +- [ ] delete/reset of one mapping but not its paired mapping +- [ ] Loop accumulates into shared state without per-iteration adjustment +- [ ] Emergency/admin function bypasses normal state update path + +FROM THE FEEDBACK LOOP: +- [ ] Feynman found an ordering concern + State Mapper found a gap in the + SAME function → compound finding +- [ ] State Mapper found masking code + Feynman explained WHY the invariant + is broken underneath → root cause finding +- [ ] Feynman found an assumption about state freshness + State Mapper + confirmed the state IS stale after a specific mutation path +- [ ] Both auditors flagged the SAME function from different angles + → highest confidence finding +``` + +--- + +## Severity Classification + +| Severity | Criteria | +|----------|----------| +| **CRITICAL** | Direct value loss, permanent DoS, or system insolvency. Exploitable now. | +| **HIGH** | Conditional value loss, privilege escalation, or broken core invariant | +| **MEDIUM** | Value leakage, griefing with cost, incorrect accounting, degraded functionality | +| **LOW** | Informational, cosmetic inconsistency, edge-case-only with no material impact | + +--- + +## Post-Audit Actions + +| Scenario | Action | +|----------|--------| +| Need deeper protocol context | Re-read the relevant contracts and documentation | +| Finding needs formal report | Write up with severity, trigger sequence, PoC, and fix | +| Need exploit validation | Write a Foundry/Hardhat PoC test to confirm | +| Uncertain about design intent | Check NatSpec, comments, and project documentation | + +--- + +## Anti-Hallucination Protocol + +``` +NEVER: +- Invent code that doesn't exist in the codebase +- Assume a coupled pair without finding code that reads BOTH values together +- Claim a function is missing an update without tracing its full call chain +- Report a finding without the exact code, trigger sequence, AND consequence +- Force Solidity terminology onto non-Solidity code +- Skip the feedback loop (Phase 4) — it's where the highest-value bugs emerge +- Present raw findings as verified results + +ALWAYS: +- Read actual code before questioning it +- Verify coupled pairs by finding code that reads BOTH values +- Trace internal calls for hidden updates (hooks, modifiers, base classes) +- Check for lazy reconciliation patterns before reporting stale state +- Show exact file paths and line numbers +- Run the feedback loop until convergence +- Present ONLY verified findings in the final report +``` + +--- + +## Quick-Start Checklist + +``` +- [ ] Phase 0: Attacker recon (goals, novel code, value stores, coupling hypothesis) +- [ ] Phase 1A: Build Function-State Matrix +- [ ] Phase 1B: Build Coupled State Dependency Map +- [ ] Phase 1C: Cross-reference → Unified Nemesis Map +- [ ] Phase 2: Feynman interrogation (all 7 categories, priority order) +- [ ] Phase 2: Feed all SUSPECT verdicts to Phase 3 +- [ ] Phase 3A: Build Mutation Matrix (enriched by Feynman suspects) +- [ ] Phase 3B: Parallel Path Comparison +- [ ] Phase 3C: Operation Ordering check +- [ ] Phase 3D: Feynman-Enriched Target analysis +- [ ] Phase 4: THE NEMESIS LOOP +- [ ] Step A: State gaps → Feynman re-interrogation +- [ ] Step B: Feynman findings → State dependency expansion +- [ ] Step C: Masking code → Joint interrogation +- [ ] Step D: Convergence check (loop if new findings, max 3 iterations) +- [ ] Phase 5: Multi-transaction journey tracing (adversarial sequences) +- [ ] Phase 6: Verify ALL C/H/M findings (code trace + PoC) +- [ ] Phase 6: Eliminate false positives +- [ ] Phase 7: Save to .audit/findings/nemesis-verified.md +- [ ] Phase 7: Present ONLY verified findings +``` diff --git a/.claude/skills/state-inconsistency-auditor/SKILL.md b/.claude/skills/state-inconsistency-auditor/SKILL.md new file mode 100644 index 00000000..39c3c94e --- /dev/null +++ b/.claude/skills/state-inconsistency-auditor/SKILL.md @@ -0,0 +1,517 @@ +--- +name: state-inconsistency-auditor +description: Finds state inconsistency bugs where an operation mutates one piece of coupled state without updating its dependent counterpart, causing silent data corruption or reverts in subsequent operations. Triggers on /state-audit, state inconsistency audit, or coupled state audit. +--- + +# State Inconsistency Auditor + +Finds bugs where an operation mutates one piece of coupled state without updating its dependent counterpart, causing silent data corruption or reverts in subsequent operations. + +**Language-agnostic by design.** Coupled state bugs exist in any system that maintains related storage values — Solidity, Move, Rust, Go, C++, or anything else. + +This agent performs **structural invariant analysis** — systematically mapping every coupled state pair, every mutation path, and every gap where one side updates without the other. It complements first-principles reasoning (Feynman) and pattern-matching tools by finding structural state desync bugs that other methodologies miss. + +## When to Activate + +- User says "/state-audit" or "state inconsistency audit" or "coupled state audit" +- User wants to find stale state, broken invariants, or desynchronized storage +- After any other audit methodology to catch what it missed + +## When NOT to Use + +- Quick pattern-matching scans where you only need known vulnerability patterns +- First-principles logic bugs only (use `/feynman` instead) +- Simple spec compliance checks +- Report generation from existing findings + +--- + +## The Abstract Pattern + +Every system has **COUPLED STATE PAIRS** — two or more storage values that must maintain a relationship (an invariant) with each other. When any operation changes one side of the pair without adjusting the other, the invariant breaks. Future operations that read both values produce incorrect results. + +Examples of coupled state: +- balance & checkpoint +- position size & accumulated tracker +- collateral & obligation +- voting power & snapshot +- shares & any per-share derived value +- principal & any cumulative index +- totalSupply & sum of all individual balances +- debt & interest accumulator +- liquidity & fee growth trackers +- stake amount & reward debt +- token balance & voting delegation +- position collateral & position health factor cache + +**The bug class:** Operation X correctly updates State A, but fails to proportionally adjust the coupled State B. State B is now stale relative to State A. + +--- + +## Language Adaptation + +When you start, **detect the language** and adapt terminology: + +| Concept | Solidity | Move | Rust | Go | C++ | +|---------|----------|------|------|----|-----| +| Storage | state variables | global storage / resources | struct fields / state | struct fields / DB | member variables | +| Mapping | mapping(k => v) | Table\ / SmartTable | HashMap / BTreeMap | map[K]V | std::map / unordered_map | +| Delete | delete mapping[key] | table::remove | map.remove(&key) | delete(map, key) | map.erase(key) | +| Event | emit Event() | event::emit() | emit! / log | EventEmit() | signal / callback | +| Internal call | internal function | friend function | pub(crate) fn | unexported func | private method | + +--- + +## Core Rules + +``` +RULE 0: MAP BEFORE YOU HUNT +Never start checking functions until you have the complete coupled state +dependency map. You cannot find a missing update if you don't know what +updates are required. + +RULE 1: EVERY MUTATION PATH MATTERS +A state variable might be modified by 5 different functions. ALL 5 must +update the coupled state. If 4 do and 1 doesn't — that's the bug. + +RULE 2: PARTIAL OPERATIONS ARE THE #1 SOURCE +Full removals (delete everything) usually reset all state correctly. +Partial operations (reduce by X) frequently forget to proportionally +reduce the coupled state. + +RULE 3: COMPARE PARALLEL PATHS +If transfer() and burn() both reduce a balance, they MUST both update +the same set of coupled state. If one does and the other doesn't — finding. + +RULE 4: DEFENSIVE CODE MASKS BUGS +Code like `x > y ? x - y : 0` or `min(computed, available)` silently +hides broken invariants. These are red flags, not safety nets. + +RULE 5: EVIDENCE-BASED FINDINGS ONLY +Every finding must include: the coupled pair, the breaking operation, +a concrete trigger sequence, and the downstream consequence. +``` + +--- + +## Audit Process + +### Phase 1: Map All Coupled State Pairs + +For every storage variable, ask: **"What other storage values must change when this one changes?"** + +Build a dependency map: + +``` +State A changes → State B MUST also change (and vice versa) +State C changes → State D and State E MUST also change +``` + +Look for: +- Any per-user value paired with any per-user accumulator/tracker +- Any balance paired with any historical snapshot or checkpoint +- Any numerator paired with its denominator +- Any position-describing value paired with any position-derived value +- Anything stored at time T that is later used with a value from time T+1 +- Any total/aggregate paired with individual components that sum to it +- Any cached computation paired with the inputs it was derived from +- Any index/accumulator paired with the last-known snapshot of that index + +**Output of Phase 1:** A Coupled State Dependency Map. + +``` +┌─────────────────────────────────────────────────────────────┐ +│ COUPLED STATE DEPENDENCY MAP │ +├─────────────────────────────────────────────────────────────┤ +│ │ +│ PAIR 1: userBalance[user] ↔ checkpoint[user] │ +│ Invariant: checkpoint must reflect balance at last update │ +│ Mutation points: deposit(), withdraw(), transfer(), burn() │ +│ │ +│ PAIR 2: totalStaked ↔ rewardPerTokenStored │ +│ Invariant: rewardPerToken must be updated before │ +│ totalStaked changes │ +│ Mutation points: stake(), unstake(), emergencyWithdraw() │ +│ │ +│ PAIR 3: position.collateral ↔ position.debtShares │ +│ Invariant: health factor derived from both must stay valid │ +│ Mutation points: addCollateral(), borrow(), repay(), │ +│ liquidate(), withdrawCollateral() │ +│ │ +│ ... │ +└─────────────────────────────────────────────────────────────┘ +``` + +--- + +### Phase 2: Find Every Operation That Mutates Each State + +For EACH state variable identified in Phase 1, list **every** function and code path that modifies it. Include: + +- **Direct writes**: `state = newValue` +- **Increments/decrements**: `state += delta`, `state -= delta` +- **Deletions**: `delete state`, `state = 0`, `state = default` +- **Indirect mutations**: calling a function that internally modifies it (e.g., `_mint()`, `_burn()`, `_transfer()`) +- **Implicit changes**: burning reduces balance via internal `_burn`, rebasing changes effective balance without explicit write +- **Batch operations**: loops or multicalls that modify state multiple times +- **External triggers**: callbacks, hooks, or oracle updates that modify state as a side effect + +**Output of Phase 2:** A Mutation Matrix. + +``` +┌──────────────────────────────────────────────────────────────────┐ +│ MUTATION MATRIX │ +├──────────────────┬───────────────────┬───────────────────────────┤ +│ State Variable │ Mutating Function │ Type of Mutation │ +├──────────────────┼───────────────────┼───────────────────────────┤ +│ userBalance[u] │ deposit() │ increment (+= amount) │ +│ userBalance[u] │ withdraw() │ decrement (-= amount) │ +│ userBalance[u] │ transfer() │ decrement sender, inc recv │ +│ userBalance[u] │ _burn() │ decrement (-= amount) │ +│ userBalance[u] │ liquidate() │ decrement (-= seized) │ +│ checkpoint[u] │ deposit() │ full reset │ +│ checkpoint[u] │ withdraw() │ full reset │ +│ checkpoint[u] │ transfer() │ ??? — CHECK THIS │ +│ checkpoint[u] │ _burn() │ ??? — CHECK THIS │ +│ checkpoint[u] │ liquidate() │ ??? — CHECK THIS │ +└──────────────────┴───────────────────┴───────────────────────────┘ +``` + +The `???` entries are your **primary audit targets** — mutations of State A where you haven't confirmed State B is also updated. + +--- + +### Phase 3: Cross-Check — The Core Audit + +For EVERY (operation, state variable) pair from Phase 2: + +> "This operation modifies State A. +> Does it ALSO update every coupled state that depends on A?" + +Check specifically: + +``` +□ Full removal (A → 0): Is every coupled state reset/cleared? +□ Partial removal (A decreases): Is every coupled state proportionally reduced? +□ Increase (A grows): Is every coupled state proportionally increased? +□ Transfer (A moves between entities): Is coupled state moved too? +□ Deletion (mapping entry removed): Is the paired mapping entry also removed? +□ Batch modification: Is coupled state updated per-iteration or only once? +``` + +**If ANY path updates A without updating its coupled state → FINDING.** + +For each potential finding, trace the FULL code path: +1. Read the function that modifies State A +2. Search for any write to State B within the same function +3. Search for any internal call that writes to State B +4. Search for any modifier/hook that writes to State B +5. If none found → confirmed finding + +--- + +### Phase 4: Check Operation Ordering Within Functions + +Many functions perform multiple state changes sequentially. Trace the exact order: + +``` +function doSomething() { + step1: reads State A and State B → computes result + step2: modifies State B based on result + step3: modifies State A + // State B is now stale relative to new State A +} +``` + +Ask at each step: +- "After this step, are ALL coupled pairs still consistent?" +- "Does step N use a value that step N-1 already invalidated?" +- "If I read the coupled pair RIGHT HERE, would the invariant hold?" +- "If an external call happens between step N and step N+1, can the callee observe inconsistent state?" + +**Common ordering bugs:** +- Claim rewards BEFORE reducing stake → rewards computed on old (higher) stake +- Update index AFTER modifying supply → index uses stale supply +- Read cached price AFTER changing position → health check uses wrong price +- Emit event with old values AFTER state change → off-chain systems desync + +--- + +### Phase 5: Compare Parallel Code Paths + +Find operations that achieve similar outcomes through different paths: + +- `transfer()` vs `burn()` — both reduce sender balance +- `withdraw()` vs `liquidate()` — both reduce position +- `partial` vs `full` removal — both decrease, different amounts +- Direct call vs routed-through-wrapper call +- Normal path vs emergency/admin path +- Single operation vs batch operation +- User-initiated vs keeper/bot-initiated + +For each group, compare: **do ALL paths update the same coupled state?** + +``` +┌────────────────────────────────────────────────────────────┐ +│ PARALLEL PATH COMPARISON │ +├─────────────────┬──────────────┬──────────────┬────────────┤ +│ Coupled State │ withdraw() │ liquidate() │ emergencyW │ +├─────────────────┼──────────────┼──────────────┼────────────┤ +│ balance │ ✓ updated │ ✓ updated │ ✓ updated │ +│ checkpoint │ ✓ updated │ ✗ MISSING │ ✗ MISSING │ +│ totalSupply │ ✓ updated │ ✓ updated │ ✗ MISSING │ +│ rewardDebt │ ✓ updated │ ✗ MISSING │ ✗ MISSING │ +└─────────────────┴──────────────┴──────────────┴────────────┘ + +FINDINGS: liquidate() and emergencyWithdraw() don't update checkpoint + or rewardDebt when reducing balance. +``` + +If Path A adjusts the coupled state but Path B doesn't → **FINDING**. + +--- + +### Phase 6: Trace Multi-Step User Journeys + +Simulate sequences where a user interacts multiple times: + +``` +1. User enters a position (state initialized) +2. Time passes / external state evolves (index grows, prices change) +3. User does PARTIAL modification (coupled state may break here) +4. More time passes / external state evolves +5. User does another operation reading the coupled state +``` + +At step 5, ask: +- "Is the coupled state still valid given the partial change at step 3?" +- "Does the computation use stale State B with current State A?" +- "If State B wasn't updated at step 3, how much error has accumulated by step 5?" + +**Key sequences to test:** +- Deposit → partial withdraw → claim rewards (rewards on correct amount?) +- Stake → unstake half → restake → unstake all (reward debt correct?) +- Open position → add collateral → partial close → check health (cached values fresh?) +- Delegate votes → transfer tokens → vote (voting power reflects current balance?) +- Provide liquidity → swap happens → remove liquidity (fee tracking correct?) + +--- + +### Phase 7: Check What Masks the Bug + +Look for defensive code that HIDES broken invariants: + +``` +MASKING PATTERN 1: Ternary clamp + x > y ? x - y : 0 + → WHY would x ever be less than y? If the invariant held, it wouldn't. + This silently returns 0 instead of reverting on the broken state. + +MASKING PATTERN 2: Try/catch swallowing + try target.call() {} catch {} + → The revert from broken state is caught and ignored. + +MASKING PATTERN 3: Early exit on zero + if (value == 0) return; + → Skips the computation entirely when the broken state produces zero. + +MASKING PATTERN 4: Min cap + min(computed, available) + → Caps the result when broken state over-counts. The over-counting + is the bug; the min() just prevents the revert. + +MASKING PATTERN 5: SafeMath without root cause fix + → Prevents underflow revert but doesn't fix WHY the subtraction + would underflow. The state is still inconsistent. + +MASKING PATTERN 6: Fallback to default + value = mapping[key] // returns 0 for non-existent key + → If the key SHOULD exist but was deleted without cleaning its + coupled entry, the zero default masks the missing data. +``` + +**These patterns convert what SHOULD be a loud failure into a silent one.** The invariant is still broken — the symptom is just suppressed. Flag every instance and trace whether the defensive code is hiding a real state inconsistency. + +--- + +## Red Flags Checklist + +``` +- [ ] A function modifies a base value but has no writes to its coupled state +- [ ] Two similar operations (e.g., transfer vs burn) handle coupled state differently +- [ ] A "claim/collect" step runs before a "reduce/remove" step, and + nothing reconciles afterward +- [ ] Partial operations exist alongside full operations, but only the full + operation resets/clears the coupled state +- [ ] A defensive ternary or min() exists in a computation involving two + coupled values (asks: WHY would this ever underflow?) +- [ ] delete or reset of one mapping but not its paired mapping +- [ ] A loop processes multiple sub-positions but accumulates into a + shared coupled value without per-iteration adjustment +- [ ] An emergency/admin function bypasses the normal state update path +- [ ] A migration or upgrade function copies State A but not State B +- [ ] A callback or hook modifies State A but the caller doesn't know + to update State B afterward +``` + +--- + +## Phase 8: Verification Gate (MANDATORY) + +**Every CRITICAL, HIGH, and MEDIUM finding MUST be verified before final report.** + +### Verification Methods: + +**Method A: Code Trace Verification** +1. Read the exact function that breaks the invariant +2. Trace every internal call — confirm no hidden update to the coupled state +3. Check modifiers, hooks, and base class overrides +4. Confirm no event-driven or callback-based reconciliation exists +5. Verdict: TRUE POSITIVE / FALSE POSITIVE + +**Method B: PoC Test Verification** +1. Write a test using the project's native framework +2. Execute the trigger sequence from the finding +3. Assert that the coupled state is inconsistent after the operation +4. Assert that a subsequent operation produces incorrect results +5. If test passes: TRUE POSITIVE. If test fails: FALSE POSITIVE + +**Method C: Hybrid (trace + PoC)** +For complex multi-contract findings spanning multiple modules. + +### Common False Positive Patterns: + +1. **Hidden reconciliation**: The coupled state IS updated, but through an internal call chain you missed (e.g., `_beforeTokenTransfer` hook). +2. **Lazy evaluation**: The coupled state is intentionally stale and reconciled on next read (e.g., `_updateReward()` modifier runs before every function). +3. **Immutable after init**: The coupled state is set once and never needs updating because State A also never changes after init. +4. **Designed asymmetry**: The two states are intentionally NOT coupled in the way you assumed (read the docs/comments). + +--- + +## Severity Classification + +| Severity | Criteria | +|----------|----------| +| **CRITICAL** | Coupled state desync causes direct value loss (wrong payouts, stolen funds, permanent lock) | +| **HIGH** | Coupled state desync causes conditional value loss or broken core functionality | +| **MEDIUM** | Coupled state desync causes incorrect accounting, griefing, or degraded functionality | +| **LOW** | Coupled state desync causes cosmetic issues, event inaccuracy, or edge-case-only errors | + +--- + +## Output Format + +Save raw findings to: `.audit/findings/state-inconsistency-raw.md` +Save verified findings to: `.audit/findings/state-inconsistency-verified.md` + +```markdown +# State Inconsistency Audit — Verified Findings + +## Coupled State Dependency Map +[The map from Phase 1] + +## Mutation Matrix +[The matrix from Phase 2] + +## Parallel Path Comparison +[The comparison table from Phase 5] + +## Verification Summary +| ID | Coupled Pair | Breaking Op | Original Severity | Verdict | Final Severity | +|----|-------------|-------------|-------------------|---------|----------------| + +## Verified Findings + +### Finding SI-001: [Title] +**Severity:** CRITICAL | HIGH | MEDIUM | LOW +**Verification:** [Code trace / PoC / Hybrid] + +**Coupled Pair:** State A ↔ State B +**Invariant:** [What relationship must hold between them] + +**Breaking Operation:** `functionName()` in `Contract.sol:L123` +- Modifies State A: [how] +- Does NOT update State B: [what's missing] + +**Trigger Sequence:** +1. [Step-by-step minimal sequence to break the invariant] + +**Consequence:** +- [What goes wrong when a later operation reads both A and B] +- [Concrete impact: wrong payout amount, locked funds, etc.] + +**Masking Code** (if present): +```[language] +// This defensive code hides the broken invariant: +[the masking pattern] +``` + +**Fix:** +```[language] +// Add the missing state synchronization: +[minimal fix] +``` + +--- + +## False Positives Eliminated +[Findings that failed verification, with explanation] + +## Summary +- Coupled state pairs mapped: [N] +- Mutation paths analyzed: [N] +- Raw findings (pre-verification): [N] +- After verification: [N] TRUE POSITIVE | [N] FALSE POSITIVE +- Final: [N] CRITICAL | [N] HIGH | [N] MEDIUM | [N] LOW +``` + +--- + +## Post-Audit Actions + +| Scenario | Action | +|----------|--------| +| Need deeper context on a function | Re-read the function and its callers line-by-line | +| Finding confirmed as true positive | Write up with severity, trigger sequence, PoC, and fix | +| Need first-principles reasoning on a pair | Run `/feynman` on the specific functions involved | +| Need exploit validation | Write a Foundry/Hardhat PoC test to confirm | +| Uncertain about design intent | Check NatSpec, comments, and project documentation | + +--- + +## Anti-Hallucination Protocol + +``` +NEVER: +- Assume two states are coupled without verifying they are read together +- Claim a function is missing an update without reading its full call chain +- Report a finding without showing the exact code that breaks the invariant +- Ignore lazy-evaluation patterns (modifiers that reconcile on entry) +- Assume a mapping deletion is a bug without checking if the paired mapping + is also deleted or intentionally kept + +ALWAYS: +- Read the actual storage declarations to understand types and relationships +- Trace internal calls to check for hidden updates +- Check _before/_after hooks and modifiers for reconciliation logic +- Verify the coupled relationship by finding code that reads BOTH values together +- Show exact file paths and line numbers for all references +``` + +--- + +## Quick-Start Checklist + +- [ ] **Phase 1:** Map all storage variables and their coupled dependencies +- [ ] **Phase 1:** Build the Coupled State Dependency Map +- [ ] **Phase 2:** For each state variable, list every mutating function +- [ ] **Phase 2:** Build the Mutation Matrix (mark `???` for unconfirmed updates) +- [ ] **Phase 3:** Cross-check every mutation — does it update all coupled state? +- [ ] **Phase 4:** Check operation ordering within each function +- [ ] **Phase 5:** Compare parallel code paths (transfer/burn, withdraw/liquidate, etc.) +- [ ] **Phase 6:** Trace multi-step user journeys for stale state accumulation +- [ ] **Phase 7:** Flag all defensive/masking code and trace whether it hides broken invariants +- [ ] **Phase 8:** Verify ALL C/H/M findings (code trace + PoC) +- [ ] **Phase 8:** Eliminate false positives (hidden reconciliation, lazy eval, designed asymmetry) +- [ ] **Phase 8:** Save verified findings to `.audit/findings/state-inconsistency-verified.md` +- [ ] **Phase 8:** Present ONLY verified report to the user diff --git a/contracts/mTokenMinBalance.sol b/contracts/mTokenMinBalance.sol index 629f0b38..31630697 100644 --- a/contracts/mTokenMinBalance.sol +++ b/contracts/mTokenMinBalance.sol @@ -12,14 +12,14 @@ import "./mToken.sol"; abstract contract mTokenMinBalance is mToken { /** * @param user address of the user - * @param isFree bool if the user is free from min balance checks + * @param isExempt bool if the user is exempt from min balance checks */ - event SetIsFreeFromMinBalance(address indexed user, bool isFree); + event SetIsMinBalanceExempt(address indexed user, bool isExempt); /** - * @notice mapping, user address => is free from min balance checks + * @notice mapping, user address => is exempt from min balance checks */ - mapping(address => bool) public isFreeFromMinBalance; + mapping(address => bool) public isMinBalanceExempt; /** * @dev leaving a storage gap for futures updates @@ -27,20 +27,20 @@ abstract contract mTokenMinBalance is mToken { uint256[50] private __gap; /** - * @notice set if a user is free from min balance checks + * @notice set if a user is exempt from min balance checks * @param user address of the user - * @param isFree bool if the user is free from min balance checks + * @param isExempt bool if the user is exempt from min balance checks */ - function setIsFreeFromMinBalance(address user, bool isFree) + function setIsMinBalanceExempt(address user, bool isExempt) external onlyRole(DEFAULT_ADMIN_ROLE, msg.sender) { - if (isFreeFromMinBalance[user] == isFree) { + if (isMinBalanceExempt[user] == isExempt) { return; } - isFreeFromMinBalance[user] = isFree; - emit SetIsFreeFromMinBalance(user, isFree); + isMinBalanceExempt[user] = isExempt; + emit SetIsMinBalanceExempt(user, isExempt); } /** @@ -51,11 +51,11 @@ abstract contract mTokenMinBalance is mToken { address to, uint256 amount ) internal virtual override { - if (from != address(0) && !isFreeFromMinBalance[from]) { + if (from != address(0) && !isMinBalanceExempt[from]) { _validateMinBalance(from, true); } - if (to != address(0) && !isFreeFromMinBalance[to]) { + if (to != address(0) && !isMinBalanceExempt[to]) { _validateMinBalance(to, from != address(0)); } diff --git a/test/unit/mtoken.test.ts b/test/unit/mtoken.test.ts index 64bb3e53..89c6582c 100644 --- a/test/unit/mtoken.test.ts +++ b/test/unit/mtoken.test.ts @@ -454,7 +454,7 @@ describe('Token contracts', () => { }); describe('mTokenMinBalance (mTokenMinBalanceTest)', () => { - describe('setIsFreeFromMinBalance()', () => { + describe('setIsMinBalanceExempt()', () => { it('should fail: call from address without DEFAULT_ADMIN_ROLE', async () => { const baseFixture = await defaultDeploy(); const { regularAccounts, mTokenMinBalance } = await loadFixture( @@ -464,11 +464,11 @@ describe('Token contracts', () => { await expect( mTokenMinBalance .connect(regularAccounts[0]) - .setIsFreeFromMinBalance(regularAccounts[1].address, true), + .setIsMinBalanceExempt(regularAccounts[1].address, true), ).revertedWith(acErrors.WMAC_HASNT_ROLE); }); - it('set isFreeFromMinBalance to true', async () => { + it('set isMinBalanceExempt to true', async () => { const baseFixture = await defaultDeploy(); const { owner, regularAccounts, mTokenMinBalance } = await loadFixture( mTokenMinBalanceFixture.bind(this, baseFixture), @@ -478,17 +478,17 @@ describe('Token contracts', () => { await expect( mTokenMinBalance .connect(owner) - .setIsFreeFromMinBalance(user.address, true), + .setIsMinBalanceExempt(user.address, true), ) - .to.emit(mTokenMinBalance, 'SetIsFreeFromMinBalance') + .to.emit(mTokenMinBalance, 'SetIsMinBalanceExempt') .withArgs(user.address, true); - expect(await mTokenMinBalance.isFreeFromMinBalance(user.address)).eq( + expect(await mTokenMinBalance.isMinBalanceExempt(user.address)).eq( true, ); }); - it('set isFreeFromMinBalance to false', async () => { + it('set isMinBalanceExempt to false', async () => { const baseFixture = await defaultDeploy(); const { owner, regularAccounts, mTokenMinBalance } = await loadFixture( mTokenMinBalanceFixture.bind(this, baseFixture), @@ -497,17 +497,17 @@ describe('Token contracts', () => { const user = regularAccounts[0]; await mTokenMinBalance .connect(owner) - .setIsFreeFromMinBalance(user.address, true); + .setIsMinBalanceExempt(user.address, true); await expect( mTokenMinBalance .connect(owner) - .setIsFreeFromMinBalance(user.address, false), + .setIsMinBalanceExempt(user.address, false), ) - .to.emit(mTokenMinBalance, 'SetIsFreeFromMinBalance') + .to.emit(mTokenMinBalance, 'SetIsMinBalanceExempt') .withArgs(user.address, false); - expect(await mTokenMinBalance.isFreeFromMinBalance(user.address)).eq( + expect(await mTokenMinBalance.isMinBalanceExempt(user.address)).eq( false, ); }); @@ -522,8 +522,8 @@ describe('Token contracts', () => { await expect( mTokenMinBalance .connect(owner) - .setIsFreeFromMinBalance(user.address, false), - ).to.not.emit(mTokenMinBalance, 'SetIsFreeFromMinBalance'); + .setIsMinBalanceExempt(user.address, false), + ).to.not.emit(mTokenMinBalance, 'SetIsMinBalanceExempt'); }); }); @@ -599,7 +599,7 @@ describe('Token contracts', () => { ); await mTokenMinBalance .connect(owner) - .setIsFreeFromMinBalance(to.address, true); + .setIsMinBalanceExempt(to.address, true); await expect( mTokenMinBalance.connect(from).transfer(to.address, amount), @@ -618,7 +618,7 @@ describe('Token contracts', () => { await mTokenMinBalance .connect(owner) - .setIsFreeFromMinBalance(from.address, true); + .setIsMinBalanceExempt(from.address, true); await mint( { tokenContract: mTokenMinBalance, owner }, from, @@ -699,7 +699,7 @@ describe('Token contracts', () => { ); await mTokenMinBalance .connect(owner) - .setIsFreeFromMinBalance(from.address, true); + .setIsMinBalanceExempt(from.address, true); await expect( mTokenMinBalance @@ -991,7 +991,7 @@ describe('Token contracts', () => { const to = regularAccounts[0]; await mTokenMinBalance .connect(owner) - .setIsFreeFromMinBalance(to.address, true); + .setIsMinBalanceExempt(to.address, true); await mint( { tokenContract: mTokenMinBalance, owner }, @@ -1116,7 +1116,7 @@ describe('Token contracts', () => { ); await mTokenMinBalance .connect(owner) - .setIsFreeFromMinBalance(holder.address, true); + .setIsMinBalanceExempt(holder.address, true); await burn( { tokenContract: mTokenMinBalance, owner }, @@ -1319,7 +1319,7 @@ describe('Token contracts', () => { ); await mTokenPermissionedMinBalance .connect(owner) - .setIsFreeFromMinBalance(to.address, true); + .setIsMinBalanceExempt(to.address, true); await expect( mTokenPermissionedMinBalance @@ -1351,7 +1351,7 @@ describe('Token contracts', () => { await accessControl.grantRole(greenlisted, to.address); await mTokenPermissionedMinBalance .connect(owner) - .setIsFreeFromMinBalance(from.address, true); + .setIsMinBalanceExempt(from.address, true); await mint( { tokenContract: mTokenPermissionedMinBalance, owner }, from, From bbfe232b40fbad8a70814505d1598eb36b80402a Mon Sep 17 00:00:00 2001 From: kostyamospan Date: Wed, 15 Jul 2026 13:01:44 +0300 Subject: [PATCH 04/19] fix: migrated to roles based exempt list from mapping --- contracts/mTokenMinBalance.sol | 48 ++--- contracts/mTokenPermissionedMinBalance.sol | 4 +- contracts/testers/mTokenMinBalanceTest.sol | 7 + .../mTokenPermissionedMinBalanceTest.sol | 7 + helpers/roles.ts | 2 + .../common/templates/mtoken.template.ts | 24 +++ test/common/fixtures.ts | 6 + test/unit/mtoken.test.ts | 172 +++++++----------- 8 files changed, 126 insertions(+), 144 deletions(-) diff --git a/contracts/mTokenMinBalance.sol b/contracts/mTokenMinBalance.sol index 31630697..22026862 100644 --- a/contracts/mTokenMinBalance.sol +++ b/contracts/mTokenMinBalance.sol @@ -10,39 +10,11 @@ import "./mToken.sol"; */ //solhint-disable contract-name-camelcase abstract contract mTokenMinBalance is mToken { - /** - * @param user address of the user - * @param isExempt bool if the user is exempt from min balance checks - */ - event SetIsMinBalanceExempt(address indexed user, bool isExempt); - - /** - * @notice mapping, user address => is exempt from min balance checks - */ - mapping(address => bool) public isMinBalanceExempt; - /** * @dev leaving a storage gap for futures updates */ uint256[50] private __gap; - /** - * @notice set if a user is exempt from min balance checks - * @param user address of the user - * @param isExempt bool if the user is exempt from min balance checks - */ - function setIsMinBalanceExempt(address user, bool isExempt) - external - onlyRole(DEFAULT_ADMIN_ROLE, msg.sender) - { - if (isMinBalanceExempt[user] == isExempt) { - return; - } - - isMinBalanceExempt[user] = isExempt; - emit SetIsMinBalanceExempt(user, isExempt); - } - /** * @dev overrides _afterTokenTransfer function to check if the recipient has a minimum balance */ @@ -51,23 +23,37 @@ abstract contract mTokenMinBalance is mToken { address to, uint256 amount ) internal virtual override { - if (from != address(0) && !isMinBalanceExempt[from]) { + if (from != address(0) && !_isMinBalanceExempt(from)) { _validateMinBalance(from, true); } - if (to != address(0) && !isMinBalanceExempt[to]) { + if (to != address(0) && !_isMinBalanceExempt(to)) { _validateMinBalance(to, from != address(0)); } 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 * @param canBeZero bool if the user can have a balance of 0 */ - function _validateMinBalance(address user, bool canBeZero) internal view { + function _validateMinBalance(address user, bool canBeZero) private view { uint256 balance = balanceOf(user); bool isMinBalanceMet = balance >= 1 ether; diff --git a/contracts/mTokenPermissionedMinBalance.sol b/contracts/mTokenPermissionedMinBalance.sol index 9a22d767..c4982605 100644 --- a/contracts/mTokenPermissionedMinBalance.sol +++ b/contracts/mTokenPermissionedMinBalance.sol @@ -11,8 +11,8 @@ import "./mTokenMinBalance.sol"; */ //solhint-disable contract-name-camelcase abstract contract mTokenPermissionedMinBalance is - mTokenPermissioned, - mTokenMinBalance + mTokenMinBalance, + mTokenPermissioned { /** * @dev leaving a storage gap for futures updates diff --git a/contracts/testers/mTokenMinBalanceTest.sol b/contracts/testers/mTokenMinBalanceTest.sol index e67c8516..bece14c1 100644 --- a/contracts/testers/mTokenMinBalanceTest.sol +++ b/contracts/testers/mTokenMinBalanceTest.sol @@ -14,6 +14,9 @@ contract mTokenMinBalanceTest is mTokenMinBalance { bytes32 public constant M_TOKEN_TEST_PAUSE_OPERATOR_ROLE = keccak256("M_TOKEN_TEST_PAUSE_OPERATOR_ROLE"); + bytes32 public constant M_TOKEN_TEST_MIN_BALANCE_EXEMPT_ROLE = + keccak256("M_TOKEN_TEST_MIN_BALANCE_EXEMPT_ROLE"); + function _disableInitializers() internal override {} function _getNameSymbol() @@ -36,4 +39,8 @@ contract mTokenMinBalanceTest is mTokenMinBalance { function _pauserRole() internal pure override returns (bytes32) { return M_TOKEN_TEST_PAUSE_OPERATOR_ROLE; } + + function _minBalanceExemptRole() internal pure override returns (bytes32) { + return M_TOKEN_TEST_MIN_BALANCE_EXEMPT_ROLE; + } } diff --git a/contracts/testers/mTokenPermissionedMinBalanceTest.sol b/contracts/testers/mTokenPermissionedMinBalanceTest.sol index 826b0284..0a5b9244 100644 --- a/contracts/testers/mTokenPermissionedMinBalanceTest.sol +++ b/contracts/testers/mTokenPermissionedMinBalanceTest.sol @@ -17,6 +17,9 @@ contract mTokenPermissionedMinBalanceTest is mTokenPermissionedMinBalance { bytes32 public constant M_TOKEN_TEST_GREENLISTED_ROLE = keccak256("M_TOKEN_TEST_GREENLISTED_ROLE"); + bytes32 public constant M_TOKEN_TEST_MIN_BALANCE_EXEMPT_ROLE = + keccak256("M_TOKEN_TEST_MIN_BALANCE_EXEMPT_ROLE"); + function _disableInitializers() internal override {} function _getNameSymbol() @@ -46,4 +49,8 @@ contract mTokenPermissionedMinBalanceTest is mTokenPermissionedMinBalance { function _greenlistedRole() internal pure override returns (bytes32) { return M_TOKEN_TEST_GREENLISTED_ROLE; } + + function _minBalanceExemptRole() internal pure override returns (bytes32) { + return M_TOKEN_TEST_MIN_BALANCE_EXEMPT_ROLE; + } } diff --git a/helpers/roles.ts b/helpers/roles.ts index 2775c6e9..b5c97376 100644 --- a/helpers/roles.ts +++ b/helpers/roles.ts @@ -134,6 +134,7 @@ type TokenRoles = { redemptionVaultAdmin: string; customFeedAdmin: string | null; greenlisted: string; + minBalanceExempt: string; }; type CommonRoles = { @@ -175,6 +176,7 @@ export const getRolesNamesForToken = (token: MTokenName): TokenRoles => { depositVaultAdmin: `${restPrefix}DEPOSIT_VAULT_ADMIN_ROLE`, redemptionVaultAdmin: `${restPrefix}REDEMPTION_VAULT_ADMIN_ROLE`, greenlisted: getGreenlistRoleName(token), + minBalanceExempt: `${tokenPrefix}_MIN_BALANCE_EXEMPT_ROLE`, }; }; export const getRolesNamesCommon = (): CommonRoles => { diff --git a/scripts/deploy/codegen/common/templates/mtoken.template.ts b/scripts/deploy/codegen/common/templates/mtoken.template.ts index aea3c22c..d4d304f0 100644 --- a/scripts/deploy/codegen/common/templates/mtoken.template.ts +++ b/scripts/deploy/codegen/common/templates/mtoken.template.ts @@ -63,6 +63,18 @@ export const getTokenContractFromTemplate = async ( bytes32 public constant ${roles.pauser} = keccak256("${roles.pauser}"); + ${ + isMinBalanceMToken + ? ` + /** + * @notice actor that is exempt from ${contractNames.token} min balance checks + */ + bytes32 public constant ${roles.minBalanceExempt} = + keccak256("${roles.minBalanceExempt}"); + ` + : '' + } + /** * @dev leaving a storage gap for futures updates */ @@ -112,6 +124,18 @@ export const getTokenContractFromTemplate = async ( }` : '' } + + ${ + isMinBalanceMToken + ? ` + /** + * @inheritdoc mTokenMinBalance + */ + function _minBalanceExemptRole() internal pure override returns (bytes32) { + return ${roles.minBalanceExempt}; + }` + : '' + } }`, }; }; diff --git a/test/common/fixtures.ts b/test/common/fixtures.ts index f4b99982..d9229279 100644 --- a/test/common/fixtures.ts +++ b/test/common/fixtures.ts @@ -1004,6 +1004,8 @@ export const mTokenMinBalanceFixture = async ( const mintRole = await mTokenMinBalance.M_TOKEN_TEST_MINT_OPERATOR_ROLE(); const burnRole = await mTokenMinBalance.M_TOKEN_TEST_BURN_OPERATOR_ROLE(); const pauseRole = await mTokenMinBalance.M_TOKEN_TEST_PAUSE_OPERATOR_ROLE(); + const minBalanceExemptRole = + await mTokenMinBalance.M_TOKEN_TEST_MIN_BALANCE_EXEMPT_ROLE(); await accessControl.grantRole(mintRole, owner.address); await accessControl.grantRole(burnRole, owner.address); @@ -1016,6 +1018,7 @@ export const mTokenMinBalanceFixture = async ( mint: mintRole, burn: burnRole, pause: pauseRole, + minBalanceExempt: minBalanceExemptRole, }, }; }; @@ -1041,6 +1044,8 @@ export const mTokenPermissionedMinBalanceFixture = async ( await mTokenPermissionedMinBalance.M_TOKEN_TEST_PAUSE_OPERATOR_ROLE(); const greenlistedRole = await mTokenPermissionedMinBalance.M_TOKEN_TEST_GREENLISTED_ROLE(); + const minBalanceExemptRole = + await mTokenPermissionedMinBalance.M_TOKEN_TEST_MIN_BALANCE_EXEMPT_ROLE(); await accessControl.grantRole(mintRole, owner.address); await accessControl.grantRole(burnRole, owner.address); @@ -1054,6 +1059,7 @@ export const mTokenPermissionedMinBalanceFixture = async ( burn: burnRole, pause: pauseRole, greenlisted: greenlistedRole, + minBalanceExempt: minBalanceExemptRole, }, }; }; diff --git a/test/unit/mtoken.test.ts b/test/unit/mtoken.test.ts index 89c6582c..ec3fda14 100644 --- a/test/unit/mtoken.test.ts +++ b/test/unit/mtoken.test.ts @@ -454,79 +454,6 @@ describe('Token contracts', () => { }); describe('mTokenMinBalance (mTokenMinBalanceTest)', () => { - describe('setIsMinBalanceExempt()', () => { - it('should fail: call from address without DEFAULT_ADMIN_ROLE', async () => { - const baseFixture = await defaultDeploy(); - const { regularAccounts, mTokenMinBalance } = await loadFixture( - mTokenMinBalanceFixture.bind(this, baseFixture), - ); - - await expect( - mTokenMinBalance - .connect(regularAccounts[0]) - .setIsMinBalanceExempt(regularAccounts[1].address, true), - ).revertedWith(acErrors.WMAC_HASNT_ROLE); - }); - - it('set isMinBalanceExempt to true', async () => { - const baseFixture = await defaultDeploy(); - const { owner, regularAccounts, mTokenMinBalance } = await loadFixture( - mTokenMinBalanceFixture.bind(this, baseFixture), - ); - - const user = regularAccounts[0]; - await expect( - mTokenMinBalance - .connect(owner) - .setIsMinBalanceExempt(user.address, true), - ) - .to.emit(mTokenMinBalance, 'SetIsMinBalanceExempt') - .withArgs(user.address, true); - - expect(await mTokenMinBalance.isMinBalanceExempt(user.address)).eq( - true, - ); - }); - - it('set isMinBalanceExempt to false', async () => { - const baseFixture = await defaultDeploy(); - const { owner, regularAccounts, mTokenMinBalance } = await loadFixture( - mTokenMinBalanceFixture.bind(this, baseFixture), - ); - - const user = regularAccounts[0]; - await mTokenMinBalance - .connect(owner) - .setIsMinBalanceExempt(user.address, true); - - await expect( - mTokenMinBalance - .connect(owner) - .setIsMinBalanceExempt(user.address, false), - ) - .to.emit(mTokenMinBalance, 'SetIsMinBalanceExempt') - .withArgs(user.address, false); - - expect(await mTokenMinBalance.isMinBalanceExempt(user.address)).eq( - false, - ); - }); - - it('no-op when value is unchanged', async () => { - const baseFixture = await defaultDeploy(); - const { owner, regularAccounts, mTokenMinBalance } = await loadFixture( - mTokenMinBalanceFixture.bind(this, baseFixture), - ); - - const user = regularAccounts[0]; - await expect( - mTokenMinBalance - .connect(owner) - .setIsMinBalanceExempt(user.address, false), - ).to.not.emit(mTokenMinBalance, 'SetIsMinBalanceExempt'); - }); - }); - describe('transfer()', () => { it('transfer when both parties hold above min balance after transfer', async () => { const baseFixture = await defaultDeploy(); @@ -584,9 +511,13 @@ describe('Token contracts', () => { it('transfer dust to empty recipient when recipient is free from min balance', async () => { const baseFixture = await defaultDeploy(); - const { owner, regularAccounts, mTokenMinBalance } = await loadFixture( - mTokenMinBalanceFixture.bind(this, baseFixture), - ); + const { + owner, + accessControl, + regularAccounts, + mTokenMinBalance, + mTokenMinBalanceRoles, + } = await loadFixture(mTokenMinBalanceFixture.bind(this, baseFixture)); const from = regularAccounts[0]; const to = regularAccounts[1]; @@ -597,9 +528,10 @@ describe('Token contracts', () => { from, parseUnits('3'), ); - await mTokenMinBalance - .connect(owner) - .setIsMinBalanceExempt(to.address, true); + await accessControl.grantRole( + mTokenMinBalanceRoles.minBalanceExempt, + to.address, + ); await expect( mTokenMinBalance.connect(from).transfer(to.address, amount), @@ -609,16 +541,21 @@ describe('Token contracts', () => { it('should fail: transfer dust from waived sender to empty non-waived recipient', async () => { const baseFixture = await defaultDeploy(); - const { owner, regularAccounts, mTokenMinBalance } = await loadFixture( - mTokenMinBalanceFixture.bind(this, baseFixture), - ); + const { + owner, + accessControl, + regularAccounts, + mTokenMinBalance, + mTokenMinBalanceRoles, + } = await loadFixture(mTokenMinBalanceFixture.bind(this, baseFixture)); const from = regularAccounts[0]; const to = regularAccounts[1]; - await mTokenMinBalance - .connect(owner) - .setIsMinBalanceExempt(from.address, true); + await accessControl.grantRole( + mTokenMinBalanceRoles.minBalanceExempt, + from.address, + ); await mint( { tokenContract: mTokenMinBalance, owner }, from, @@ -680,9 +617,13 @@ describe('Token contracts', () => { it('transfer leaving sender below min balance when sender is free from min balance', async () => { const baseFixture = await defaultDeploy(); - const { owner, regularAccounts, mTokenMinBalance } = await loadFixture( - mTokenMinBalanceFixture.bind(this, baseFixture), - ); + const { + owner, + accessControl, + regularAccounts, + mTokenMinBalance, + mTokenMinBalanceRoles, + } = await loadFixture(mTokenMinBalanceFixture.bind(this, baseFixture)); const from = regularAccounts[0]; const to = regularAccounts[1]; @@ -697,9 +638,10 @@ describe('Token contracts', () => { to, parseUnits('1'), ); - await mTokenMinBalance - .connect(owner) - .setIsMinBalanceExempt(from.address, true); + await accessControl.grantRole( + mTokenMinBalanceRoles.minBalanceExempt, + from.address, + ); await expect( mTokenMinBalance @@ -984,14 +926,19 @@ describe('Token contracts', () => { it('mint less than 1 token to empty recipient when free from min balance', async () => { const baseFixture = await defaultDeploy(); - const { owner, regularAccounts, mTokenMinBalance } = await loadFixture( - mTokenMinBalanceFixture.bind(this, baseFixture), - ); + const { + owner, + accessControl, + regularAccounts, + mTokenMinBalance, + mTokenMinBalanceRoles, + } = await loadFixture(mTokenMinBalanceFixture.bind(this, baseFixture)); const to = regularAccounts[0]; - await mTokenMinBalance - .connect(owner) - .setIsMinBalanceExempt(to.address, true); + await accessControl.grantRole( + mTokenMinBalanceRoles.minBalanceExempt, + to.address, + ); await mint( { tokenContract: mTokenMinBalance, owner }, @@ -1104,9 +1051,13 @@ describe('Token contracts', () => { it('burn leaving holder below min balance when free from min balance', async () => { const baseFixture = await defaultDeploy(); - const { owner, regularAccounts, mTokenMinBalance } = await loadFixture( - mTokenMinBalanceFixture.bind(this, baseFixture), - ); + const { + owner, + accessControl, + regularAccounts, + mTokenMinBalance, + mTokenMinBalanceRoles, + } = await loadFixture(mTokenMinBalanceFixture.bind(this, baseFixture)); const holder = regularAccounts[0]; await mint( @@ -1114,9 +1065,10 @@ describe('Token contracts', () => { holder, parseUnits('3'), ); - await mTokenMinBalance - .connect(owner) - .setIsMinBalanceExempt(holder.address, true); + await accessControl.grantRole( + mTokenMinBalanceRoles.minBalanceExempt, + holder.address, + ); await burn( { tokenContract: mTokenMinBalance, owner }, @@ -1307,7 +1259,8 @@ describe('Token contracts', () => { const from = regularAccounts[0]; const to = regularAccounts[1]; - const { greenlisted } = mTokenPermissionedMinBalanceRoles; + const { greenlisted, minBalanceExempt } = + mTokenPermissionedMinBalanceRoles; const amount = parseUnits('0.1'); await accessControl.grantRole(greenlisted, from.address); @@ -1317,9 +1270,7 @@ describe('Token contracts', () => { from, parseUnits('3'), ); - await mTokenPermissionedMinBalance - .connect(owner) - .setIsMinBalanceExempt(to.address, true); + await accessControl.grantRole(minBalanceExempt, to.address); await expect( mTokenPermissionedMinBalance @@ -1345,13 +1296,12 @@ describe('Token contracts', () => { const from = regularAccounts[0]; const to = regularAccounts[1]; - const { greenlisted } = mTokenPermissionedMinBalanceRoles; + const { greenlisted, minBalanceExempt } = + mTokenPermissionedMinBalanceRoles; await accessControl.grantRole(greenlisted, from.address); await accessControl.grantRole(greenlisted, to.address); - await mTokenPermissionedMinBalance - .connect(owner) - .setIsMinBalanceExempt(from.address, true); + await accessControl.grantRole(minBalanceExempt, from.address); await mint( { tokenContract: mTokenPermissionedMinBalance, owner }, from, From 2125ba8bf9f9694ea6afc18593b51ce5165f461d Mon Sep 17 00:00:00 2001 From: kostyamospan Date: Wed, 15 Jul 2026 13:31:53 +0300 Subject: [PATCH 05/19] refactor: removed flag from _validateMinBalance --- contracts/mTokenMinBalance.sol | 19 +++++++------------ 1 file changed, 7 insertions(+), 12 deletions(-) diff --git a/contracts/mTokenMinBalance.sol b/contracts/mTokenMinBalance.sol index 22026862..f2755b61 100644 --- a/contracts/mTokenMinBalance.sol +++ b/contracts/mTokenMinBalance.sol @@ -24,11 +24,11 @@ abstract contract mTokenMinBalance is mToken { uint256 amount ) internal virtual override { if (from != address(0) && !_isMinBalanceExempt(from)) { - _validateMinBalance(from, true); + _validateMinBalance(from); } if (to != address(0) && !_isMinBalanceExempt(to)) { - _validateMinBalance(to, from != address(0)); + _validateMinBalance(to); } super._afterTokenTransfer(from, to, amount); @@ -51,17 +51,12 @@ abstract contract mTokenMinBalance is mToken { /** * @dev validates the minimum balance of a user * @param user address of the user - * @param canBeZero bool if the user can have a balance of 0 */ - function _validateMinBalance(address user, bool canBeZero) private view { + function _validateMinBalance(address user) private view { uint256 balance = balanceOf(user); - - bool isMinBalanceMet = balance >= 1 ether; - - if (canBeZero) { - isMinBalanceMet = balance == 0 || isMinBalanceMet; - } - - require(isMinBalanceMet, "MTMB: min balance not met"); + require( + balance == 0 || balance >= 1 ether, + "MTMB: min balance not met" + ); } } From a54a98f428ce053d9180cd1908c788a5e7ace5b8 Mon Sep 17 00:00:00 2001 From: kostyamospan Date: Wed, 15 Jul 2026 17:27:28 +0300 Subject: [PATCH 06/19] fix: upgrade tests added --- test/unit/mtoken.test.ts | 126 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 126 insertions(+) diff --git a/test/unit/mtoken.test.ts b/test/unit/mtoken.test.ts index ec3fda14..8c214f71 100644 --- a/test/unit/mtoken.test.ts +++ b/test/unit/mtoken.test.ts @@ -1,9 +1,15 @@ import { loadFixture } from '@nomicfoundation/hardhat-network-helpers'; import { expect } from 'chai'; import { parseUnits } from 'ethers/lib/utils'; +import { upgrades } from 'hardhat'; import { MTokenNameEnum } from '../../config'; import { getRolesForToken, getRolesNamesForToken } from '../../helpers/roles'; +import { + MTokenPermissionedMinBalanceTest, + MTokenPermissionedMinBalanceTest__factory, + MTokenPermissionedTest__factory, +} from '../../typechain-types'; import { acErrors, blackList } from '../common/ac.helpers'; import { defaultDeploy, @@ -1889,3 +1895,123 @@ describe('Shared greenlist role (mGLO -> mGLOBAL)', () => { ); }); }); + +describe('mToken upgrades', () => { + describe('PermissionedMinBalance -> Permissioned', () => { + const permissionedMinBalanceProxyFixture = async () => { + const { owner, accessControl, regularAccounts } = await defaultDeploy(); + + const token = (await upgrades.deployProxy( + new MTokenPermissionedMinBalanceTest__factory(owner), + [accessControl.address], + { initializer: 'initialize', kind: 'transparent' }, + )) as MTokenPermissionedMinBalanceTest; + + const mintRole = await token.M_TOKEN_TEST_MINT_OPERATOR_ROLE(); + const burnRole = await token.M_TOKEN_TEST_BURN_OPERATOR_ROLE(); + const pauseRole = await token.M_TOKEN_TEST_PAUSE_OPERATOR_ROLE(); + const greenlisted = await token.M_TOKEN_TEST_GREENLISTED_ROLE(); + const minBalanceExempt = + await token.M_TOKEN_TEST_MIN_BALANCE_EXEMPT_ROLE(); + + await accessControl.grantRole(mintRole, owner.address); + await accessControl.grantRole(burnRole, owner.address); + await accessControl.grantRole(pauseRole, owner.address); + + return { + owner, + accessControl, + regularAccounts, + token, + roles: { mintRole, burnRole, pauseRole, greenlisted, minBalanceExempt }, + }; + }; + + it('preserves balances and keeps greenlist after dropping min-balance checks', async () => { + const { owner, accessControl, regularAccounts, token, roles } = + await loadFixture(permissionedMinBalanceProxyFixture); + + const from = regularAccounts[0]; + const to = regularAccounts[1]; + const dust = parseUnits('0.1'); + + await accessControl.grantRole(roles.greenlisted, from.address); + await accessControl.grantRole(roles.greenlisted, to.address); + + await mint({ tokenContract: token, owner }, from, parseUnits('3')); + + const balanceBefore = await token.balanceOf(from.address); + const totalSupplyBefore = await token.totalSupply(); + const nameBefore = await token.name(); + const symbolBefore = await token.symbol(); + + await expect(token.connect(from).transfer(to.address, dust)).revertedWith( + 'MTMB: min balance not met', + ); + + await upgrades.upgradeProxy( + token.address, + new MTokenPermissionedTest__factory(owner), + { + unsafeSkipStorageCheck: true, + }, + ); + + const upgraded = MTokenPermissionedTest__factory.connect( + token.address, + owner, + ); + + expect(await upgraded.balanceOf(from.address)).eq(balanceBefore); + expect(await upgraded.totalSupply()).eq(totalSupplyBefore); + expect(await upgraded.name()).eq(nameBefore); + expect(await upgraded.symbol()).eq(symbolBefore); + + await expect(upgraded.connect(from).transfer(to.address, dust)).not + .reverted; + expect(await upgraded.balanceOf(to.address)).eq(dust); + + await accessControl.revokeRole(roles.greenlisted, from.address); + await expect( + upgraded.connect(from).transfer(to.address, dust), + ).revertedWith(acErrors.WMAC_HASNT_ROLE); + }); + + it('preserves greenlist mint rules after upgrade', async () => { + const { owner, accessControl, regularAccounts, token, roles } = + await loadFixture(permissionedMinBalanceProxyFixture); + + const recipient = regularAccounts[0]; + + await mint({ tokenContract: token, owner }, recipient, parseUnits('1'), { + revertMessage: acErrors.WMAC_HASNT_ROLE, + }); + + await upgrades.upgradeProxy( + token.address, + new MTokenPermissionedTest__factory(owner), + { unsafeSkipStorageCheck: true }, + ); + + const upgraded = MTokenPermissionedTest__factory.connect( + token.address, + owner, + ); + + await mint( + { tokenContract: upgraded, owner }, + recipient, + parseUnits('1'), + { revertMessage: acErrors.WMAC_HASNT_ROLE }, + ); + + await accessControl.grantRole(roles.greenlisted, recipient.address); + await mint( + { tokenContract: upgraded, owner }, + recipient, + parseUnits('0.5'), + ); + expect(await upgraded.balanceOf(recipient.address)).eq(parseUnits('0.5')); + }); + }); +}); From 5bbf9965d45f5d762cf60f1c363df956e7d48a5c Mon Sep 17 00:00:00 2001 From: kostyamospan Date: Wed, 15 Jul 2026 17:45:16 +0300 Subject: [PATCH 07/19] fix: removed extra gaps for upgrade safety --- contracts/mTokenPermissionedMinBalance.sol | 6 ++---- .../deploy/codegen/common/templates/mtoken.template.ts | 9 +++++++++ 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/contracts/mTokenPermissionedMinBalance.sol b/contracts/mTokenPermissionedMinBalance.sol index c4982605..8a245ea0 100644 --- a/contracts/mTokenPermissionedMinBalance.sol +++ b/contracts/mTokenPermissionedMinBalance.sol @@ -14,10 +14,8 @@ abstract contract mTokenPermissionedMinBalance is mTokenMinBalance, mTokenPermissioned { - /** - * @dev leaving a storage gap for futures updates - */ - uint256[50] private __gap; + // no gap as we would upgrade some of the deployments + // to mTokenMinBalance /** * @dev overrides _beforeTokenTransfer function to call the parent hooks diff --git a/scripts/deploy/codegen/common/templates/mtoken.template.ts b/scripts/deploy/codegen/common/templates/mtoken.template.ts index d4d304f0..7a9b2c56 100644 --- a/scripts/deploy/codegen/common/templates/mtoken.template.ts +++ b/scripts/deploy/codegen/common/templates/mtoken.template.ts @@ -75,10 +75,19 @@ export const getTokenContractFromTemplate = async ( : '' } + ${ + isPermissionedMToken && isMinBalanceMToken + ? ` + // no gap as we would upgrade some of the deployments + // to mTokenMinBalance + ` + : ` /** * @dev leaving a storage gap for futures updates */ uint256[50] private __gap; + ` + } /** * @inheritdoc mToken From e1488bbba05aacb3cc5723372520efe07c858f6d Mon Sep 17 00:00:00 2001 From: kostya-midas Date: Fri, 17 Jul 2026 18:01:18 +0300 Subject: [PATCH 08/19] fix: typo --- contracts/mTokenPermissionedMinBalance.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/mTokenPermissionedMinBalance.sol b/contracts/mTokenPermissionedMinBalance.sol index 8a245ea0..9806a432 100644 --- a/contracts/mTokenPermissionedMinBalance.sol +++ b/contracts/mTokenPermissionedMinBalance.sol @@ -29,7 +29,7 @@ abstract contract mTokenPermissionedMinBalance is } /** - * @dev overrides _beforeTokenTransfer function to call the parent hooks + * @dev overrides _afterTokenTransfer function to call the parent hooks */ function _afterTokenTransfer( address from, From 98bb0ce3a15cdf20ff01d10035cc398c7601b336 Mon Sep 17 00:00:00 2001 From: kostya-midas Date: Mon, 20 Jul 2026 14:52:21 +0300 Subject: [PATCH 09/19] fix: mwin metadata and contract --- contracts/products/mWIN/mWIN.sol | 19 +++++++++++++++---- helpers/mtokens-metadata.ts | 1 + scripts/upgrades/proposeUpgrade_Token.ts | 2 +- 3 files changed, 17 insertions(+), 5 deletions(-) diff --git a/contracts/products/mWIN/mWIN.sol b/contracts/products/mWIN/mWIN.sol index c31efe28..d00bd219 100644 --- a/contracts/products/mWIN/mWIN.sol +++ b/contracts/products/mWIN/mWIN.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.9; -import "../../mTokenPermissioned.sol"; +import "../../mTokenPermissionedMinBalance.sol"; import "./MWinMidasAccessControlRoles.sol"; /** @@ -9,7 +9,7 @@ import "./MWinMidasAccessControlRoles.sol"; * @author RedDuck Software */ //solhint-disable contract-name-camelcase -contract mWIN is mTokenPermissioned, MWinMidasAccessControlRoles { +contract mWIN is mTokenPermissionedMinBalance, MWinMidasAccessControlRoles { /** * @notice actor that can mint mWIN */ @@ -29,9 +29,13 @@ contract mWIN is mTokenPermissioned, MWinMidasAccessControlRoles { keccak256("M_WIN_PAUSE_OPERATOR_ROLE"); /** - * @dev leaving a storage gap for futures updates + * @notice actor that is exempt from mWIN min balance checks */ - uint256[50] private __gap; + bytes32 public constant M_WIN_MIN_BALANCE_EXEMPT_ROLE = + keccak256("M_WIN_MIN_BALANCE_EXEMPT_ROLE"); + + // no gap as we would upgrade some of the deployments + // to mTokenMinBalance /** * @inheritdoc mToken @@ -72,4 +76,11 @@ contract mWIN is mTokenPermissioned, MWinMidasAccessControlRoles { function _greenlistedRole() internal pure override returns (bytes32) { return M_WIN_GREENLISTED_ROLE; } + + /** + * @inheritdoc mTokenMinBalance + */ + function _minBalanceExemptRole() internal pure override returns (bytes32) { + return M_WIN_MIN_BALANCE_EXEMPT_ROLE; + } } diff --git a/helpers/mtokens-metadata.ts b/helpers/mtokens-metadata.ts index 00b5e62d..acd8ff68 100644 --- a/helpers/mtokens-metadata.ts +++ b/helpers/mtokens-metadata.ts @@ -315,6 +315,7 @@ export const mTokensMetadata: Record< name: 'Midas Wellington Income Opportunities', symbol: 'mWIN', isPermissioned: true, + isMinBalance: true, }, qHVNUSD: { name: 'Qapture Safe Haven', diff --git a/scripts/upgrades/proposeUpgrade_Token.ts b/scripts/upgrades/proposeUpgrade_Token.ts index 525e25c0..52ab7066 100644 --- a/scripts/upgrades/proposeUpgrade_Token.ts +++ b/scripts/upgrades/proposeUpgrade_Token.ts @@ -7,7 +7,7 @@ import { getMTokenOrThrow } from '../../helpers/utils'; import { DeployFunction } from '../deploy/common/types'; const func: DeployFunction = async (hre: HardhatRuntimeEnvironment) => { - const upgradeId = 'mwin-upgrade-permissioned'; + const upgradeId = 'mwin-upgrade-permissioned-min-balance'; const networkAddresses = getCurrentAddresses(hre); const mToken = getMTokenOrThrow(hre); const tokenAddresses = networkAddresses?.[mToken]; From 4b3c7728a72a405ef3df59caf9d59c5649dd8983 Mon Sep 17 00:00:00 2001 From: kostya-midas Date: Mon, 20 Jul 2026 18:33:19 +0300 Subject: [PATCH 10/19] chore: new impl deployed --- .openzeppelin/mainnet.json | 220 ++++++++++++++++++++++++++++++ scripts/deploy/common/timelock.ts | 20 ++- 2 files changed, 233 insertions(+), 7 deletions(-) diff --git a/.openzeppelin/mainnet.json b/.openzeppelin/mainnet.json index 3f1e6fa6..1063c8d8 100644 --- a/.openzeppelin/mainnet.json +++ b/.openzeppelin/mainnet.json @@ -119416,6 +119416,226 @@ } } } + }, + "4ab677bc2848d9a588023e79f9676786276377df378e7c55c4f424e0c6cea7b3": { + "address": "0x3421478bDE3Ce905d85fE24682fa8CafdfF3E44a", + "txHash": "0xa64bb971fe47aaa6e0f7aa7b9e2a6d80b1215e085f8992e25e2f857ff5620904", + "layout": { + "solcVersion": "0.8.9", + "storage": [ + { + "label": "_initialized", + "offset": 0, + "slot": "0", + "type": "t_uint8", + "contract": "Initializable", + "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:63", + "retypedFrom": "bool" + }, + { + "label": "_initializing", + "offset": 1, + "slot": "0", + "type": "t_bool", + "contract": "Initializable", + "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:68" + }, + { + "label": "__gap", + "offset": 0, + "slot": "1", + "type": "t_array(t_uint256)50_storage", + "contract": "ContextUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/utils/ContextUpgradeable.sol:36" + }, + { + "label": "_balances", + "offset": 0, + "slot": "51", + "type": "t_mapping(t_address,t_uint256)", + "contract": "ERC20Upgradeable", + "src": "@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol:40" + }, + { + "label": "_allowances", + "offset": 0, + "slot": "52", + "type": "t_mapping(t_address,t_mapping(t_address,t_uint256))", + "contract": "ERC20Upgradeable", + "src": "@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol:42" + }, + { + "label": "_totalSupply", + "offset": 0, + "slot": "53", + "type": "t_uint256", + "contract": "ERC20Upgradeable", + "src": "@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol:44" + }, + { + "label": "_name", + "offset": 0, + "slot": "54", + "type": "t_string_storage", + "contract": "ERC20Upgradeable", + "src": "@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol:46" + }, + { + "label": "_symbol", + "offset": 0, + "slot": "55", + "type": "t_string_storage", + "contract": "ERC20Upgradeable", + "src": "@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol:47" + }, + { + "label": "__gap", + "offset": 0, + "slot": "56", + "type": "t_array(t_uint256)45_storage", + "contract": "ERC20Upgradeable", + "src": "@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol:376" + }, + { + "label": "_paused", + "offset": 0, + "slot": "101", + "type": "t_bool", + "contract": "PausableUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/security/PausableUpgradeable.sol:29" + }, + { + "label": "__gap", + "offset": 0, + "slot": "102", + "type": "t_array(t_uint256)49_storage", + "contract": "PausableUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/security/PausableUpgradeable.sol:116" + }, + { + "label": "__gap", + "offset": 0, + "slot": "151", + "type": "t_array(t_uint256)50_storage", + "contract": "ERC20PausableUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/token/ERC20/extensions/ERC20PausableUpgradeable.sol:48" + }, + { + "label": "accessControl", + "offset": 0, + "slot": "201", + "type": "t_contract(MidasAccessControl)3346", + "contract": "WithMidasAccessControl", + "src": "contracts/access/WithMidasAccessControl.sol:24" + }, + { + "label": "__gap", + "offset": 0, + "slot": "202", + "type": "t_array(t_uint256)50_storage", + "contract": "WithMidasAccessControl", + "src": "contracts/access/WithMidasAccessControl.sol:29" + }, + { + "label": "__gap", + "offset": 0, + "slot": "252", + "type": "t_array(t_uint256)50_storage", + "contract": "Blacklistable", + "src": "contracts/access/Blacklistable.sol:16" + }, + { + "label": "metadata", + "offset": 0, + "slot": "302", + "type": "t_mapping(t_bytes32,t_bytes_storage)", + "contract": "mToken", + "src": "contracts/mToken.sol:18" + }, + { + "label": "__gap", + "offset": 0, + "slot": "303", + "type": "t_array(t_uint256)50_storage", + "contract": "mToken", + "src": "contracts/mToken.sol:23" + }, + { + "label": "__gap", + "offset": 0, + "slot": "353", + "type": "t_array(t_uint256)50_storage", + "contract": "mTokenMinBalance", + "src": "contracts/mTokenMinBalance.sol:16" + }, + { + "label": "__gap", + "offset": 0, + "slot": "403", + "type": "t_array(t_uint256)50_storage", + "contract": "mTokenPermissioned", + "src": "contracts/mTokenPermissioned.sol:16" + } + ], + "types": { + "t_address": { + "label": "address", + "numberOfBytes": "20" + }, + "t_array(t_uint256)45_storage": { + "label": "uint256[45]", + "numberOfBytes": "1440" + }, + "t_array(t_uint256)49_storage": { + "label": "uint256[49]", + "numberOfBytes": "1568" + }, + "t_array(t_uint256)50_storage": { + "label": "uint256[50]", + "numberOfBytes": "1600" + }, + "t_bool": { + "label": "bool", + "numberOfBytes": "1" + }, + "t_bytes32": { + "label": "bytes32", + "numberOfBytes": "32" + }, + "t_bytes_storage": { + "label": "bytes", + "numberOfBytes": "32" + }, + "t_contract(MidasAccessControl)3346": { + "label": "contract MidasAccessControl", + "numberOfBytes": "20" + }, + "t_mapping(t_address,t_mapping(t_address,t_uint256))": { + "label": "mapping(address => mapping(address => uint256))", + "numberOfBytes": "32" + }, + "t_mapping(t_address,t_uint256)": { + "label": "mapping(address => uint256)", + "numberOfBytes": "32" + }, + "t_mapping(t_bytes32,t_bytes_storage)": { + "label": "mapping(bytes32 => bytes)", + "numberOfBytes": "32" + }, + "t_string_storage": { + "label": "string", + "numberOfBytes": "32" + }, + "t_uint256": { + "label": "uint256", + "numberOfBytes": "32" + }, + "t_uint8": { + "label": "uint8", + "numberOfBytes": "1" + } + } + } } } } diff --git a/scripts/deploy/common/timelock.ts b/scripts/deploy/common/timelock.ts index 9267616c..d76e5990 100644 --- a/scripts/deploy/common/timelock.ts +++ b/scripts/deploy/common/timelock.ts @@ -212,7 +212,7 @@ const validateSimulateContractUpgrade = async ( await increase(days(3)); await mine(); - const proxyAdmin = (await hre.upgrades.admin.getInstance()) as ProxyAdmin; + const proxyAdmin = await getProxyAdmin(hre); const proxyAdminOwner = await proxyAdmin.owner(); const timelock = await getTimelockContract(hre); @@ -446,7 +446,7 @@ const getUpgradeTx = async ( hre: HardhatRuntimeEnvironment, { newImplementation, proxyAddress, initializerCalldata }: GetUpgradeTxParams, ) => { - const admin = await hre.upgrades.admin.getInstance(); + const admin = await getProxyAdmin(hre); const upgradeCallData = initializerCalldata ? admin.interface.encodeFunctionData('upgradeAndCall', [ @@ -466,7 +466,7 @@ const getTransferOwnershipTx = async ( hre: HardhatRuntimeEnvironment, { newOwner }: { newOwner: string }, ) => { - const admin = await hre.upgrades.admin.getInstance(); + const admin = await getProxyAdmin(hre); const upgradeCallData = admin.interface.encodeFunctionData( 'transferOwnership', @@ -529,7 +529,6 @@ const proposeTimelockTx: PopulateTxFn = async ( if (isOperationExists) { throw new Error('Operation is already exists'); } - const tx = await timelockContract.populateTransaction.schedule( ...params, await timelockContract.getMinDelay(), @@ -558,7 +557,7 @@ const createUpgradeTimelockTx = async ( hre, salt, async (hre) => { - const admin = (await hre.upgrades.admin.getInstance()) as ProxyAdmin; + const admin = await getProxyAdmin(hre); const currentImpl = await admin.getProxyImplementation( params.proxyAddress, @@ -580,6 +579,7 @@ const createUpgradeTimelockTx = async ( // Runs even under skipValidation — this is the incident-preventing check. const { abi } = await hre.artifacts.readArtifact(params.contractName); + await assertReinitializerCovered({ provider: hre.ethers.provider, proxyAddress: params.proxyAddress, @@ -623,7 +623,7 @@ const createTransferOwnershipTimelockTx = async ( async (hre) => { const timelockContract = await getTimelockContract(hre); - const admin = (await hre.upgrades.admin.getInstance()) as ProxyAdmin; + const admin = await getProxyAdmin(hre); const currentOwner = await admin.owner(); @@ -668,7 +668,7 @@ const createTimeLockTx = async ( return false; } - const admin = (await hre.upgrades.admin.getInstance()) as ProxyAdmin; + const admin = await getProxyAdmin(hre); const networkAddresses = getCurrentAddresses(hre); @@ -747,6 +747,12 @@ const createTimeLockTx = async ( return true; }; +const getProxyAdmin = async (hre: HardhatRuntimeEnvironment) => { + return (await hre.upgrades.admin.getInstance()).connect( + hre.ethers.provider, + ) as ProxyAdmin; +}; + function parseRevertReason(data: string) { try { // 0x08c379a0 = Error(string) From 2a5fb213d271ddd6b92c64ddb917e8b64e804e78 Mon Sep 17 00:00:00 2001 From: kostya-midas Date: Tue, 21 Jul 2026 17:16:26 +0300 Subject: [PATCH 11/19] chore: mwin aggregator upgrade propose --- .openzeppelin/mainnet.json | 176 ++++++++++++++++++ .../mWIN/MWinCustomAggregatorFeed.sol | 15 ++ .../upgrades/executeUpgrade_Aggregators.ts | 5 +- .../upgrades/proposeUpgrade_Aggregators.ts | 4 +- 4 files changed, 196 insertions(+), 4 deletions(-) diff --git a/.openzeppelin/mainnet.json b/.openzeppelin/mainnet.json index 1063c8d8..e6fa6105 100644 --- a/.openzeppelin/mainnet.json +++ b/.openzeppelin/mainnet.json @@ -119636,6 +119636,182 @@ } } } + }, + "9dfbaa430e00eac2e2aa870498562f8dcdcc409143cf4cb1973b67d7ecc112d9": { + "address": "0x7156b140AaD6999F48767Db6687F46e8Df331C4C", + "txHash": "0xcabb320c7d7fd542ccd3bec4deb3d82a6c85ccfef0323a8d9b2d9bdd6df4b776", + "layout": { + "solcVersion": "0.8.9", + "storage": [ + { + "label": "_initialized", + "offset": 0, + "slot": "0", + "type": "t_uint8", + "contract": "Initializable", + "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:63", + "retypedFrom": "bool" + }, + { + "label": "_initializing", + "offset": 1, + "slot": "0", + "type": "t_bool", + "contract": "Initializable", + "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:68" + }, + { + "label": "accessControl", + "offset": 2, + "slot": "0", + "type": "t_contract(MidasAccessControl)3335", + "contract": "WithMidasAccessControl", + "src": "contracts/access/WithMidasAccessControl.sol:24" + }, + { + "label": "__gap", + "offset": 0, + "slot": "1", + "type": "t_array(t_uint256)50_storage", + "contract": "WithMidasAccessControl", + "src": "contracts/access/WithMidasAccessControl.sol:29" + }, + { + "label": "description", + "offset": 0, + "slot": "51", + "type": "t_string_storage", + "contract": "CustomAggregatorV3CompatibleFeed", + "src": "contracts/feeds/CustomAggregatorV3CompatibleFeed.sol:31" + }, + { + "label": "latestRound", + "offset": 0, + "slot": "52", + "type": "t_uint80", + "contract": "CustomAggregatorV3CompatibleFeed", + "src": "contracts/feeds/CustomAggregatorV3CompatibleFeed.sol:36" + }, + { + "label": "maxAnswerDeviation", + "offset": 0, + "slot": "53", + "type": "t_uint256", + "contract": "CustomAggregatorV3CompatibleFeed", + "src": "contracts/feeds/CustomAggregatorV3CompatibleFeed.sol:42" + }, + { + "label": "minAnswer", + "offset": 0, + "slot": "54", + "type": "t_int192", + "contract": "CustomAggregatorV3CompatibleFeed", + "src": "contracts/feeds/CustomAggregatorV3CompatibleFeed.sol:47" + }, + { + "label": "maxAnswer", + "offset": 0, + "slot": "55", + "type": "t_int192", + "contract": "CustomAggregatorV3CompatibleFeed", + "src": "contracts/feeds/CustomAggregatorV3CompatibleFeed.sol:52" + }, + { + "label": "_roundData", + "offset": 0, + "slot": "56", + "type": "t_mapping(t_uint80,t_struct(RoundData)3499_storage)", + "contract": "CustomAggregatorV3CompatibleFeed", + "src": "contracts/feeds/CustomAggregatorV3CompatibleFeed.sol:57" + }, + { + "label": "__gap", + "offset": 0, + "slot": "57", + "type": "t_array(t_uint256)50_storage", + "contract": "MWinCustomAggregatorFeed", + "src": "contracts/products/mWIN/MWinCustomAggregatorFeed.sol:20" + } + ], + "types": { + "t_array(t_uint256)50_storage": { + "label": "uint256[50]", + "numberOfBytes": "1600" + }, + "t_bool": { + "label": "bool", + "numberOfBytes": "1" + }, + "t_contract(MidasAccessControl)3335": { + "label": "contract MidasAccessControl", + "numberOfBytes": "20" + }, + "t_int192": { + "label": "int192", + "numberOfBytes": "24" + }, + "t_int256": { + "label": "int256", + "numberOfBytes": "32" + }, + "t_mapping(t_uint80,t_struct(RoundData)3499_storage)": { + "label": "mapping(uint80 => struct CustomAggregatorV3CompatibleFeed.RoundData)", + "numberOfBytes": "32" + }, + "t_string_storage": { + "label": "string", + "numberOfBytes": "32" + }, + "t_struct(RoundData)3499_storage": { + "label": "struct CustomAggregatorV3CompatibleFeed.RoundData", + "members": [ + { + "label": "roundId", + "type": "t_uint80", + "offset": 0, + "slot": "0" + }, + { + "label": "answer", + "type": "t_int256", + "offset": 0, + "slot": "1" + }, + { + "label": "startedAt", + "type": "t_uint256", + "offset": 0, + "slot": "2" + }, + { + "label": "updatedAt", + "type": "t_uint256", + "offset": 0, + "slot": "3" + }, + { + "label": "answeredInRound", + "type": "t_uint80", + "offset": 0, + "slot": "4" + } + ], + "numberOfBytes": "160" + }, + "t_uint256": { + "label": "uint256", + "numberOfBytes": "32" + }, + "t_uint8": { + "label": "uint8", + "numberOfBytes": "1" + }, + "t_uint80": { + "label": "uint80", + "numberOfBytes": "10" + } + } + } } } } diff --git a/contracts/products/mWIN/MWinCustomAggregatorFeed.sol b/contracts/products/mWIN/MWinCustomAggregatorFeed.sol index 5744ec2b..f1199c01 100644 --- a/contracts/products/mWIN/MWinCustomAggregatorFeed.sol +++ b/contracts/products/mWIN/MWinCustomAggregatorFeed.sol @@ -19,6 +19,21 @@ contract MWinCustomAggregatorFeed is */ uint256[50] private __gap; + /** + * @notice reinitialize the feed with new min and max answer + * @param _newMinAnswer The new min answer + * @param _newMaxAnswer The new max answer + */ + function initializeV2(int192 _newMinAnswer, int192 _newMaxAnswer) + external + reinitializer(2) + { + require(_newMinAnswer < _newMaxAnswer, "CA: !min/max"); + + minAnswer = _newMinAnswer; + maxAnswer = _newMaxAnswer; + } + /** * @inheritdoc CustomAggregatorV3CompatibleFeed */ diff --git a/scripts/upgrades/executeUpgrade_Aggregators.ts b/scripts/upgrades/executeUpgrade_Aggregators.ts index c687eb42..e2a8b052 100644 --- a/scripts/upgrades/executeUpgrade_Aggregators.ts +++ b/scripts/upgrades/executeUpgrade_Aggregators.ts @@ -8,7 +8,8 @@ import { getMTokenOrThrow } from '../../helpers/utils'; import { DeployFunction } from '../deploy/common/types'; const func: DeployFunction = async (hre: HardhatRuntimeEnvironment) => { - const upgradeId = 'mkralpha-custom-aggregator-upgrade-v2'; + const upgradeId = 'mwin-custom-aggregator-upgrade-v2'; + const networkAddresses = getCurrentAddresses(hre); const mToken = getMTokenOrThrow(hre); const tokenAddresses = networkAddresses?.[mToken]; @@ -25,7 +26,7 @@ const func: DeployFunction = async (hre: HardhatRuntimeEnvironment) => { { contractType: 'customAggregator', initializer: 'initializeV2', - initializerArgs: [parseUnits('0.66', 8)], + initializerArgs: [parseUnits('90', 8), parseUnits('140', 8)], }, ], }, diff --git a/scripts/upgrades/proposeUpgrade_Aggregators.ts b/scripts/upgrades/proposeUpgrade_Aggregators.ts index ef2707d9..f1719210 100644 --- a/scripts/upgrades/proposeUpgrade_Aggregators.ts +++ b/scripts/upgrades/proposeUpgrade_Aggregators.ts @@ -8,7 +8,7 @@ import { getMTokenOrThrow } from '../../helpers/utils'; import { DeployFunction } from '../deploy/common/types'; const func: DeployFunction = async (hre: HardhatRuntimeEnvironment) => { - const upgradeId = 'mre7-custom-aggregator-upgrade-v3'; + const upgradeId = 'mwin-custom-aggregator-upgrade-v2'; const networkAddresses = getCurrentAddresses(hre); const mToken = getMTokenOrThrow(hre); @@ -26,7 +26,7 @@ const func: DeployFunction = async (hre: HardhatRuntimeEnvironment) => { { contractType: 'customAggregator', initializer: 'initializeV2', - initializerArgs: [parseUnits('0.66', 8)], + initializerArgs: [parseUnits('90', 8), parseUnits('140', 8)], }, ], }, From 4e52c6ac056427fbad55fb409802cd96ad98a926 Mon Sep 17 00:00:00 2001 From: kostya-midas Date: Wed, 22 Jul 2026 14:28:29 +0300 Subject: [PATCH 12/19] fix: propose reinitializer values --- scripts/upgrades/executeUpgrade_Aggregators.ts | 2 +- scripts/upgrades/proposeUpgrade_Aggregators.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/upgrades/executeUpgrade_Aggregators.ts b/scripts/upgrades/executeUpgrade_Aggregators.ts index e2a8b052..45cfec1b 100644 --- a/scripts/upgrades/executeUpgrade_Aggregators.ts +++ b/scripts/upgrades/executeUpgrade_Aggregators.ts @@ -26,7 +26,7 @@ const func: DeployFunction = async (hre: HardhatRuntimeEnvironment) => { { contractType: 'customAggregator', initializer: 'initializeV2', - initializerArgs: [parseUnits('90', 8), parseUnits('140', 8)], + initializerArgs: [parseUnits('90000', 8), parseUnits('140000', 8)], }, ], }, diff --git a/scripts/upgrades/proposeUpgrade_Aggregators.ts b/scripts/upgrades/proposeUpgrade_Aggregators.ts index f1719210..942c6eae 100644 --- a/scripts/upgrades/proposeUpgrade_Aggregators.ts +++ b/scripts/upgrades/proposeUpgrade_Aggregators.ts @@ -26,7 +26,7 @@ const func: DeployFunction = async (hre: HardhatRuntimeEnvironment) => { { contractType: 'customAggregator', initializer: 'initializeV2', - initializerArgs: [parseUnits('90', 8), parseUnits('140', 8)], + initializerArgs: [parseUnits('90000', 8), parseUnits('140000', 8)], }, ], }, From 198375cb58702db91dbb5df10756ef7cda469506 Mon Sep 17 00:00:00 2001 From: kostya-midas Date: Fri, 24 Jul 2026 13:08:05 +0300 Subject: [PATCH 13/19] fix: execute script --- scripts/upgrades/executeUpgrade_Token.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/upgrades/executeUpgrade_Token.ts b/scripts/upgrades/executeUpgrade_Token.ts index d0cbaeba..dd3c3273 100644 --- a/scripts/upgrades/executeUpgrade_Token.ts +++ b/scripts/upgrades/executeUpgrade_Token.ts @@ -7,7 +7,7 @@ import { getMTokenOrThrow } from '../../helpers/utils'; import { DeployFunction } from '../deploy/common/types'; const func: DeployFunction = async (hre: HardhatRuntimeEnvironment) => { - const upgradeId = 'mwin-upgrade-permissioned'; + const upgradeId = 'mwin-upgrade-permissioned-min-balance'; const networkAddresses = getCurrentAddresses(hre); const mToken = getMTokenOrThrow(hre); const tokenAddresses = networkAddresses?.[mToken]; From 8bd68005954878ca86b83ef4976bd6333a4e1235 Mon Sep 17 00:00:00 2001 From: Dmytro Horbatenko Date: Mon, 27 Jul 2026 17:45:21 +0300 Subject: [PATCH 14/19] fix: collect redemption fees before burning mTokens --- contracts/RedemptionVault.sol | 14 +- contracts/RedemptionVaultWithAave.sol | 2 +- contracts/RedemptionVaultWithBUIDL.sol | 2 +- contracts/RedemptionVaultWithMToken.sol | 2 +- contracts/RedemptionVaultWithMorpho.sol | 2 +- contracts/RedemptionVaultWithUSTB.sol | 2 +- test/common/fixtures.ts | 116 ++++++++++++++ test/unit/RedemptionVault.test.ts | 167 +++++++++++++++++++- test/unit/RedemptionVaultWithAave.test.ts | 2 +- test/unit/RedemptionVaultWithBUIDL.test.ts | 2 +- test/unit/RedemptionVaultWithMToken.test.ts | 2 +- test/unit/RedemptionVaultWithMorpho.test.ts | 38 ++++- test/unit/RedemptionVaultWithUSTB.test.ts | 2 +- 13 files changed, 333 insertions(+), 20 deletions(-) diff --git a/contracts/RedemptionVault.sol b/contracts/RedemptionVault.sol index 8f5762a2..fb031262 100644 --- a/contracts/RedemptionVault.sol +++ b/contracts/RedemptionVault.sol @@ -615,7 +615,6 @@ contract RedemptionVault is ManageableVault, IRedemptionVault { _requireAndUpdateAllowance(tokenOutCopy, amountTokenOut); - mToken.burn(user, calcResult.amountMTokenWithoutFee); if (calcResult.feeAmount > 0) _tokenTransferFromUser( address(mToken), @@ -623,6 +622,7 @@ contract RedemptionVault is ManageableVault, IRedemptionVault { calcResult.feeAmount, 18 ); + mToken.burn(user, calcResult.amountMTokenWithoutFee); _tokenTransferToUser( tokenOutCopy, @@ -682,12 +682,6 @@ 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), @@ -695,6 +689,12 @@ contract RedemptionVault is ManageableVault, IRedemptionVault { calcResult.feeAmount, 18 ); + _tokenTransferFromUser( + address(mToken), + address(this), + calcResult.amountMTokenWithoutFee, + 18 // mToken always have 18 decimals + ); requestId = currentRequestId.current(); currentRequestId.increment(); diff --git a/contracts/RedemptionVaultWithAave.sol b/contracts/RedemptionVaultWithAave.sol index 2ac74b85..bd81544e 100644 --- a/contracts/RedemptionVaultWithAave.sol +++ b/contracts/RedemptionVaultWithAave.sol @@ -129,7 +129,6 @@ contract RedemptionVaultWithAave is RedemptionVault { _requireAndUpdateAllowance(tokenOutCopy, amountTokenOut); - mToken.burn(user, calcResult.amountMTokenWithoutFee); if (calcResult.feeAmount > 0) _tokenTransferFromUser( address(mToken), @@ -137,6 +136,7 @@ contract RedemptionVaultWithAave is RedemptionVault { calcResult.feeAmount, 18 ); + mToken.burn(user, calcResult.amountMTokenWithoutFee); uint256 amountTokenOutWithoutFeeFrom18 = ((calcResult .amountMTokenWithoutFee * mTokenRate) / tokenOutRate) diff --git a/contracts/RedemptionVaultWithBUIDL.sol b/contracts/RedemptionVaultWithBUIDL.sol index 488ce356..d9202b18 100644 --- a/contracts/RedemptionVaultWithBUIDL.sol +++ b/contracts/RedemptionVaultWithBUIDL.sol @@ -164,7 +164,6 @@ contract RedemptionVaultWIthBUIDL is RedemptionVault { _requireAndUpdateAllowance(tokenOutCopy, amountTokenOut); - mToken.burn(user, calcResult.amountMTokenWithoutFee); if (calcResult.feeAmount > 0) _tokenTransferFromUser( address(mToken), @@ -172,6 +171,7 @@ contract RedemptionVaultWIthBUIDL is RedemptionVault { calcResult.feeAmount, 18 ); + mToken.burn(user, calcResult.amountMTokenWithoutFee); amountTokenOutWithoutFee = (calcResult.amountMTokenWithoutFee * mTokenRate) / diff --git a/contracts/RedemptionVaultWithMToken.sol b/contracts/RedemptionVaultWithMToken.sol index 7fa9024d..ad78e706 100644 --- a/contracts/RedemptionVaultWithMToken.sol +++ b/contracts/RedemptionVaultWithMToken.sol @@ -162,7 +162,6 @@ contract RedemptionVaultWithMToken is RedemptionVault { _requireAndUpdateAllowance(tokenOutCopy, amountTokenOut); - mToken.burn(user, calcResult.amountMTokenWithoutFee); if (calcResult.feeAmount > 0) _tokenTransferFromUser( address(mToken), @@ -170,6 +169,7 @@ contract RedemptionVaultWithMToken is RedemptionVault { calcResult.feeAmount, 18 ); + mToken.burn(user, calcResult.amountMTokenWithoutFee); uint256 amountTokenOutWithoutFeeFrom18 = ((calcResult .amountMTokenWithoutFee * mTokenRate) / tokenOutRate) diff --git a/contracts/RedemptionVaultWithMorpho.sol b/contracts/RedemptionVaultWithMorpho.sol index bb4db3d8..55a714d6 100644 --- a/contracts/RedemptionVaultWithMorpho.sol +++ b/contracts/RedemptionVaultWithMorpho.sol @@ -133,7 +133,6 @@ contract RedemptionVaultWithMorpho is RedemptionVault { _requireAndUpdateAllowance(tokenOutCopy, amountTokenOut); - mToken.burn(user, calcResult.amountMTokenWithoutFee); if (calcResult.feeAmount > 0) _tokenTransferFromUser( address(mToken), @@ -141,6 +140,7 @@ contract RedemptionVaultWithMorpho is RedemptionVault { calcResult.feeAmount, 18 ); + mToken.burn(user, calcResult.amountMTokenWithoutFee); uint256 amountTokenOutWithoutFeeFrom18 = ((calcResult .amountMTokenWithoutFee * mTokenRate) / tokenOutRate) diff --git a/contracts/RedemptionVaultWithUSTB.sol b/contracts/RedemptionVaultWithUSTB.sol index 16905126..60a31675 100644 --- a/contracts/RedemptionVaultWithUSTB.sol +++ b/contracts/RedemptionVaultWithUSTB.sol @@ -130,7 +130,6 @@ contract RedemptionVaultWithUSTB is RedemptionVault { _requireAndUpdateAllowance(tokenOutCopy, amountTokenOut); - mToken.burn(user, calcResult.amountMTokenWithoutFee); if (calcResult.feeAmount > 0) _tokenTransferFromUser( address(mToken), @@ -138,6 +137,7 @@ contract RedemptionVaultWithUSTB is RedemptionVault { calcResult.feeAmount, 18 ); + mToken.burn(user, calcResult.amountMTokenWithoutFee); uint256 amountTokenOutWithoutFeeFrom18 = ((calcResult .amountMTokenWithoutFee * mTokenRate) / tokenOutRate) diff --git a/test/common/fixtures.ts b/test/common/fixtures.ts index d9229279..d2675e47 100644 --- a/test/common/fixtures.ts +++ b/test/common/fixtures.ts @@ -1023,6 +1023,122 @@ export const mTokenMinBalanceFixture = async ( }; }; +export const mTokenMinBalanceRedemptionFixture = async () => { + const fx = await mTokenMinBalanceFixture(); + const { + accessControl, + dataFeed, + feeReceiver, + mockedSanctionsList, + morphoVaultMock, + mTokenMinBalance, + mTokenMinBalanceRoles, + mTokenToUsdDataFeed, + owner, + requestRedeemer, + stableCoins, + tokensReceiver, + } = fx; + + const redemptionVault = await new RedemptionVaultTest__factory( + owner, + ).deploy(); + await redemptionVault.initialize( + accessControl.address, + { + mToken: mTokenMinBalance.address, + mTokenDataFeed: mTokenToUsdDataFeed.address, + }, + { + feeReceiver: feeReceiver.address, + tokensReceiver: tokensReceiver.address, + }, + { + instantFee: parseUnits('0.5', 2), + instantDailyLimit: parseUnits('500000'), + }, + mockedSanctionsList.address, + parseUnits('2', 2), + parseUnits('1.1'), + { + fiatAdditionalFee: parseUnits('0.1', 2), + fiatFlatFee: parseUnits('30'), + minFiatRedeemAmount: parseUnits('1000'), + }, + requestRedeemer.address, + ); + + const redemptionVaultWithMorpho = + await new RedemptionVaultWithMorphoTest__factory(owner).deploy(); + await redemptionVaultWithMorpho.initialize( + accessControl.address, + { + mToken: mTokenMinBalance.address, + mTokenDataFeed: mTokenToUsdDataFeed.address, + }, + { + feeReceiver: feeReceiver.address, + tokensReceiver: tokensReceiver.address, + }, + { + instantFee: parseUnits('0.5', 2), + instantDailyLimit: parseUnits('500000'), + }, + mockedSanctionsList.address, + parseUnits('2', 2), + parseUnits('1.1'), + { + fiatAdditionalFee: parseUnits('0.1', 2), + fiatFlatFee: parseUnits('30'), + minFiatRedeemAmount: parseUnits('1000'), + }, + requestRedeemer.address, + ); + + await accessControl.grantRole( + mTokenMinBalanceRoles.burn, + redemptionVault.address, + ); + await accessControl.grantRole( + mTokenMinBalanceRoles.burn, + redemptionVaultWithMorpho.address, + ); + await accessControl.grantRole( + mTokenMinBalanceRoles.minBalanceExempt, + feeReceiver.address, + ); + + const paymentToken = stableCoins.usdc; + for (const vault of [redemptionVault, redemptionVaultWithMorpho]) { + await vault.addPaymentToken( + paymentToken.address, + dataFeed.address, + 0, + parseUnits('1000000000'), + true, + ); + } + + await redemptionVaultWithMorpho.setMorphoVault( + paymentToken.address, + morphoVaultMock.address, + ); + + await paymentToken.mint(redemptionVault.address, parseUnits('100', 8)); + await paymentToken.mint(morphoVaultMock.address, parseUnits('100', 8)); + await morphoVaultMock.mint( + redemptionVaultWithMorpho.address, + parseUnits('100', 8), + ); + + return { + ...fx, + paymentToken, + redemptionVault, + redemptionVaultWithMorpho, + }; +}; + /** * mTokenPermissionedMinBalanceTest fixture for permissioned + min-balance unit tests. */ diff --git a/test/unit/RedemptionVault.test.ts b/test/unit/RedemptionVault.test.ts index 424610e4..04a82ae0 100644 --- a/test/unit/RedemptionVault.test.ts +++ b/test/unit/RedemptionVault.test.ts @@ -22,7 +22,11 @@ import { setRoundDataGrowth, } from '../common/custom-feed-growth.helpers'; import { setRoundData } from '../common/data-feed.helpers'; -import { defaultDeploy, mTokenPermissionedFixture } from '../common/fixtures'; +import { + defaultDeploy, + mTokenMinBalanceRedemptionFixture, + mTokenPermissionedFixture, +} from '../common/fixtures'; import { addPaymentTokenTest, addWaivedFeeAccountTest, @@ -1468,6 +1472,131 @@ describe('RedemptionVault', function () { }); describe('redeemInstant()', () => { + it('with min-balance mToken - redeems the full balance after transferring the fee', async () => { + const { + feeReceiver, + mTokenMinBalance, + paymentToken, + redemptionVault, + regularAccounts, + } = await mTokenMinBalanceRedemptionFixture(); + const user = regularAccounts[0]; + const amount = parseUnits('1.1'); + + await mTokenMinBalance.mint(user.address, amount); + await mTokenMinBalance + .connect(user) + .approve(redemptionVault.address, amount); + + await expect( + redemptionVault + .connect(user) + ['redeemInstant(address,uint256,uint256)']( + paymentToken.address, + amount, + 0, + ), + ).not.reverted; + expect(await mTokenMinBalance.balanceOf(user.address)).eq(0); + expect(await mTokenMinBalance.balanceOf(feeReceiver.address)).eq( + parseUnits('0.0055'), + ); + }); + + it('with min-balance mToken - allows a redemption that leaves exactly one token', async () => { + const { + mTokenMinBalance, + paymentToken, + redemptionVault, + regularAccounts, + } = await mTokenMinBalanceRedemptionFixture(); + const user = regularAccounts[0]; + const amount = parseUnits('1.1'); + + await mTokenMinBalance.mint(user.address, parseUnits('2.1')); + await mTokenMinBalance + .connect(user) + .approve(redemptionVault.address, amount); + + await expect( + redemptionVault + .connect(user) + ['redeemInstant(address,uint256,uint256)']( + paymentToken.address, + amount, + 0, + ), + ).not.reverted; + expect(await mTokenMinBalance.balanceOf(user.address)).eq( + parseUnits('1'), + ); + }); + + it('should fail: with min-balance mToken - redemption would leave dust', async () => { + const { + feeReceiver, + mTokenMinBalance, + paymentToken, + redemptionVault, + regularAccounts, + } = await mTokenMinBalanceRedemptionFixture(); + const user = regularAccounts[0]; + const amount = parseUnits('1.1'); + const balance = parseUnits('2'); + + await mTokenMinBalance.mint(user.address, balance); + await mTokenMinBalance + .connect(user) + .approve(redemptionVault.address, amount); + + await expect( + redemptionVault + .connect(user) + ['redeemInstant(address,uint256,uint256)']( + paymentToken.address, + amount, + 0, + ), + ).revertedWith('MTMB: min balance not met'); + expect(await mTokenMinBalance.balanceOf(user.address)).eq(balance); + expect(await mTokenMinBalance.balanceOf(feeReceiver.address)).eq(0); + }); + + it('should fail: with min-balance mToken - fee receiver is not exempt', async () => { + const { + accessControl, + feeReceiver, + mTokenMinBalance, + mTokenMinBalanceRoles, + paymentToken, + redemptionVault, + regularAccounts, + } = await mTokenMinBalanceRedemptionFixture(); + const user = regularAccounts[0]; + const amount = parseUnits('1.1'); + + await accessControl.revokeRole( + mTokenMinBalanceRoles.minBalanceExempt, + feeReceiver.address, + ); + await mTokenMinBalance.mint(user.address, amount); + await mTokenMinBalance + .connect(user) + .approve(redemptionVault.address, amount); + + await expect( + redemptionVault + .connect(user) + ['redeemInstant(address,uint256,uint256)']( + paymentToken.address, + amount, + 0, + ), + ).revertedWith('MTMB: min balance not met'); + expect(await mTokenMinBalance.balanceOf(user.address)).eq(amount); + expect(await mTokenMinBalance.balanceOf(feeReceiver.address)).eq(0); + }); + it('should fail: when there is no token in vault', async () => { const { owner, @@ -1603,7 +1732,7 @@ describe('RedemptionVault', function () { stableCoins.dai, 100, { - revertMessage: 'ERC20: burn amount exceeds balance', + revertMessage: 'ERC20: transfer amount exceeds balance', }, ); }); @@ -2775,6 +2904,40 @@ describe('RedemptionVault', function () { }); describe('redeemRequest()', () => { + it('with min-balance mToken - requests the full balance after transferring the fee', async () => { + const { + feeReceiver, + mTokenMinBalance, + paymentToken, + redemptionVault, + regularAccounts, + } = await mTokenMinBalanceRedemptionFixture(); + const user = regularAccounts[0]; + const amount = parseUnits('1.1'); + + await redemptionVault.changeTokenFee( + paymentToken.address, + parseUnits('0.5', 2), + ); + await mTokenMinBalance.mint(user.address, amount); + await mTokenMinBalance + .connect(user) + .approve(redemptionVault.address, amount); + + await expect( + redemptionVault + .connect(user) + ['redeemRequest(address,uint256)'](paymentToken.address, amount), + ).not.reverted; + expect(await mTokenMinBalance.balanceOf(user.address)).eq(0); + expect(await mTokenMinBalance.balanceOf(feeReceiver.address)).eq( + parseUnits('0.0055'), + ); + expect(await mTokenMinBalance.balanceOf(redemptionVault.address)).eq( + parseUnits('1.0945'), + ); + }); + it('should fail: when there is no token in vault', async () => { const { owner, diff --git a/test/unit/RedemptionVaultWithAave.test.ts b/test/unit/RedemptionVaultWithAave.test.ts index 72869bae..5df78e89 100644 --- a/test/unit/RedemptionVaultWithAave.test.ts +++ b/test/unit/RedemptionVaultWithAave.test.ts @@ -1022,7 +1022,7 @@ describe('RedemptionVaultWithAave', function () { stableCoins.usdc, 100, { - revertMessage: 'ERC20: burn amount exceeds balance', + revertMessage: 'ERC20: transfer amount exceeds balance', }, ); }); diff --git a/test/unit/RedemptionVaultWithBUIDL.test.ts b/test/unit/RedemptionVaultWithBUIDL.test.ts index 36f92de5..e228a821 100644 --- a/test/unit/RedemptionVaultWithBUIDL.test.ts +++ b/test/unit/RedemptionVaultWithBUIDL.test.ts @@ -1411,7 +1411,7 @@ describe('RedemptionVaultWithBUIDL', function () { stableCoins.usdc, 100, { - revertMessage: 'ERC20: burn amount exceeds balance', + revertMessage: 'ERC20: transfer amount exceeds balance', }, ); }); diff --git a/test/unit/RedemptionVaultWithMToken.test.ts b/test/unit/RedemptionVaultWithMToken.test.ts index 0210ccb1..4ee0ac0c 100644 --- a/test/unit/RedemptionVaultWithMToken.test.ts +++ b/test/unit/RedemptionVaultWithMToken.test.ts @@ -996,7 +996,7 @@ describe('RedemptionVaultWithMToken', function () { stableCoins.dai, 100, { - revertMessage: 'ERC20: burn amount exceeds balance', + revertMessage: 'ERC20: transfer amount exceeds balance', }, ); }); diff --git a/test/unit/RedemptionVaultWithMorpho.test.ts b/test/unit/RedemptionVaultWithMorpho.test.ts index 1517a731..89544e89 100644 --- a/test/unit/RedemptionVaultWithMorpho.test.ts +++ b/test/unit/RedemptionVaultWithMorpho.test.ts @@ -16,7 +16,10 @@ import { pauseVaultFn, } from '../common/common.helpers'; import { setRoundData } from '../common/data-feed.helpers'; -import { defaultDeploy } from '../common/fixtures'; +import { + defaultDeploy, + mTokenMinBalanceRedemptionFixture, +} from '../common/fixtures'; import { addPaymentTokenTest, setInstantFeeTest, @@ -948,6 +951,37 @@ describe('RedemptionVaultWithMorpho', function () { }); describe('redeemInstant()', () => { + it('with min-balance mToken - redeems the full balance after transferring the fee', async () => { + const { + feeReceiver, + mTokenMinBalance, + paymentToken, + redemptionVaultWithMorpho, + regularAccounts, + } = await mTokenMinBalanceRedemptionFixture(); + const user = regularAccounts[0]; + const amount = parseUnits('1.1'); + + await mTokenMinBalance.mint(user.address, amount); + await mTokenMinBalance + .connect(user) + .approve(redemptionVaultWithMorpho.address, amount); + + await expect( + redemptionVaultWithMorpho + .connect(user) + ['redeemInstant(address,uint256,uint256)']( + paymentToken.address, + amount, + 0, + ), + ).not.reverted; + expect(await mTokenMinBalance.balanceOf(user.address)).eq(0); + expect(await mTokenMinBalance.balanceOf(feeReceiver.address)).eq( + parseUnits('0.0055'), + ); + }); + it('should fail: when there is no token in vault', async () => { const { owner, @@ -1108,7 +1142,7 @@ describe('RedemptionVaultWithMorpho', function () { stableCoins.usdc, 100, { - revertMessage: 'ERC20: burn amount exceeds balance', + revertMessage: 'ERC20: transfer amount exceeds balance', }, ); }); diff --git a/test/unit/RedemptionVaultWithUSTB.test.ts b/test/unit/RedemptionVaultWithUSTB.test.ts index 9ce5b13d..58b7d1c8 100644 --- a/test/unit/RedemptionVaultWithUSTB.test.ts +++ b/test/unit/RedemptionVaultWithUSTB.test.ts @@ -1444,7 +1444,7 @@ describe('RedemptionVaultWithUSTB', function () { stableCoins.usdc, 100, { - revertMessage: 'ERC20: burn amount exceeds balance', + revertMessage: 'ERC20: transfer amount exceeds balance', }, ); }); From 0452797ea57d29179a0fcea0bdfaa96f17be82a8 Mon Sep 17 00:00:00 2001 From: Dmytro Horbatenko Date: Mon, 27 Jul 2026 18:54:42 +0300 Subject: [PATCH 15/19] fix: reuse redemption helpers for min-balance flows --- test/common/redemption-vault.helpers.ts | 24 +++---- test/unit/RedemptionVault.test.ts | 73 ++++++++++++--------- test/unit/RedemptionVaultWithMorpho.test.ts | 27 ++++---- 3 files changed, 69 insertions(+), 55 deletions(-) diff --git a/test/common/redemption-vault.helpers.ts b/test/common/redemption-vault.helpers.ts index 083d8aa1..42ae843b 100644 --- a/test/common/redemption-vault.helpers.ts +++ b/test/common/redemption-vault.helpers.ts @@ -16,8 +16,6 @@ import { ERC20, ERC20__factory, IERC20, - MTBILL, - MToken, RedemptionVault, RedemptionVaultWIthBUIDL, RedemptionVaultWithAave, @@ -28,7 +26,7 @@ import { } from '../../typechain-types'; type CommonParamsRedeem = { - mTBILL: MToken | MTBILL; + mTBILL: IERC20; } & Pick< Awaited>, 'owner' | 'mTokenToUsdDataFeed' @@ -137,6 +135,10 @@ export const redeemInstantTest = async ( amountIn, true, ); + const tokenDecimals = await tokenContract.decimals(); + const eventAmountOut = amountOut.mul( + BigNumber.from(10).pow(18 - tokenDecimals), + ); await expect(callFn()) .to.emit( @@ -149,14 +151,14 @@ export const redeemInstantTest = async ( ) .withArgs( ...[ - sender, + sender.address, tokenOut, withRecipient ? recipient : undefined, - amountTBillIn, + amountIn, fee, - amountOut, + eventAmountOut, ].filter((v) => v !== undefined), - ).to.not.reverted; + ); const balanceAfterUser = await mTBILL.balanceOf(sender.address); const balanceAfterReceiver = await mTBILL.balanceOf(tokensReceiver); @@ -276,14 +278,14 @@ export const redeemRequestTest = async ( ) .withArgs( ...[ - latestRequestIdBefore.add(1), - sender, + latestRequestIdBefore, + sender.address, tokenOut, withRecipient ? recipient : undefined, - amountTBillIn, + amountIn, fee, ].filter((v) => v !== undefined), - ).to.not.reverted; + ); const latestRequestIdAfter = await redemptionVault.currentRequestId(); const request = await redemptionVault.redeemRequests(latestRequestIdBefore); diff --git a/test/unit/RedemptionVault.test.ts b/test/unit/RedemptionVault.test.ts index 04a82ae0..ac3d20f7 100644 --- a/test/unit/RedemptionVault.test.ts +++ b/test/unit/RedemptionVault.test.ts @@ -1474,8 +1474,9 @@ describe('RedemptionVault', function () { describe('redeemInstant()', () => { it('with min-balance mToken - redeems the full balance after transferring the fee', async () => { const { - feeReceiver, mTokenMinBalance, + mTokenToUsdDataFeed, + owner, paymentToken, redemptionVault, regularAccounts, @@ -1488,24 +1489,26 @@ describe('RedemptionVault', function () { .connect(user) .approve(redemptionVault.address, amount); - await expect( - redemptionVault - .connect(user) - ['redeemInstant(address,uint256,uint256)']( - paymentToken.address, - amount, - 0, - ), - ).not.reverted; - expect(await mTokenMinBalance.balanceOf(user.address)).eq(0); - expect(await mTokenMinBalance.balanceOf(feeReceiver.address)).eq( - parseUnits('0.0055'), + await redeemInstantTest( + { + redemptionVault, + owner, + mTBILL: mTokenMinBalance, + mTokenToUsdDataFeed, + }, + paymentToken, + 1.1, + { from: user }, ); + + expect(await mTokenMinBalance.balanceOf(user.address)).eq(0); }); it('with min-balance mToken - allows a redemption that leaves exactly one token', async () => { const { mTokenMinBalance, + mTokenToUsdDataFeed, + owner, paymentToken, redemptionVault, regularAccounts, @@ -1518,15 +1521,18 @@ describe('RedemptionVault', function () { .connect(user) .approve(redemptionVault.address, amount); - await expect( - redemptionVault - .connect(user) - ['redeemInstant(address,uint256,uint256)']( - paymentToken.address, - amount, - 0, - ), - ).not.reverted; + await redeemInstantTest( + { + redemptionVault, + owner, + mTBILL: mTokenMinBalance, + mTokenToUsdDataFeed, + }, + paymentToken, + 1.1, + { from: user }, + ); + expect(await mTokenMinBalance.balanceOf(user.address)).eq( parseUnits('1'), ); @@ -2906,8 +2912,9 @@ describe('RedemptionVault', function () { describe('redeemRequest()', () => { it('with min-balance mToken - requests the full balance after transferring the fee', async () => { const { - feeReceiver, mTokenMinBalance, + mTokenToUsdDataFeed, + owner, paymentToken, redemptionVault, regularAccounts, @@ -2924,15 +2931,19 @@ describe('RedemptionVault', function () { .connect(user) .approve(redemptionVault.address, amount); - await expect( - redemptionVault - .connect(user) - ['redeemRequest(address,uint256)'](paymentToken.address, amount), - ).not.reverted; - expect(await mTokenMinBalance.balanceOf(user.address)).eq(0); - expect(await mTokenMinBalance.balanceOf(feeReceiver.address)).eq( - parseUnits('0.0055'), + await redeemRequestTest( + { + redemptionVault, + owner, + mTBILL: mTokenMinBalance, + mTokenToUsdDataFeed, + }, + paymentToken, + 1.1, + { from: user }, ); + + expect(await mTokenMinBalance.balanceOf(user.address)).eq(0); expect(await mTokenMinBalance.balanceOf(redemptionVault.address)).eq( parseUnits('1.0945'), ); diff --git a/test/unit/RedemptionVaultWithMorpho.test.ts b/test/unit/RedemptionVaultWithMorpho.test.ts index 89544e89..8321a3c4 100644 --- a/test/unit/RedemptionVaultWithMorpho.test.ts +++ b/test/unit/RedemptionVaultWithMorpho.test.ts @@ -953,8 +953,9 @@ describe('RedemptionVaultWithMorpho', function () { describe('redeemInstant()', () => { it('with min-balance mToken - redeems the full balance after transferring the fee', async () => { const { - feeReceiver, mTokenMinBalance, + mTokenToUsdDataFeed, + owner, paymentToken, redemptionVaultWithMorpho, regularAccounts, @@ -967,19 +968,19 @@ describe('RedemptionVaultWithMorpho', function () { .connect(user) .approve(redemptionVaultWithMorpho.address, amount); - await expect( - redemptionVaultWithMorpho - .connect(user) - ['redeemInstant(address,uint256,uint256)']( - paymentToken.address, - amount, - 0, - ), - ).not.reverted; - expect(await mTokenMinBalance.balanceOf(user.address)).eq(0); - expect(await mTokenMinBalance.balanceOf(feeReceiver.address)).eq( - parseUnits('0.0055'), + await redeemInstantTest( + { + redemptionVault: redemptionVaultWithMorpho, + owner, + mTBILL: mTokenMinBalance, + mTokenToUsdDataFeed, + }, + paymentToken, + 1.1, + { from: user }, ); + + expect(await mTokenMinBalance.balanceOf(user.address)).eq(0); }); it('should fail: when there is no token in vault', async () => { From 0fb1ecbc574dc88fd6eb547faf9ff952a7887f76 Mon Sep 17 00:00:00 2001 From: Dmytro Horbatenko Date: Tue, 28 Jul 2026 11:16:38 +0300 Subject: [PATCH 16/19] fix: cover min-balance redemptions across all vaults --- test/common/fixtures.ts | 202 ++++++++++++++++++- test/unit/RedemptionVaultWithAave.test.ts | 136 ++++++++++++- test/unit/RedemptionVaultWithBUIDL.test.ts | 136 ++++++++++++- test/unit/RedemptionVaultWithMToken.test.ts | 137 ++++++++++++- test/unit/RedemptionVaultWithMorpho.test.ts | 99 +++++++++ test/unit/RedemptionVaultWithSwapper.test.ts | 137 ++++++++++++- test/unit/RedemptionVaultWithUSTB.test.ts | 136 ++++++++++++- 7 files changed, 977 insertions(+), 6 deletions(-) diff --git a/test/common/fixtures.ts b/test/common/fixtures.ts index d2675e47..094d6717 100644 --- a/test/common/fixtures.ts +++ b/test/common/fixtures.ts @@ -1027,8 +1027,11 @@ export const mTokenMinBalanceRedemptionFixture = async () => { const fx = await mTokenMinBalanceFixture(); const { accessControl, + aavePoolMock, + buidlRedemption, dataFeed, feeReceiver, + liquidityProvider, mockedSanctionsList, morphoVaultMock, mTokenMinBalance, @@ -1038,6 +1041,7 @@ export const mTokenMinBalanceRedemptionFixture = async () => { requestRedeemer, stableCoins, tokensReceiver, + ustbRedemption, } = fx; const redemptionVault = await new RedemptionVaultTest__factory( @@ -1068,6 +1072,69 @@ export const mTokenMinBalanceRedemptionFixture = async () => { requestRedeemer.address, ); + const redemptionVaultWithAave = + await new RedemptionVaultWithAaveTest__factory(owner).deploy(); + await redemptionVaultWithAave.initialize( + accessControl.address, + { + mToken: mTokenMinBalance.address, + mTokenDataFeed: mTokenToUsdDataFeed.address, + }, + { + feeReceiver: feeReceiver.address, + tokensReceiver: tokensReceiver.address, + }, + { + instantFee: parseUnits('0.5', 2), + instantDailyLimit: parseUnits('500000'), + }, + mockedSanctionsList.address, + parseUnits('2', 2), + parseUnits('1.1'), + { + fiatAdditionalFee: parseUnits('0.1', 2), + fiatFlatFee: parseUnits('30'), + minFiatRedeemAmount: parseUnits('1000'), + }, + requestRedeemer.address, + ); + await redemptionVaultWithAave.setAavePool( + stableCoins.usdc.address, + aavePoolMock.address, + ); + + const redemptionVaultWithBUIDL = + await new RedemptionVaultWithBUIDLTest__factory(owner).deploy(); + await redemptionVaultWithBUIDL[ + 'initialize(address,(address,address),(address,address),(uint256,uint256),address,uint256,uint256,(uint256,uint256,uint256),address,address,uint256,uint256)' + ]( + accessControl.address, + { + mToken: mTokenMinBalance.address, + mTokenDataFeed: mTokenToUsdDataFeed.address, + }, + { + feeReceiver: feeReceiver.address, + tokensReceiver: tokensReceiver.address, + }, + { + instantFee: parseUnits('0.5', 2), + instantDailyLimit: parseUnits('500000'), + }, + mockedSanctionsList.address, + parseUnits('2', 2), + parseUnits('1.1'), + { + fiatAdditionalFee: parseUnits('0.1', 2), + fiatFlatFee: parseUnits('30'), + minFiatRedeemAmount: parseUnits('1000'), + }, + requestRedeemer.address, + buidlRedemption.address, + parseUnits('250000', 6), + parseUnits('250000', 6), + ); + const redemptionVaultWithMorpho = await new RedemptionVaultWithMorphoTest__factory(owner).deploy(); await redemptionVaultWithMorpho.initialize( @@ -1095,6 +1162,97 @@ export const mTokenMinBalanceRedemptionFixture = async () => { requestRedeemer.address, ); + const redemptionVaultWithMToken = + await new RedemptionVaultWithMTokenTest__factory(owner).deploy(); + await redemptionVaultWithMToken[ + 'initialize(address,(address,address),(address,address),(uint256,uint256),address,uint256,uint256,(uint256,uint256,uint256),address,address)' + ]( + accessControl.address, + { + mToken: mTokenMinBalance.address, + mTokenDataFeed: mTokenToUsdDataFeed.address, + }, + { + feeReceiver: feeReceiver.address, + tokensReceiver: tokensReceiver.address, + }, + { + instantFee: parseUnits('0.5', 2), + instantDailyLimit: parseUnits('500000'), + }, + mockedSanctionsList.address, + parseUnits('2', 2), + parseUnits('1.1'), + { + fiatAdditionalFee: parseUnits('0.1', 2), + fiatFlatFee: parseUnits('30'), + minFiatRedeemAmount: parseUnits('1000'), + }, + requestRedeemer.address, + redemptionVault.address, + ); + + const redemptionVaultWithUSTB = + await new RedemptionVaultWithUSTBTest__factory(owner).deploy(); + await redemptionVaultWithUSTB[ + 'initialize(address,(address,address),(address,address),(uint256,uint256),address,uint256,uint256,(uint256,uint256,uint256),address,address)' + ]( + accessControl.address, + { + mToken: mTokenMinBalance.address, + mTokenDataFeed: mTokenToUsdDataFeed.address, + }, + { + feeReceiver: feeReceiver.address, + tokensReceiver: tokensReceiver.address, + }, + { + instantFee: parseUnits('0.5', 2), + instantDailyLimit: parseUnits('500000'), + }, + mockedSanctionsList.address, + parseUnits('2', 2), + parseUnits('1.1'), + { + fiatAdditionalFee: parseUnits('0.1', 2), + fiatFlatFee: parseUnits('30'), + minFiatRedeemAmount: parseUnits('1000'), + }, + requestRedeemer.address, + ustbRedemption.address, + ); + + const redemptionVaultWithSwapper = + await new RedemptionVaultWithSwapperTest__factory(owner).deploy(); + await redemptionVaultWithSwapper[ + 'initialize(address,(address,address),(address,address),(uint256,uint256),address,uint256,uint256,(uint256,uint256,uint256),address,address,address)' + ]( + accessControl.address, + { + mToken: mTokenMinBalance.address, + mTokenDataFeed: mTokenToUsdDataFeed.address, + }, + { + feeReceiver: feeReceiver.address, + tokensReceiver: tokensReceiver.address, + }, + { + instantFee: parseUnits('0.5', 2), + instantDailyLimit: parseUnits('500000'), + }, + mockedSanctionsList.address, + parseUnits('2', 2), + parseUnits('1.1'), + { + fiatAdditionalFee: parseUnits('0.1', 2), + fiatFlatFee: parseUnits('30'), + minFiatRedeemAmount: parseUnits('1000'), + }, + requestRedeemer.address, + redemptionVault.address, + liquidityProvider.address, + ); + await accessControl.grantRole( mTokenMinBalanceRoles.burn, redemptionVault.address, @@ -1103,13 +1261,41 @@ export const mTokenMinBalanceRedemptionFixture = async () => { mTokenMinBalanceRoles.burn, redemptionVaultWithMorpho.address, ); + await accessControl.grantRole( + mTokenMinBalanceRoles.burn, + redemptionVaultWithAave.address, + ); + await accessControl.grantRole( + mTokenMinBalanceRoles.burn, + redemptionVaultWithBUIDL.address, + ); + await accessControl.grantRole( + mTokenMinBalanceRoles.burn, + redemptionVaultWithMToken.address, + ); + await accessControl.grantRole( + mTokenMinBalanceRoles.burn, + redemptionVaultWithSwapper.address, + ); + await accessControl.grantRole( + mTokenMinBalanceRoles.burn, + redemptionVaultWithUSTB.address, + ); await accessControl.grantRole( mTokenMinBalanceRoles.minBalanceExempt, feeReceiver.address, ); const paymentToken = stableCoins.usdc; - for (const vault of [redemptionVault, redemptionVaultWithMorpho]) { + for (const vault of [ + redemptionVault, + redemptionVaultWithAave, + redemptionVaultWithBUIDL, + redemptionVaultWithMToken, + redemptionVaultWithMorpho, + redemptionVaultWithSwapper, + redemptionVaultWithUSTB, + ]) { await vault.addPaymentToken( paymentToken.address, dataFeed.address, @@ -1130,12 +1316,26 @@ export const mTokenMinBalanceRedemptionFixture = async () => { redemptionVaultWithMorpho.address, parseUnits('100', 8), ); + for (const vault of [ + redemptionVaultWithAave, + redemptionVaultWithBUIDL, + redemptionVaultWithMToken, + redemptionVaultWithSwapper, + redemptionVaultWithUSTB, + ]) { + await paymentToken.mint(vault.address, parseUnits('100', 8)); + } return { ...fx, paymentToken, redemptionVault, + redemptionVaultWithAave, + redemptionVaultWithBUIDL, redemptionVaultWithMorpho, + redemptionVaultWithMToken, + redemptionVaultWithSwapper, + redemptionVaultWithUSTB, }; }; diff --git a/test/unit/RedemptionVaultWithAave.test.ts b/test/unit/RedemptionVaultWithAave.test.ts index 5df78e89..052e983a 100644 --- a/test/unit/RedemptionVaultWithAave.test.ts +++ b/test/unit/RedemptionVaultWithAave.test.ts @@ -16,7 +16,10 @@ import { pauseVaultFn, } from '../common/common.helpers'; import { setRoundData } from '../common/data-feed.helpers'; -import { defaultDeploy } from '../common/fixtures'; +import { + defaultDeploy, + mTokenMinBalanceRedemptionFixture, +} from '../common/fixtures'; import { addPaymentTokenTest, setInstantFeeTest, @@ -862,6 +865,137 @@ describe('RedemptionVaultWithAave', function () { }); describe('redeemInstant()', () => { + it('with min-balance mToken - redeems the full balance after transferring the fee', async () => { + const { + mTokenMinBalance, + mTokenToUsdDataFeed, + owner, + paymentToken, + redemptionVaultWithAave, + regularAccounts, + } = await mTokenMinBalanceRedemptionFixture(); + const user = regularAccounts[0]; + const amount = parseUnits('1.1'); + + await mTokenMinBalance.mint(user.address, amount); + await mTokenMinBalance + .connect(user) + .approve(redemptionVaultWithAave.address, amount); + + await redeemInstantTest( + { + redemptionVault: redemptionVaultWithAave, + owner, + mTBILL: mTokenMinBalance, + mTokenToUsdDataFeed, + }, + paymentToken, + 1.1, + { from: user }, + ); + + expect(await mTokenMinBalance.balanceOf(user.address)).eq(0); + }); + + it('with min-balance mToken - allows a redemption that leaves exactly one token', async () => { + const { + mTokenMinBalance, + mTokenToUsdDataFeed, + owner, + paymentToken, + redemptionVaultWithAave, + regularAccounts, + } = await mTokenMinBalanceRedemptionFixture(); + const user = regularAccounts[0]; + const amount = parseUnits('1.1'); + + await mTokenMinBalance.mint(user.address, parseUnits('2.1')); + await mTokenMinBalance + .connect(user) + .approve(redemptionVaultWithAave.address, amount); + + await redeemInstantTest( + { + redemptionVault: redemptionVaultWithAave, + owner, + mTBILL: mTokenMinBalance, + mTokenToUsdDataFeed, + }, + paymentToken, + 1.1, + { from: user }, + ); + + expect(await mTokenMinBalance.balanceOf(user.address)).eq( + parseUnits('1'), + ); + }); + + it('should fail: with min-balance mToken - redemption would leave dust', async () => { + const { + feeReceiver, + mTokenMinBalance, + paymentToken, + redemptionVaultWithAave, + regularAccounts, + } = await mTokenMinBalanceRedemptionFixture(); + const user = regularAccounts[0]; + const amount = parseUnits('1.1'); + const balance = parseUnits('2'); + + await mTokenMinBalance.mint(user.address, balance); + await mTokenMinBalance + .connect(user) + .approve(redemptionVaultWithAave.address, amount); + + await expect( + redemptionVaultWithAave + .connect(user) + ['redeemInstant(address,uint256,uint256)']( + paymentToken.address, + amount, + 0, + ), + ).revertedWith('MTMB: min balance not met'); + expect(await mTokenMinBalance.balanceOf(user.address)).eq(balance); + expect(await mTokenMinBalance.balanceOf(feeReceiver.address)).eq(0); + }); + + it('should fail: with min-balance mToken - fee receiver is not exempt', async () => { + const { + accessControl, + feeReceiver, + mTokenMinBalance, + mTokenMinBalanceRoles, + paymentToken, + redemptionVaultWithAave, + regularAccounts, + } = await mTokenMinBalanceRedemptionFixture(); + const user = regularAccounts[0]; + const amount = parseUnits('1.1'); + + await accessControl.revokeRole( + mTokenMinBalanceRoles.minBalanceExempt, + feeReceiver.address, + ); + await mTokenMinBalance.mint(user.address, amount); + await mTokenMinBalance + .connect(user) + .approve(redemptionVaultWithAave.address, amount); + + await expect( + redemptionVaultWithAave + .connect(user) + ['redeemInstant(address,uint256,uint256)']( + paymentToken.address, + amount, + 0, + ), + ).revertedWith('MTMB: min balance not met'); + expect(await mTokenMinBalance.balanceOf(user.address)).eq(amount); + expect(await mTokenMinBalance.balanceOf(feeReceiver.address)).eq(0); + }); + it('should fail: when there is no token in vault', async () => { const { owner, diff --git a/test/unit/RedemptionVaultWithBUIDL.test.ts b/test/unit/RedemptionVaultWithBUIDL.test.ts index e228a821..1f74ddd5 100644 --- a/test/unit/RedemptionVaultWithBUIDL.test.ts +++ b/test/unit/RedemptionVaultWithBUIDL.test.ts @@ -19,7 +19,10 @@ import { pauseVaultFn, } from '../common/common.helpers'; import { setRoundData } from '../common/data-feed.helpers'; -import { defaultDeploy } from '../common/fixtures'; +import { + defaultDeploy, + mTokenMinBalanceRedemptionFixture, +} from '../common/fixtures'; import { addPaymentTokenTest, addWaivedFeeAccountTest, @@ -1251,6 +1254,137 @@ describe('RedemptionVaultWithBUIDL', function () { }); describe('redeemInstant()', () => { + it('with min-balance mToken - redeems the full balance after transferring the fee', async () => { + const { + mTokenMinBalance, + mTokenToUsdDataFeed, + owner, + paymentToken, + redemptionVaultWithBUIDL, + regularAccounts, + } = await mTokenMinBalanceRedemptionFixture(); + const user = regularAccounts[0]; + const amount = parseUnits('1.1'); + + await mTokenMinBalance.mint(user.address, amount); + await mTokenMinBalance + .connect(user) + .approve(redemptionVaultWithBUIDL.address, amount); + + await redeemInstantTest( + { + redemptionVault: redemptionVaultWithBUIDL, + owner, + mTBILL: mTokenMinBalance, + mTokenToUsdDataFeed, + }, + paymentToken, + 1.1, + { from: user }, + ); + + expect(await mTokenMinBalance.balanceOf(user.address)).eq(0); + }); + + it('with min-balance mToken - allows a redemption that leaves exactly one token', async () => { + const { + mTokenMinBalance, + mTokenToUsdDataFeed, + owner, + paymentToken, + redemptionVaultWithBUIDL, + regularAccounts, + } = await mTokenMinBalanceRedemptionFixture(); + const user = regularAccounts[0]; + const amount = parseUnits('1.1'); + + await mTokenMinBalance.mint(user.address, parseUnits('2.1')); + await mTokenMinBalance + .connect(user) + .approve(redemptionVaultWithBUIDL.address, amount); + + await redeemInstantTest( + { + redemptionVault: redemptionVaultWithBUIDL, + owner, + mTBILL: mTokenMinBalance, + mTokenToUsdDataFeed, + }, + paymentToken, + 1.1, + { from: user }, + ); + + expect(await mTokenMinBalance.balanceOf(user.address)).eq( + parseUnits('1'), + ); + }); + + it('should fail: with min-balance mToken - redemption would leave dust', async () => { + const { + feeReceiver, + mTokenMinBalance, + paymentToken, + redemptionVaultWithBUIDL, + regularAccounts, + } = await mTokenMinBalanceRedemptionFixture(); + const user = regularAccounts[0]; + const amount = parseUnits('1.1'); + const balance = parseUnits('2'); + + await mTokenMinBalance.mint(user.address, balance); + await mTokenMinBalance + .connect(user) + .approve(redemptionVaultWithBUIDL.address, amount); + + await expect( + redemptionVaultWithBUIDL + .connect(user) + ['redeemInstant(address,uint256,uint256)']( + paymentToken.address, + amount, + 0, + ), + ).revertedWith('MTMB: min balance not met'); + expect(await mTokenMinBalance.balanceOf(user.address)).eq(balance); + expect(await mTokenMinBalance.balanceOf(feeReceiver.address)).eq(0); + }); + + it('should fail: with min-balance mToken - fee receiver is not exempt', async () => { + const { + accessControl, + feeReceiver, + mTokenMinBalance, + mTokenMinBalanceRoles, + paymentToken, + redemptionVaultWithBUIDL, + regularAccounts, + } = await mTokenMinBalanceRedemptionFixture(); + const user = regularAccounts[0]; + const amount = parseUnits('1.1'); + + await accessControl.revokeRole( + mTokenMinBalanceRoles.minBalanceExempt, + feeReceiver.address, + ); + await mTokenMinBalance.mint(user.address, amount); + await mTokenMinBalance + .connect(user) + .approve(redemptionVaultWithBUIDL.address, amount); + + await expect( + redemptionVaultWithBUIDL + .connect(user) + ['redeemInstant(address,uint256,uint256)']( + paymentToken.address, + amount, + 0, + ), + ).revertedWith('MTMB: min balance not met'); + expect(await mTokenMinBalance.balanceOf(user.address)).eq(amount); + expect(await mTokenMinBalance.balanceOf(feeReceiver.address)).eq(0); + }); + it('should fail: when there is no token in vault', async () => { const { owner, diff --git a/test/unit/RedemptionVaultWithMToken.test.ts b/test/unit/RedemptionVaultWithMToken.test.ts index 4ee0ac0c..a68dc05e 100644 --- a/test/unit/RedemptionVaultWithMToken.test.ts +++ b/test/unit/RedemptionVaultWithMToken.test.ts @@ -16,7 +16,10 @@ import { pauseVaultFn, } from '../common/common.helpers'; import { setRoundData } from '../common/data-feed.helpers'; -import { defaultDeploy } from '../common/fixtures'; +import { + defaultDeploy, + mTokenMinBalanceRedemptionFixture, +} from '../common/fixtures'; import { addPaymentTokenTest, setInstantFeeTest, @@ -34,6 +37,7 @@ import { redeemInstantWithMTokenTest } from '../common/redemption-vault-mtoken.h import { approveRedeemRequestTest, redeemFiatRequestTest, + redeemInstantTest, redeemRequestTest, rejectRedeemRequestTest, safeApproveRedeemRequestTest, @@ -818,6 +822,137 @@ describe('RedemptionVaultWithMToken', function () { }); describe('redeemInstant()', () => { + it('with min-balance mToken - redeems the full balance after transferring the fee', async () => { + const { + mTokenMinBalance, + mTokenToUsdDataFeed, + owner, + paymentToken, + redemptionVaultWithMToken, + regularAccounts, + } = await mTokenMinBalanceRedemptionFixture(); + const user = regularAccounts[0]; + const amount = parseUnits('1.1'); + + await mTokenMinBalance.mint(user.address, amount); + await mTokenMinBalance + .connect(user) + .approve(redemptionVaultWithMToken.address, amount); + + await redeemInstantTest( + { + redemptionVault: redemptionVaultWithMToken, + owner, + mTBILL: mTokenMinBalance, + mTokenToUsdDataFeed, + }, + paymentToken, + 1.1, + { from: user }, + ); + + expect(await mTokenMinBalance.balanceOf(user.address)).eq(0); + }); + + it('with min-balance mToken - allows a redemption that leaves exactly one token', async () => { + const { + mTokenMinBalance, + mTokenToUsdDataFeed, + owner, + paymentToken, + redemptionVaultWithMToken, + regularAccounts, + } = await mTokenMinBalanceRedemptionFixture(); + const user = regularAccounts[0]; + const amount = parseUnits('1.1'); + + await mTokenMinBalance.mint(user.address, parseUnits('2.1')); + await mTokenMinBalance + .connect(user) + .approve(redemptionVaultWithMToken.address, amount); + + await redeemInstantTest( + { + redemptionVault: redemptionVaultWithMToken, + owner, + mTBILL: mTokenMinBalance, + mTokenToUsdDataFeed, + }, + paymentToken, + 1.1, + { from: user }, + ); + + expect(await mTokenMinBalance.balanceOf(user.address)).eq( + parseUnits('1'), + ); + }); + + it('should fail: with min-balance mToken - redemption would leave dust', async () => { + const { + feeReceiver, + mTokenMinBalance, + paymentToken, + redemptionVaultWithMToken, + regularAccounts, + } = await mTokenMinBalanceRedemptionFixture(); + const user = regularAccounts[0]; + const amount = parseUnits('1.1'); + const balance = parseUnits('2'); + + await mTokenMinBalance.mint(user.address, balance); + await mTokenMinBalance + .connect(user) + .approve(redemptionVaultWithMToken.address, amount); + + await expect( + redemptionVaultWithMToken + .connect(user) + ['redeemInstant(address,uint256,uint256)']( + paymentToken.address, + amount, + 0, + ), + ).revertedWith('MTMB: min balance not met'); + expect(await mTokenMinBalance.balanceOf(user.address)).eq(balance); + expect(await mTokenMinBalance.balanceOf(feeReceiver.address)).eq(0); + }); + + it('should fail: with min-balance mToken - fee receiver is not exempt', async () => { + const { + accessControl, + feeReceiver, + mTokenMinBalance, + mTokenMinBalanceRoles, + paymentToken, + redemptionVaultWithMToken, + regularAccounts, + } = await mTokenMinBalanceRedemptionFixture(); + const user = regularAccounts[0]; + const amount = parseUnits('1.1'); + + await accessControl.revokeRole( + mTokenMinBalanceRoles.minBalanceExempt, + feeReceiver.address, + ); + await mTokenMinBalance.mint(user.address, amount); + await mTokenMinBalance + .connect(user) + .approve(redemptionVaultWithMToken.address, amount); + + await expect( + redemptionVaultWithMToken + .connect(user) + ['redeemInstant(address,uint256,uint256)']( + paymentToken.address, + amount, + 0, + ), + ).revertedWith('MTMB: min balance not met'); + expect(await mTokenMinBalance.balanceOf(user.address)).eq(amount); + expect(await mTokenMinBalance.balanceOf(feeReceiver.address)).eq(0); + }); + it('should fail: when there is no token in vault', async () => { const { owner, diff --git a/test/unit/RedemptionVaultWithMorpho.test.ts b/test/unit/RedemptionVaultWithMorpho.test.ts index 8321a3c4..d234fe5f 100644 --- a/test/unit/RedemptionVaultWithMorpho.test.ts +++ b/test/unit/RedemptionVaultWithMorpho.test.ts @@ -951,6 +951,105 @@ describe('RedemptionVaultWithMorpho', function () { }); describe('redeemInstant()', () => { + it('with min-balance mToken - allows a redemption that leaves exactly one token', async () => { + const { + mTokenMinBalance, + mTokenToUsdDataFeed, + owner, + paymentToken, + redemptionVaultWithMorpho, + regularAccounts, + } = await mTokenMinBalanceRedemptionFixture(); + const user = regularAccounts[0]; + const amount = parseUnits('1.1'); + + await mTokenMinBalance.mint(user.address, parseUnits('2.1')); + await mTokenMinBalance + .connect(user) + .approve(redemptionVaultWithMorpho.address, amount); + + await redeemInstantTest( + { + redemptionVault: redemptionVaultWithMorpho, + owner, + mTBILL: mTokenMinBalance, + mTokenToUsdDataFeed, + }, + paymentToken, + 1.1, + { from: user }, + ); + + expect(await mTokenMinBalance.balanceOf(user.address)).eq( + parseUnits('1'), + ); + }); + + it('should fail: with min-balance mToken - redemption would leave dust', async () => { + const { + feeReceiver, + mTokenMinBalance, + paymentToken, + redemptionVaultWithMorpho, + regularAccounts, + } = await mTokenMinBalanceRedemptionFixture(); + const user = regularAccounts[0]; + const amount = parseUnits('1.1'); + const balance = parseUnits('2'); + + await mTokenMinBalance.mint(user.address, balance); + await mTokenMinBalance + .connect(user) + .approve(redemptionVaultWithMorpho.address, amount); + + await expect( + redemptionVaultWithMorpho + .connect(user) + ['redeemInstant(address,uint256,uint256)']( + paymentToken.address, + amount, + 0, + ), + ).revertedWith('MTMB: min balance not met'); + expect(await mTokenMinBalance.balanceOf(user.address)).eq(balance); + expect(await mTokenMinBalance.balanceOf(feeReceiver.address)).eq(0); + }); + + it('should fail: with min-balance mToken - fee receiver is not exempt', async () => { + const { + accessControl, + feeReceiver, + mTokenMinBalance, + mTokenMinBalanceRoles, + paymentToken, + redemptionVaultWithMorpho, + regularAccounts, + } = await mTokenMinBalanceRedemptionFixture(); + const user = regularAccounts[0]; + const amount = parseUnits('1.1'); + + await accessControl.revokeRole( + mTokenMinBalanceRoles.minBalanceExempt, + feeReceiver.address, + ); + await mTokenMinBalance.mint(user.address, amount); + await mTokenMinBalance + .connect(user) + .approve(redemptionVaultWithMorpho.address, amount); + + await expect( + redemptionVaultWithMorpho + .connect(user) + ['redeemInstant(address,uint256,uint256)']( + paymentToken.address, + amount, + 0, + ), + ).revertedWith('MTMB: min balance not met'); + expect(await mTokenMinBalance.balanceOf(user.address)).eq(amount); + expect(await mTokenMinBalance.balanceOf(feeReceiver.address)).eq(0); + }); + it('with min-balance mToken - redeems the full balance after transferring the fee', async () => { const { mTokenMinBalance, diff --git a/test/unit/RedemptionVaultWithSwapper.test.ts b/test/unit/RedemptionVaultWithSwapper.test.ts index f284be69..eaa0f23e 100644 --- a/test/unit/RedemptionVaultWithSwapper.test.ts +++ b/test/unit/RedemptionVaultWithSwapper.test.ts @@ -12,7 +12,10 @@ import { pauseVaultFn, } from '../common/common.helpers'; import { setRoundData } from '../common/data-feed.helpers'; -import { defaultDeploy } from '../common/fixtures'; +import { + defaultDeploy, + mTokenMinBalanceRedemptionFixture, +} from '../common/fixtures'; import { addPaymentTokenTest, changeTokenAllowanceTest, @@ -26,6 +29,7 @@ import { setLiquidityProviderTest, setSwapperVaultTest, } from '../common/redemption-vault-swapper.helpers'; +import { redeemInstantTest } from '../common/redemption-vault.helpers'; import { sanctionUser } from '../common/with-sanctions-list.helpers'; describe('MBasisRedemptionVaultWithSwapper', () => { @@ -230,6 +234,137 @@ describe('MBasisRedemptionVaultWithSwapper', () => { }); }); describe('redeemInstant()', () => { + it('with min-balance mToken - redeems the full balance after transferring the fee', async () => { + const { + mTokenMinBalance, + mTokenToUsdDataFeed, + owner, + paymentToken, + redemptionVaultWithSwapper, + regularAccounts, + } = await mTokenMinBalanceRedemptionFixture(); + const user = regularAccounts[0]; + const amount = parseUnits('1.1'); + + await mTokenMinBalance.mint(user.address, amount); + await mTokenMinBalance + .connect(user) + .approve(redemptionVaultWithSwapper.address, amount); + + await redeemInstantTest( + { + redemptionVault: redemptionVaultWithSwapper, + owner, + mTBILL: mTokenMinBalance, + mTokenToUsdDataFeed, + }, + paymentToken, + 1.1, + { from: user }, + ); + + expect(await mTokenMinBalance.balanceOf(user.address)).eq(0); + }); + + it('with min-balance mToken - allows a redemption that leaves exactly one token', async () => { + const { + mTokenMinBalance, + mTokenToUsdDataFeed, + owner, + paymentToken, + redemptionVaultWithSwapper, + regularAccounts, + } = await mTokenMinBalanceRedemptionFixture(); + const user = regularAccounts[0]; + const amount = parseUnits('1.1'); + + await mTokenMinBalance.mint(user.address, parseUnits('2.1')); + await mTokenMinBalance + .connect(user) + .approve(redemptionVaultWithSwapper.address, amount); + + await redeemInstantTest( + { + redemptionVault: redemptionVaultWithSwapper, + owner, + mTBILL: mTokenMinBalance, + mTokenToUsdDataFeed, + }, + paymentToken, + 1.1, + { from: user }, + ); + + expect(await mTokenMinBalance.balanceOf(user.address)).eq( + parseUnits('1'), + ); + }); + + it('should fail: with min-balance mToken - redemption would leave dust', async () => { + const { + feeReceiver, + mTokenMinBalance, + paymentToken, + redemptionVaultWithSwapper, + regularAccounts, + } = await mTokenMinBalanceRedemptionFixture(); + const user = regularAccounts[0]; + const amount = parseUnits('1.1'); + const balance = parseUnits('2'); + + await mTokenMinBalance.mint(user.address, balance); + await mTokenMinBalance + .connect(user) + .approve(redemptionVaultWithSwapper.address, amount); + + await expect( + redemptionVaultWithSwapper + .connect(user) + ['redeemInstant(address,uint256,uint256)']( + paymentToken.address, + amount, + 0, + ), + ).revertedWith('MTMB: min balance not met'); + expect(await mTokenMinBalance.balanceOf(user.address)).eq(balance); + expect(await mTokenMinBalance.balanceOf(feeReceiver.address)).eq(0); + }); + + it('should fail: with min-balance mToken - fee receiver is not exempt', async () => { + const { + accessControl, + feeReceiver, + mTokenMinBalance, + mTokenMinBalanceRoles, + paymentToken, + redemptionVaultWithSwapper, + regularAccounts, + } = await mTokenMinBalanceRedemptionFixture(); + const user = regularAccounts[0]; + const amount = parseUnits('1.1'); + + await accessControl.revokeRole( + mTokenMinBalanceRoles.minBalanceExempt, + feeReceiver.address, + ); + await mTokenMinBalance.mint(user.address, amount); + await mTokenMinBalance + .connect(user) + .approve(redemptionVaultWithSwapper.address, amount); + + await expect( + redemptionVaultWithSwapper + .connect(user) + ['redeemInstant(address,uint256,uint256)']( + paymentToken.address, + amount, + 0, + ), + ).revertedWith('MTMB: min balance not met'); + expect(await mTokenMinBalance.balanceOf(user.address)).eq(amount); + expect(await mTokenMinBalance.balanceOf(feeReceiver.address)).eq(0); + }); + it('should fail: when there is no token in vault', async () => { const { owner, diff --git a/test/unit/RedemptionVaultWithUSTB.test.ts b/test/unit/RedemptionVaultWithUSTB.test.ts index 58b7d1c8..a7e2f9e8 100644 --- a/test/unit/RedemptionVaultWithUSTB.test.ts +++ b/test/unit/RedemptionVaultWithUSTB.test.ts @@ -17,7 +17,10 @@ import { pauseVaultFn, } from '../common/common.helpers'; import { setRoundData } from '../common/data-feed.helpers'; -import { defaultDeploy } from '../common/fixtures'; +import { + defaultDeploy, + mTokenMinBalanceRedemptionFixture, +} from '../common/fixtures'; import { addPaymentTokenTest, setInstantFeeTest, @@ -1284,6 +1287,137 @@ describe('RedemptionVaultWithUSTB', function () { }); describe('redeemInstant()', () => { + it('with min-balance mToken - redeems the full balance after transferring the fee', async () => { + const { + mTokenMinBalance, + mTokenToUsdDataFeed, + owner, + paymentToken, + redemptionVaultWithUSTB, + regularAccounts, + } = await mTokenMinBalanceRedemptionFixture(); + const user = regularAccounts[0]; + const amount = parseUnits('1.1'); + + await mTokenMinBalance.mint(user.address, amount); + await mTokenMinBalance + .connect(user) + .approve(redemptionVaultWithUSTB.address, amount); + + await redeemInstantTest( + { + redemptionVault: redemptionVaultWithUSTB, + owner, + mTBILL: mTokenMinBalance, + mTokenToUsdDataFeed, + }, + paymentToken, + 1.1, + { from: user }, + ); + + expect(await mTokenMinBalance.balanceOf(user.address)).eq(0); + }); + + it('with min-balance mToken - allows a redemption that leaves exactly one token', async () => { + const { + mTokenMinBalance, + mTokenToUsdDataFeed, + owner, + paymentToken, + redemptionVaultWithUSTB, + regularAccounts, + } = await mTokenMinBalanceRedemptionFixture(); + const user = regularAccounts[0]; + const amount = parseUnits('1.1'); + + await mTokenMinBalance.mint(user.address, parseUnits('2.1')); + await mTokenMinBalance + .connect(user) + .approve(redemptionVaultWithUSTB.address, amount); + + await redeemInstantTest( + { + redemptionVault: redemptionVaultWithUSTB, + owner, + mTBILL: mTokenMinBalance, + mTokenToUsdDataFeed, + }, + paymentToken, + 1.1, + { from: user }, + ); + + expect(await mTokenMinBalance.balanceOf(user.address)).eq( + parseUnits('1'), + ); + }); + + it('should fail: with min-balance mToken - redemption would leave dust', async () => { + const { + feeReceiver, + mTokenMinBalance, + paymentToken, + redemptionVaultWithUSTB, + regularAccounts, + } = await mTokenMinBalanceRedemptionFixture(); + const user = regularAccounts[0]; + const amount = parseUnits('1.1'); + const balance = parseUnits('2'); + + await mTokenMinBalance.mint(user.address, balance); + await mTokenMinBalance + .connect(user) + .approve(redemptionVaultWithUSTB.address, amount); + + await expect( + redemptionVaultWithUSTB + .connect(user) + ['redeemInstant(address,uint256,uint256)']( + paymentToken.address, + amount, + 0, + ), + ).revertedWith('MTMB: min balance not met'); + expect(await mTokenMinBalance.balanceOf(user.address)).eq(balance); + expect(await mTokenMinBalance.balanceOf(feeReceiver.address)).eq(0); + }); + + it('should fail: with min-balance mToken - fee receiver is not exempt', async () => { + const { + accessControl, + feeReceiver, + mTokenMinBalance, + mTokenMinBalanceRoles, + paymentToken, + redemptionVaultWithUSTB, + regularAccounts, + } = await mTokenMinBalanceRedemptionFixture(); + const user = regularAccounts[0]; + const amount = parseUnits('1.1'); + + await accessControl.revokeRole( + mTokenMinBalanceRoles.minBalanceExempt, + feeReceiver.address, + ); + await mTokenMinBalance.mint(user.address, amount); + await mTokenMinBalance + .connect(user) + .approve(redemptionVaultWithUSTB.address, amount); + + await expect( + redemptionVaultWithUSTB + .connect(user) + ['redeemInstant(address,uint256,uint256)']( + paymentToken.address, + amount, + 0, + ), + ).revertedWith('MTMB: min balance not met'); + expect(await mTokenMinBalance.balanceOf(user.address)).eq(amount); + expect(await mTokenMinBalance.balanceOf(feeReceiver.address)).eq(0); + }); + it('should fail: when there is no token in vault', async () => { const { owner, From d97b00aef1c4d615982f4eb3827c33fe467a060d Mon Sep 17 00:00:00 2001 From: kostya-midas Date: Wed, 29 Jul 2026 12:02:34 +0300 Subject: [PATCH 17/19] chore: mwin mtoken rv upgrade --- .openzeppelin/mainnet.json | 565 +++++++++++++++++++- scripts/upgrades/configs/upgrade-configs.ts | 13 + 2 files changed, 560 insertions(+), 18 deletions(-) diff --git a/.openzeppelin/mainnet.json b/.openzeppelin/mainnet.json index e6fa6105..67f9e560 100644 --- a/.openzeppelin/mainnet.json +++ b/.openzeppelin/mainnet.json @@ -115871,7 +115871,7 @@ "label": "accessControl", "offset": 2, "slot": "0", - "type": "t_contract(MidasAccessControl)8606", + "type": "t_contract(MidasAccessControl)14032", "contract": "WithMidasAccessControl", "src": "contracts/access/WithMidasAccessControl.sol:24" }, @@ -115967,7 +115967,7 @@ "label": "currentRequestId", "offset": 0, "slot": "354", - "type": "t_struct(Counter)4184_storage", + "type": "t_struct(Counter)5102_storage", "contract": "ManageableVault", "src": "contracts/abstract/ManageableVault.sol:53" }, @@ -115975,7 +115975,7 @@ "label": "mToken", "offset": 0, "slot": "355", - "type": "t_contract(IMToken)8967", + "type": "t_contract(IMToken)14647", "contract": "ManageableVault", "src": "contracts/abstract/ManageableVault.sol:66" }, @@ -115983,7 +115983,7 @@ "label": "mTokenDataFeed", "offset": 0, "slot": "356", - "type": "t_contract(IDataFeed)8928", + "type": "t_contract(IDataFeed)14354", "contract": "ManageableVault", "src": "contracts/abstract/ManageableVault.sol:71" }, @@ -116055,7 +116055,7 @@ "label": "tokensConfig", "offset": 0, "slot": "366", - "type": "t_mapping(t_address,t_struct(TokenConfig)8980_storage)", + "type": "t_mapping(t_address,t_struct(TokenConfig)14660_storage)", "contract": "ManageableVault", "src": "contracts/abstract/ManageableVault.sol:118" }, @@ -116111,7 +116111,7 @@ "label": "redeemRequests", "offset": 0, "slot": "422", - "type": "t_mapping(t_uint256,t_struct(Request)9248_storage)", + "type": "t_mapping(t_uint256,t_struct(Request)14928_storage)", "contract": "RedemptionVault", "src": "contracts/RedemptionVault.sol:84" }, @@ -116143,7 +116143,7 @@ "label": "redemptionVault", "offset": 0, "slot": "524", - "type": "t_contract(IRedemptionVault)9485", + "type": "t_contract(IRedemptionVault)15165", "contract": "RedemptionVaultWithMToken", "src": "contracts/RedemptionVaultWithMToken.sol:32", "renamedFrom": "mTbillRedemptionVault" @@ -116203,23 +116203,23 @@ "label": "bytes4", "numberOfBytes": "4" }, - "t_contract(IDataFeed)8928": { + "t_contract(IDataFeed)14354": { "label": "contract IDataFeed", "numberOfBytes": "20" }, - "t_contract(IMToken)8967": { + "t_contract(IMToken)14647": { "label": "contract IMToken", "numberOfBytes": "20" }, - "t_contract(IRedemptionVault)9485": { + "t_contract(IRedemptionVault)15165": { "label": "contract IRedemptionVault", "numberOfBytes": "20" }, - "t_contract(MidasAccessControl)8606": { + "t_contract(MidasAccessControl)14032": { "label": "contract MidasAccessControl", "numberOfBytes": "20" }, - "t_enum(RequestStatus)8984": { + "t_enum(RequestStatus)14664": { "label": "enum RequestStatus", "members": ["Pending", "Processed", "Canceled"], "numberOfBytes": "1" @@ -116228,7 +116228,7 @@ "label": "mapping(address => bool)", "numberOfBytes": "32" }, - "t_mapping(t_address,t_struct(TokenConfig)8980_storage)": { + "t_mapping(t_address,t_struct(TokenConfig)14660_storage)": { "label": "mapping(address => struct TokenConfig)", "numberOfBytes": "32" }, @@ -116240,7 +116240,7 @@ "label": "mapping(bytes4 => bool)", "numberOfBytes": "32" }, - "t_mapping(t_uint256,t_struct(Request)9248_storage)": { + "t_mapping(t_uint256,t_struct(Request)14928_storage)": { "label": "mapping(uint256 => struct Request)", "numberOfBytes": "32" }, @@ -116260,7 +116260,7 @@ ], "numberOfBytes": "64" }, - "t_struct(Counter)4184_storage": { + "t_struct(Counter)5102_storage": { "label": "struct Counters.Counter", "members": [ { @@ -116272,7 +116272,7 @@ ], "numberOfBytes": "32" }, - "t_struct(Request)9248_storage": { + "t_struct(Request)14928_storage": { "label": "struct Request", "members": [ { @@ -116289,7 +116289,7 @@ }, { "label": "status", - "type": "t_enum(RequestStatus)8984", + "type": "t_enum(RequestStatus)14664", "offset": 20, "slot": "1" }, @@ -116332,7 +116332,7 @@ ], "numberOfBytes": "64" }, - "t_struct(TokenConfig)8980_storage": { + "t_struct(TokenConfig)14660_storage": { "label": "struct TokenConfig", "members": [ { @@ -119812,6 +119812,535 @@ } } } + }, + "cb519bdb0660028d1ba032c5d608c91f58897dac1f0f41c5201a8d5e2b0be96c": { + "address": "0x4F2B9102B436fC37e4667f8d93304F8f0a5Bc5bb", + "txHash": "0x923f6ff786648201c7519593c2f64ebfdf13a96324b599d317ce563bca322deb", + "layout": { + "solcVersion": "0.8.9", + "storage": [ + { + "label": "_initialized", + "offset": 0, + "slot": "0", + "type": "t_uint8", + "contract": "Initializable", + "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:63", + "retypedFrom": "bool" + }, + { + "label": "_initializing", + "offset": 1, + "slot": "0", + "type": "t_bool", + "contract": "Initializable", + "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:68" + }, + { + "label": "accessControl", + "offset": 2, + "slot": "0", + "type": "t_contract(MidasAccessControl)16693", + "contract": "WithMidasAccessControl", + "src": "contracts/access/WithMidasAccessControl.sol:24" + }, + { + "label": "__gap", + "offset": 0, + "slot": "1", + "type": "t_array(t_uint256)50_storage", + "contract": "WithMidasAccessControl", + "src": "contracts/access/WithMidasAccessControl.sol:29" + }, + { + "label": "__gap", + "offset": 0, + "slot": "51", + "type": "t_array(t_uint256)50_storage", + "contract": "ContextUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/utils/ContextUpgradeable.sol:36" + }, + { + "label": "_paused", + "offset": 0, + "slot": "101", + "type": "t_bool", + "contract": "PausableUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/security/PausableUpgradeable.sol:29" + }, + { + "label": "__gap", + "offset": 0, + "slot": "102", + "type": "t_array(t_uint256)49_storage", + "contract": "PausableUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/security/PausableUpgradeable.sol:116" + }, + { + "label": "fnPaused", + "offset": 0, + "slot": "151", + "type": "t_mapping(t_bytes4,t_bool)", + "contract": "Pausable", + "src": "contracts/access/Pausable.sol:14" + }, + { + "label": "__gap", + "offset": 0, + "slot": "152", + "type": "t_array(t_uint256)50_storage", + "contract": "Pausable", + "src": "contracts/access/Pausable.sol:19" + }, + { + "label": "__gap", + "offset": 0, + "slot": "202", + "type": "t_array(t_uint256)50_storage", + "contract": "Blacklistable", + "src": "contracts/access/Blacklistable.sol:16" + }, + { + "label": "greenlistEnabled", + "offset": 0, + "slot": "252", + "type": "t_bool", + "contract": "Greenlistable", + "src": "contracts/access/Greenlistable.sol:16" + }, + { + "label": "__gap", + "offset": 0, + "slot": "253", + "type": "t_array(t_uint256)50_storage", + "contract": "Greenlistable", + "src": "contracts/access/Greenlistable.sol:21" + }, + { + "label": "sanctionsList", + "offset": 0, + "slot": "303", + "type": "t_address", + "contract": "WithSanctionsList", + "src": "contracts/abstract/WithSanctionsList.sol:18" + }, + { + "label": "__gap", + "offset": 0, + "slot": "304", + "type": "t_array(t_uint256)50_storage", + "contract": "WithSanctionsList", + "src": "contracts/abstract/WithSanctionsList.sol:23" + }, + { + "label": "currentRequestId", + "offset": 0, + "slot": "354", + "type": "t_struct(Counter)7492_storage", + "contract": "ManageableVault", + "src": "contracts/abstract/ManageableVault.sol:53" + }, + { + "label": "mToken", + "offset": 0, + "slot": "355", + "type": "t_contract(IMToken)19433", + "contract": "ManageableVault", + "src": "contracts/abstract/ManageableVault.sol:66" + }, + { + "label": "mTokenDataFeed", + "offset": 0, + "slot": "356", + "type": "t_contract(IDataFeed)19140", + "contract": "ManageableVault", + "src": "contracts/abstract/ManageableVault.sol:71" + }, + { + "label": "tokensReceiver", + "offset": 0, + "slot": "357", + "type": "t_address", + "contract": "ManageableVault", + "src": "contracts/abstract/ManageableVault.sol:76" + }, + { + "label": "instantFee", + "offset": 0, + "slot": "358", + "type": "t_uint256", + "contract": "ManageableVault", + "src": "contracts/abstract/ManageableVault.sol:81" + }, + { + "label": "instantDailyLimit", + "offset": 0, + "slot": "359", + "type": "t_uint256", + "contract": "ManageableVault", + "src": "contracts/abstract/ManageableVault.sol:88" + }, + { + "label": "dailyLimits", + "offset": 0, + "slot": "360", + "type": "t_mapping(t_uint256,t_uint256)", + "contract": "ManageableVault", + "src": "contracts/abstract/ManageableVault.sol:93" + }, + { + "label": "feeReceiver", + "offset": 0, + "slot": "361", + "type": "t_address", + "contract": "ManageableVault", + "src": "contracts/abstract/ManageableVault.sol:98" + }, + { + "label": "variationTolerance", + "offset": 0, + "slot": "362", + "type": "t_uint256", + "contract": "ManageableVault", + "src": "contracts/abstract/ManageableVault.sol:103" + }, + { + "label": "waivedFeeRestriction", + "offset": 0, + "slot": "363", + "type": "t_mapping(t_address,t_bool)", + "contract": "ManageableVault", + "src": "contracts/abstract/ManageableVault.sol:108" + }, + { + "label": "_paymentTokens", + "offset": 0, + "slot": "364", + "type": "t_struct(AddressSet)3891_storage", + "contract": "ManageableVault", + "src": "contracts/abstract/ManageableVault.sol:113" + }, + { + "label": "tokensConfig", + "offset": 0, + "slot": "366", + "type": "t_mapping(t_address,t_struct(TokenConfig)19446_storage)", + "contract": "ManageableVault", + "src": "contracts/abstract/ManageableVault.sol:118" + }, + { + "label": "minAmount", + "offset": 0, + "slot": "367", + "type": "t_uint256", + "contract": "ManageableVault", + "src": "contracts/abstract/ManageableVault.sol:123" + }, + { + "label": "isFreeFromMinAmount", + "offset": 0, + "slot": "368", + "type": "t_mapping(t_address,t_bool)", + "contract": "ManageableVault", + "src": "contracts/abstract/ManageableVault.sol:128" + }, + { + "label": "__gap", + "offset": 0, + "slot": "369", + "type": "t_array(t_uint256)50_storage", + "contract": "ManageableVault", + "src": "contracts/abstract/ManageableVault.sol:133" + }, + { + "label": "minFiatRedeemAmount", + "offset": 0, + "slot": "419", + "type": "t_uint256", + "contract": "RedemptionVault", + "src": "contracts/RedemptionVault.sol:69" + }, + { + "label": "fiatAdditionalFee", + "offset": 0, + "slot": "420", + "type": "t_uint256", + "contract": "RedemptionVault", + "src": "contracts/RedemptionVault.sol:74" + }, + { + "label": "fiatFlatFee", + "offset": 0, + "slot": "421", + "type": "t_uint256", + "contract": "RedemptionVault", + "src": "contracts/RedemptionVault.sol:79" + }, + { + "label": "redeemRequests", + "offset": 0, + "slot": "422", + "type": "t_mapping(t_uint256,t_struct(Request)19714_storage)", + "contract": "RedemptionVault", + "src": "contracts/RedemptionVault.sol:84" + }, + { + "label": "requestRedeemer", + "offset": 0, + "slot": "423", + "type": "t_address", + "contract": "RedemptionVault", + "src": "contracts/RedemptionVault.sol:89" + }, + { + "label": "__gap", + "offset": 0, + "slot": "424", + "type": "t_array(t_uint256)50_storage", + "contract": "RedemptionVault", + "src": "contracts/RedemptionVault.sol:94" + }, + { + "label": "___gap", + "offset": 0, + "slot": "474", + "type": "t_array(t_uint256)50_storage", + "contract": "RedemptionVaultWithMToken", + "src": "contracts/RedemptionVaultWithMToken.sol:26" + }, + { + "label": "redemptionVault", + "offset": 0, + "slot": "524", + "type": "t_contract(IRedemptionVault)19951", + "contract": "RedemptionVaultWithMToken", + "src": "contracts/RedemptionVaultWithMToken.sol:32", + "renamedFrom": "mTbillRedemptionVault" + }, + { + "label": "liquidityProvider_deprecated", + "offset": 0, + "slot": "525", + "type": "t_address", + "contract": "RedemptionVaultWithMToken", + "src": "contracts/RedemptionVaultWithMToken.sol:39", + "renamedFrom": "liquidityProvider" + }, + { + "label": "__gap", + "offset": 0, + "slot": "526", + "type": "t_array(t_uint256)50_storage", + "contract": "RedemptionVaultWithMToken", + "src": "contracts/RedemptionVaultWithMToken.sol:44" + }, + { + "label": "__gap", + "offset": 0, + "slot": "576", + "type": "t_array(t_uint256)50_storage", + "contract": "MWinRedemptionVaultWithMToken", + "src": "contracts/products/mWIN/MWinRedemptionVaultWithMToken.sol:21" + } + ], + "types": { + "t_address": { + "label": "address", + "numberOfBytes": "20" + }, + "t_array(t_bytes32)dyn_storage": { + "label": "bytes32[]", + "numberOfBytes": "32" + }, + "t_array(t_uint256)49_storage": { + "label": "uint256[49]", + "numberOfBytes": "1568" + }, + "t_array(t_uint256)50_storage": { + "label": "uint256[50]", + "numberOfBytes": "1600" + }, + "t_bool": { + "label": "bool", + "numberOfBytes": "1" + }, + "t_bytes32": { + "label": "bytes32", + "numberOfBytes": "32" + }, + "t_bytes4": { + "label": "bytes4", + "numberOfBytes": "4" + }, + "t_contract(IDataFeed)19140": { + "label": "contract IDataFeed", + "numberOfBytes": "20" + }, + "t_contract(IMToken)19433": { + "label": "contract IMToken", + "numberOfBytes": "20" + }, + "t_contract(IRedemptionVault)19951": { + "label": "contract IRedemptionVault", + "numberOfBytes": "20" + }, + "t_contract(MidasAccessControl)16693": { + "label": "contract MidasAccessControl", + "numberOfBytes": "20" + }, + "t_enum(RequestStatus)19450": { + "label": "enum RequestStatus", + "members": ["Pending", "Processed", "Canceled"], + "numberOfBytes": "1" + }, + "t_mapping(t_address,t_bool)": { + "label": "mapping(address => bool)", + "numberOfBytes": "32" + }, + "t_mapping(t_address,t_struct(TokenConfig)19446_storage)": { + "label": "mapping(address => struct TokenConfig)", + "numberOfBytes": "32" + }, + "t_mapping(t_bytes32,t_uint256)": { + "label": "mapping(bytes32 => uint256)", + "numberOfBytes": "32" + }, + "t_mapping(t_bytes4,t_bool)": { + "label": "mapping(bytes4 => bool)", + "numberOfBytes": "32" + }, + "t_mapping(t_uint256,t_struct(Request)19714_storage)": { + "label": "mapping(uint256 => struct Request)", + "numberOfBytes": "32" + }, + "t_mapping(t_uint256,t_uint256)": { + "label": "mapping(uint256 => uint256)", + "numberOfBytes": "32" + }, + "t_struct(AddressSet)3891_storage": { + "label": "struct EnumerableSetUpgradeable.AddressSet", + "members": [ + { + "label": "_inner", + "type": "t_struct(Set)3576_storage", + "offset": 0, + "slot": "0" + } + ], + "numberOfBytes": "64" + }, + "t_struct(Counter)7492_storage": { + "label": "struct Counters.Counter", + "members": [ + { + "label": "_value", + "type": "t_uint256", + "offset": 0, + "slot": "0" + } + ], + "numberOfBytes": "32" + }, + "t_struct(Request)19714_storage": { + "label": "struct Request", + "members": [ + { + "label": "sender", + "type": "t_address", + "offset": 0, + "slot": "0" + }, + { + "label": "tokenOut", + "type": "t_address", + "offset": 0, + "slot": "1" + }, + { + "label": "status", + "type": "t_enum(RequestStatus)19450", + "offset": 20, + "slot": "1" + }, + { + "label": "amountMToken", + "type": "t_uint256", + "offset": 0, + "slot": "2" + }, + { + "label": "mTokenRate", + "type": "t_uint256", + "offset": 0, + "slot": "3" + }, + { + "label": "tokenOutRate", + "type": "t_uint256", + "offset": 0, + "slot": "4" + } + ], + "numberOfBytes": "160" + }, + "t_struct(Set)3576_storage": { + "label": "struct EnumerableSetUpgradeable.Set", + "members": [ + { + "label": "_values", + "type": "t_array(t_bytes32)dyn_storage", + "offset": 0, + "slot": "0" + }, + { + "label": "_indexes", + "type": "t_mapping(t_bytes32,t_uint256)", + "offset": 0, + "slot": "1" + } + ], + "numberOfBytes": "64" + }, + "t_struct(TokenConfig)19446_storage": { + "label": "struct TokenConfig", + "members": [ + { + "label": "dataFeed", + "type": "t_address", + "offset": 0, + "slot": "0" + }, + { + "label": "fee", + "type": "t_uint256", + "offset": 0, + "slot": "1" + }, + { + "label": "allowance", + "type": "t_uint256", + "offset": 0, + "slot": "2" + }, + { + "label": "stable", + "type": "t_bool", + "offset": 0, + "slot": "3" + } + ], + "numberOfBytes": "128" + }, + "t_uint256": { + "label": "uint256", + "numberOfBytes": "32" + }, + "t_uint8": { + "label": "uint8", + "numberOfBytes": "1" + } + } + } } } } diff --git a/scripts/upgrades/configs/upgrade-configs.ts b/scripts/upgrades/configs/upgrade-configs.ts index 1840fbfe..8682390c 100644 --- a/scripts/upgrades/configs/upgrade-configs.ts +++ b/scripts/upgrades/configs/upgrade-configs.ts @@ -45,6 +45,19 @@ export const upgradeConfigs: UpgradeConfig = { }, }, }, + 'mwin-rv-fee-transfer-order-fix': { + vaults: { + [chainIds.main]: { + overrides: { + mWIN: { + overrides: { + redemptionVaultMToken: true, + }, + }, + }, + }, + }, + }, 'batch-upgrade-scope-w-supply-cap': { initializers: { depositVault: { From 885bc08c5e634e2678a404608860e9cba2a93c11 Mon Sep 17 00:00:00 2001 From: Dmytro Horbatenko Date: Thu, 30 Jul 2026 12:00:39 +0300 Subject: [PATCH 18/19] feat: mGLOeuro on Ethereum --- .openzeppelin/mainnet.json | 1889 +++++++++++++++++ ROLES.md | 14 + config/constants/addresses.ts | 29 + config/types/tokens.ts | 2 + .../mGLOeuro/MGloEuroCustomAggregatorFeed.sol | 28 + .../products/mGLOeuro/MGloEuroDataFeed.sol | 24 + .../MGloEuroDepositVaultWithMorpho.sol | 34 + .../MGloEuroMidasAccessControlRoles.sol | 33 + .../MGloEuroRedemptionVaultWithMorpho.sol | 34 + contracts/products/mGLOeuro/mGLOeuro.sol | 80 + helpers/contracts.ts | 1 + helpers/mtokens-metadata.ts | 5 + helpers/roles.ts | 3 + scripts/deploy/common/roles.ts | 8 + scripts/deploy/configs/index.ts | 2 + scripts/deploy/configs/mGLOeuro.ts | 132 ++ scripts/deploy/configs/payment-tokens.ts | 35 + scripts/dump_roles.ts | 16 +- test/common/token.tests.ts | 8 +- 19 files changed, 2372 insertions(+), 5 deletions(-) create mode 100644 contracts/products/mGLOeuro/MGloEuroCustomAggregatorFeed.sol create mode 100644 contracts/products/mGLOeuro/MGloEuroDataFeed.sol create mode 100644 contracts/products/mGLOeuro/MGloEuroDepositVaultWithMorpho.sol create mode 100644 contracts/products/mGLOeuro/MGloEuroMidasAccessControlRoles.sol create mode 100644 contracts/products/mGLOeuro/MGloEuroRedemptionVaultWithMorpho.sol create mode 100644 contracts/products/mGLOeuro/mGLOeuro.sol create mode 100644 scripts/deploy/configs/mGLOeuro.ts diff --git a/.openzeppelin/mainnet.json b/.openzeppelin/mainnet.json index 67f9e560..72ceff1d 100644 --- a/.openzeppelin/mainnet.json +++ b/.openzeppelin/mainnet.json @@ -1649,6 +1649,61 @@ "address": "0x7F72AA0339b4944E1A77df847168AE1936c1EBBB", "txHash": "0x874c5ae49ebe8d2db1d9ba4adec60cafcdc283e2058eaf595ca295e61a8991d7", "kind": "transparent" + }, + { + "address": "0x2A2895f343CA5a5E3d81A53e4C959367EB339f11", + "txHash": "0x00b0da8f99b0815fc549893a2fdf7f7c4b0bf2782c4d3810fea84fff40882a82", + "kind": "transparent" + }, + { + "address": "0xFBD70c323238B5EDA94b6DEfFc5dB8c70F1f0e6D", + "txHash": "0x61ec0bda12baee45c917b3ebddb60391d496562f358bbd51562560fb5cf1b935", + "kind": "transparent" + }, + { + "address": "0x04DbaB674457A696cd4884c2E7c8cc6cA3118513", + "txHash": "0xb3784ec11392433943aedbf44a707e572e3b5845957b9dc729ad4e389ac2892f", + "kind": "transparent" + }, + { + "address": "0xD9795cDFb09f1c599e16f6b150f642eC529b7aBB", + "txHash": "0x19f809150ba382e7ca8c418385e6cfd05da55132c170329e7c4cd944b936ca45", + "kind": "transparent" + }, + { + "address": "0x4D88E4a32cf289ECfA6C9303B57aF0b6c132a733", + "txHash": "0xe7dc2d51d77e253662c8345cafa890abd8aad036a75f328dde70b5fc641cd2ae", + "kind": "transparent" + }, + { + "address": "0x2F8bae126b8d416C009b82Ccd60e4f9cAbbd7702", + "txHash": "0xe6c6392811a15dd038b39feeae52e6d548542ba2d1762ba97fb13a6e99a2643d", + "kind": "transparent" + }, + { + "address": "0x8B7B1689396ef4468df1c0faB181D29B47089609", + "txHash": "0x15f56462c56c6978ad1a3b3482f70c13917a28d73bb5a886becc1cee5915344c", + "kind": "transparent" + }, + { + "address": "0xc27Cd7c0203B73933dcbcF7bdB4cC5D7bDA80A64", + "txHash": "0x820baeed6ed592f6ae360378f56bd454a9c1f0092e046f0b9998ae8b70301b3d", + "kind": "transparent" + }, + { + "address": "0xF76653eBc3e47B80e89F0e5b9CF6A5CAaB848946", + "txHash": "0x3dc0765875214cceeda83a56448dbebbd4358479c3ddea0b2099b980612e1452", + "kind": "transparent" + }, + { + "address": "0xE0Dcf20b0460e1f9222528F3997F9D71Ad6375C5", + "txHash": "0x087a6f7a192d1d451567185cfa42605abcfd01c65c214467f4a4e70a2dafc169", + "kind": "transparent" + }, + { + "address": "0x1E9f04408FF38d1FB70CE939fC87D26D61F6F758", + "txHash": "0x122ef1f77608537623efd69a17a490c7dd21691e4887179a2f0111607b2cf9a2", + "kind": "transparent" } ], "impls": { @@ -120341,6 +120396,1840 @@ } } } + }, + "20ef74ef348cd04515894e02664350c2512b5a2a546ab7ba2520753130b859b0": { + "address": "0x02D7ec4A7F42bbb1e3879599a5c23c70c35748cf", + "txHash": "0xb03bc433fcbf555affb26155e25b0df28ff2a76caf447cf99d4cf59292cdcb6a", + "layout": { + "solcVersion": "0.8.9", + "storage": [ + { + "label": "_initialized", + "offset": 0, + "slot": "0", + "type": "t_uint8", + "contract": "Initializable", + "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:63", + "retypedFrom": "bool" + }, + { + "label": "_initializing", + "offset": 1, + "slot": "0", + "type": "t_bool", + "contract": "Initializable", + "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:68" + }, + { + "label": "accessControl", + "offset": 2, + "slot": "0", + "type": "t_contract(MidasAccessControl)17280", + "contract": "WithMidasAccessControl", + "src": "contracts/access/WithMidasAccessControl.sol:24" + }, + { + "label": "__gap", + "offset": 0, + "slot": "1", + "type": "t_array(t_uint256)50_storage", + "contract": "WithMidasAccessControl", + "src": "contracts/access/WithMidasAccessControl.sol:29" + }, + { + "label": "numeratorFeed", + "offset": 0, + "slot": "51", + "type": "t_contract(IDataFeed)19792", + "contract": "CompositeDataFeed", + "src": "contracts/feeds/CompositeDataFeed.sol:20" + }, + { + "label": "denominatorFeed", + "offset": 0, + "slot": "52", + "type": "t_contract(IDataFeed)19792", + "contract": "CompositeDataFeed", + "src": "contracts/feeds/CompositeDataFeed.sol:26" + }, + { + "label": "minExpectedAnswer", + "offset": 0, + "slot": "53", + "type": "t_uint256", + "contract": "CompositeDataFeed", + "src": "contracts/feeds/CompositeDataFeed.sol:31" + }, + { + "label": "maxExpectedAnswer", + "offset": 0, + "slot": "54", + "type": "t_uint256", + "contract": "CompositeDataFeed", + "src": "contracts/feeds/CompositeDataFeed.sol:36" + }, + { + "label": "__gap", + "offset": 0, + "slot": "55", + "type": "t_array(t_uint256)50_storage", + "contract": "CompositeDataFeed", + "src": "contracts/feeds/CompositeDataFeed.sol:41" + } + ], + "types": { + "t_array(t_uint256)50_storage": { + "label": "uint256[50]", + "numberOfBytes": "1600" + }, + "t_bool": { + "label": "bool", + "numberOfBytes": "1" + }, + "t_contract(IDataFeed)19792": { + "label": "contract IDataFeed", + "numberOfBytes": "20" + }, + "t_contract(MidasAccessControl)17280": { + "label": "contract MidasAccessControl", + "numberOfBytes": "20" + }, + "t_uint256": { + "label": "uint256", + "numberOfBytes": "32" + }, + "t_uint8": { + "label": "uint8", + "numberOfBytes": "1" + } + } + } + }, + "645dd6bb23526df5ae142cf011cf472c3c084791cd6c6ce321e3a04ffbff6593": { + "address": "0xCA18bBD53554b2461d0739eaF25e21c872a20E59", + "txHash": "0x9e1ad1439eef4138d39ac49a1d81d5f42ab1f99bb524c7473dd2ab772407b400", + "layout": { + "solcVersion": "0.8.9", + "storage": [ + { + "label": "_initialized", + "offset": 0, + "slot": "0", + "type": "t_uint8", + "contract": "Initializable", + "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:63", + "retypedFrom": "bool" + }, + { + "label": "_initializing", + "offset": 1, + "slot": "0", + "type": "t_bool", + "contract": "Initializable", + "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:68" + }, + { + "label": "accessControl", + "offset": 2, + "slot": "0", + "type": "t_contract(MidasAccessControl)17280", + "contract": "WithMidasAccessControl", + "src": "contracts/access/WithMidasAccessControl.sol:24" + }, + { + "label": "__gap", + "offset": 0, + "slot": "1", + "type": "t_array(t_uint256)50_storage", + "contract": "WithMidasAccessControl", + "src": "contracts/access/WithMidasAccessControl.sol:29" + }, + { + "label": "description", + "offset": 0, + "slot": "51", + "type": "t_string_storage", + "contract": "CustomAggregatorV3CompatibleFeed", + "src": "contracts/feeds/CustomAggregatorV3CompatibleFeed.sol:31" + }, + { + "label": "latestRound", + "offset": 0, + "slot": "52", + "type": "t_uint80", + "contract": "CustomAggregatorV3CompatibleFeed", + "src": "contracts/feeds/CustomAggregatorV3CompatibleFeed.sol:36" + }, + { + "label": "maxAnswerDeviation", + "offset": 0, + "slot": "53", + "type": "t_uint256", + "contract": "CustomAggregatorV3CompatibleFeed", + "src": "contracts/feeds/CustomAggregatorV3CompatibleFeed.sol:42" + }, + { + "label": "minAnswer", + "offset": 0, + "slot": "54", + "type": "t_int192", + "contract": "CustomAggregatorV3CompatibleFeed", + "src": "contracts/feeds/CustomAggregatorV3CompatibleFeed.sol:47" + }, + { + "label": "maxAnswer", + "offset": 0, + "slot": "55", + "type": "t_int192", + "contract": "CustomAggregatorV3CompatibleFeed", + "src": "contracts/feeds/CustomAggregatorV3CompatibleFeed.sol:52" + }, + { + "label": "_roundData", + "offset": 0, + "slot": "56", + "type": "t_mapping(t_uint80,t_struct(RoundData)17975_storage)", + "contract": "CustomAggregatorV3CompatibleFeed", + "src": "contracts/feeds/CustomAggregatorV3CompatibleFeed.sol:57" + } + ], + "types": { + "t_array(t_uint256)50_storage": { + "label": "uint256[50]", + "numberOfBytes": "1600" + }, + "t_bool": { + "label": "bool", + "numberOfBytes": "1" + }, + "t_contract(MidasAccessControl)17280": { + "label": "contract MidasAccessControl", + "numberOfBytes": "20" + }, + "t_int192": { + "label": "int192", + "numberOfBytes": "24" + }, + "t_int256": { + "label": "int256", + "numberOfBytes": "32" + }, + "t_mapping(t_uint80,t_struct(RoundData)17975_storage)": { + "label": "mapping(uint80 => struct CustomAggregatorV3CompatibleFeed.RoundData)", + "numberOfBytes": "32" + }, + "t_string_storage": { + "label": "string", + "numberOfBytes": "32" + }, + "t_struct(RoundData)17975_storage": { + "label": "struct CustomAggregatorV3CompatibleFeed.RoundData", + "members": [ + { + "label": "roundId", + "type": "t_uint80", + "offset": 0, + "slot": "0" + }, + { + "label": "answer", + "type": "t_int256", + "offset": 0, + "slot": "1" + }, + { + "label": "startedAt", + "type": "t_uint256", + "offset": 0, + "slot": "2" + }, + { + "label": "updatedAt", + "type": "t_uint256", + "offset": 0, + "slot": "3" + }, + { + "label": "answeredInRound", + "type": "t_uint80", + "offset": 0, + "slot": "4" + } + ], + "numberOfBytes": "160" + }, + "t_uint256": { + "label": "uint256", + "numberOfBytes": "32" + }, + "t_uint8": { + "label": "uint8", + "numberOfBytes": "1" + }, + "t_uint80": { + "label": "uint80", + "numberOfBytes": "10" + } + } + } + }, + "ac38a1923bfd1a4465db3352916ecea0a4ae507dac0883d1a3903512f5b2ba34": { + "address": "0x578ba8bf689d167Ac30DFe2a8285A3A16519B18d", + "txHash": "0x762b73b5e701d7a5d2d5a3262a9f8dd378b6c6b5b4b90bd391af3682dacfe85b", + "layout": { + "solcVersion": "0.8.9", + "storage": [ + { + "label": "_initialized", + "offset": 0, + "slot": "0", + "type": "t_uint8", + "contract": "Initializable", + "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:63", + "retypedFrom": "bool" + }, + { + "label": "_initializing", + "offset": 1, + "slot": "0", + "type": "t_bool", + "contract": "Initializable", + "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:68" + }, + { + "label": "__gap", + "offset": 0, + "slot": "1", + "type": "t_array(t_uint256)50_storage", + "contract": "ContextUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/utils/ContextUpgradeable.sol:36" + }, + { + "label": "_balances", + "offset": 0, + "slot": "51", + "type": "t_mapping(t_address,t_uint256)", + "contract": "ERC20Upgradeable", + "src": "@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol:40" + }, + { + "label": "_allowances", + "offset": 0, + "slot": "52", + "type": "t_mapping(t_address,t_mapping(t_address,t_uint256))", + "contract": "ERC20Upgradeable", + "src": "@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol:42" + }, + { + "label": "_totalSupply", + "offset": 0, + "slot": "53", + "type": "t_uint256", + "contract": "ERC20Upgradeable", + "src": "@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol:44" + }, + { + "label": "_name", + "offset": 0, + "slot": "54", + "type": "t_string_storage", + "contract": "ERC20Upgradeable", + "src": "@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol:46" + }, + { + "label": "_symbol", + "offset": 0, + "slot": "55", + "type": "t_string_storage", + "contract": "ERC20Upgradeable", + "src": "@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol:47" + }, + { + "label": "__gap", + "offset": 0, + "slot": "56", + "type": "t_array(t_uint256)45_storage", + "contract": "ERC20Upgradeable", + "src": "@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol:376" + }, + { + "label": "_paused", + "offset": 0, + "slot": "101", + "type": "t_bool", + "contract": "PausableUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/security/PausableUpgradeable.sol:29" + }, + { + "label": "__gap", + "offset": 0, + "slot": "102", + "type": "t_array(t_uint256)49_storage", + "contract": "PausableUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/security/PausableUpgradeable.sol:116" + }, + { + "label": "__gap", + "offset": 0, + "slot": "151", + "type": "t_array(t_uint256)50_storage", + "contract": "ERC20PausableUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/token/ERC20/extensions/ERC20PausableUpgradeable.sol:48" + }, + { + "label": "accessControl", + "offset": 0, + "slot": "201", + "type": "t_contract(MidasAccessControl)17280", + "contract": "WithMidasAccessControl", + "src": "contracts/access/WithMidasAccessControl.sol:24" + }, + { + "label": "__gap", + "offset": 0, + "slot": "202", + "type": "t_array(t_uint256)50_storage", + "contract": "WithMidasAccessControl", + "src": "contracts/access/WithMidasAccessControl.sol:29" + }, + { + "label": "__gap", + "offset": 0, + "slot": "252", + "type": "t_array(t_uint256)50_storage", + "contract": "Blacklistable", + "src": "contracts/access/Blacklistable.sol:16" + }, + { + "label": "metadata", + "offset": 0, + "slot": "302", + "type": "t_mapping(t_bytes32,t_bytes_storage)", + "contract": "mToken", + "src": "contracts/mToken.sol:18" + }, + { + "label": "__gap", + "offset": 0, + "slot": "303", + "type": "t_array(t_uint256)50_storage", + "contract": "mToken", + "src": "contracts/mToken.sol:23" + }, + { + "label": "__gap", + "offset": 0, + "slot": "353", + "type": "t_array(t_uint256)50_storage", + "contract": "mTokenMinBalance", + "src": "contracts/mTokenMinBalance.sol:16" + }, + { + "label": "__gap", + "offset": 0, + "slot": "403", + "type": "t_array(t_uint256)50_storage", + "contract": "mGLOeuro", + "src": "contracts/products/mGLOeuro/mGLOeuro.sol:39" + } + ], + "types": { + "t_address": { + "label": "address", + "numberOfBytes": "20" + }, + "t_array(t_uint256)45_storage": { + "label": "uint256[45]", + "numberOfBytes": "1440" + }, + "t_array(t_uint256)49_storage": { + "label": "uint256[49]", + "numberOfBytes": "1568" + }, + "t_array(t_uint256)50_storage": { + "label": "uint256[50]", + "numberOfBytes": "1600" + }, + "t_bool": { + "label": "bool", + "numberOfBytes": "1" + }, + "t_bytes32": { + "label": "bytes32", + "numberOfBytes": "32" + }, + "t_bytes_storage": { + "label": "bytes", + "numberOfBytes": "32" + }, + "t_contract(MidasAccessControl)17280": { + "label": "contract MidasAccessControl", + "numberOfBytes": "20" + }, + "t_mapping(t_address,t_mapping(t_address,t_uint256))": { + "label": "mapping(address => mapping(address => uint256))", + "numberOfBytes": "32" + }, + "t_mapping(t_address,t_uint256)": { + "label": "mapping(address => uint256)", + "numberOfBytes": "32" + }, + "t_mapping(t_bytes32,t_bytes_storage)": { + "label": "mapping(bytes32 => bytes)", + "numberOfBytes": "32" + }, + "t_string_storage": { + "label": "string", + "numberOfBytes": "32" + }, + "t_uint256": { + "label": "uint256", + "numberOfBytes": "32" + }, + "t_uint8": { + "label": "uint8", + "numberOfBytes": "1" + } + } + } + }, + "a95446ac61efa7df55ae397582ff4792bafae41969ba247aca00166681446056": { + "address": "0xbFd25A5d18B2E49eb4d486A99d9373440909795a", + "txHash": "0xb0dbfd155d7958e5a9111234ebea41aae14a2d69c8e99f098e6de961c3258b42", + "layout": { + "solcVersion": "0.8.9", + "storage": [ + { + "label": "_initialized", + "offset": 0, + "slot": "0", + "type": "t_uint8", + "contract": "Initializable", + "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:63", + "retypedFrom": "bool" + }, + { + "label": "_initializing", + "offset": 1, + "slot": "0", + "type": "t_bool", + "contract": "Initializable", + "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:68" + }, + { + "label": "accessControl", + "offset": 2, + "slot": "0", + "type": "t_contract(MidasAccessControl)17280", + "contract": "WithMidasAccessControl", + "src": "contracts/access/WithMidasAccessControl.sol:24" + }, + { + "label": "__gap", + "offset": 0, + "slot": "1", + "type": "t_array(t_uint256)50_storage", + "contract": "WithMidasAccessControl", + "src": "contracts/access/WithMidasAccessControl.sol:29" + }, + { + "label": "description", + "offset": 0, + "slot": "51", + "type": "t_string_storage", + "contract": "CustomAggregatorV3CompatibleFeed", + "src": "contracts/feeds/CustomAggregatorV3CompatibleFeed.sol:31" + }, + { + "label": "latestRound", + "offset": 0, + "slot": "52", + "type": "t_uint80", + "contract": "CustomAggregatorV3CompatibleFeed", + "src": "contracts/feeds/CustomAggregatorV3CompatibleFeed.sol:36" + }, + { + "label": "maxAnswerDeviation", + "offset": 0, + "slot": "53", + "type": "t_uint256", + "contract": "CustomAggregatorV3CompatibleFeed", + "src": "contracts/feeds/CustomAggregatorV3CompatibleFeed.sol:42" + }, + { + "label": "minAnswer", + "offset": 0, + "slot": "54", + "type": "t_int192", + "contract": "CustomAggregatorV3CompatibleFeed", + "src": "contracts/feeds/CustomAggregatorV3CompatibleFeed.sol:47" + }, + { + "label": "maxAnswer", + "offset": 0, + "slot": "55", + "type": "t_int192", + "contract": "CustomAggregatorV3CompatibleFeed", + "src": "contracts/feeds/CustomAggregatorV3CompatibleFeed.sol:52" + }, + { + "label": "_roundData", + "offset": 0, + "slot": "56", + "type": "t_mapping(t_uint80,t_struct(RoundData)17975_storage)", + "contract": "CustomAggregatorV3CompatibleFeed", + "src": "contracts/feeds/CustomAggregatorV3CompatibleFeed.sol:57" + }, + { + "label": "__gap", + "offset": 0, + "slot": "57", + "type": "t_array(t_uint256)50_storage", + "contract": "MGloEuroCustomAggregatorFeed", + "src": "contracts/products/mGLOeuro/MGloEuroCustomAggregatorFeed.sol:20" + } + ], + "types": { + "t_array(t_uint256)50_storage": { + "label": "uint256[50]", + "numberOfBytes": "1600" + }, + "t_bool": { + "label": "bool", + "numberOfBytes": "1" + }, + "t_contract(MidasAccessControl)17280": { + "label": "contract MidasAccessControl", + "numberOfBytes": "20" + }, + "t_int192": { + "label": "int192", + "numberOfBytes": "24" + }, + "t_int256": { + "label": "int256", + "numberOfBytes": "32" + }, + "t_mapping(t_uint80,t_struct(RoundData)17975_storage)": { + "label": "mapping(uint80 => struct CustomAggregatorV3CompatibleFeed.RoundData)", + "numberOfBytes": "32" + }, + "t_string_storage": { + "label": "string", + "numberOfBytes": "32" + }, + "t_struct(RoundData)17975_storage": { + "label": "struct CustomAggregatorV3CompatibleFeed.RoundData", + "members": [ + { + "label": "roundId", + "type": "t_uint80", + "offset": 0, + "slot": "0" + }, + { + "label": "answer", + "type": "t_int256", + "offset": 0, + "slot": "1" + }, + { + "label": "startedAt", + "type": "t_uint256", + "offset": 0, + "slot": "2" + }, + { + "label": "updatedAt", + "type": "t_uint256", + "offset": 0, + "slot": "3" + }, + { + "label": "answeredInRound", + "type": "t_uint80", + "offset": 0, + "slot": "4" + } + ], + "numberOfBytes": "160" + }, + "t_uint256": { + "label": "uint256", + "numberOfBytes": "32" + }, + "t_uint8": { + "label": "uint8", + "numberOfBytes": "1" + }, + "t_uint80": { + "label": "uint80", + "numberOfBytes": "10" + } + } + } + }, + "3fbddeea5f4ee5684e4ba90a02d73e84eecbed8cd72b3cec1eb41028f325474f": { + "address": "0x69ca8c969884A755dDA6B8DC2F91A10e63148675", + "txHash": "0x1d19c14329994681654de49c1065455296214e6254f96d7bdb8d19f7d5478fee", + "layout": { + "solcVersion": "0.8.9", + "storage": [ + { + "label": "_initialized", + "offset": 0, + "slot": "0", + "type": "t_uint8", + "contract": "Initializable", + "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:63", + "retypedFrom": "bool" + }, + { + "label": "_initializing", + "offset": 1, + "slot": "0", + "type": "t_bool", + "contract": "Initializable", + "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:68" + }, + { + "label": "accessControl", + "offset": 2, + "slot": "0", + "type": "t_contract(MidasAccessControl)17280", + "contract": "WithMidasAccessControl", + "src": "contracts/access/WithMidasAccessControl.sol:24" + }, + { + "label": "__gap", + "offset": 0, + "slot": "1", + "type": "t_array(t_uint256)50_storage", + "contract": "WithMidasAccessControl", + "src": "contracts/access/WithMidasAccessControl.sol:29" + }, + { + "label": "aggregator", + "offset": 0, + "slot": "51", + "type": "t_contract(AggregatorV3Interface)45", + "contract": "DataFeed", + "src": "contracts/feeds/DataFeed.sol:22" + }, + { + "label": "healthyDiff", + "offset": 0, + "slot": "52", + "type": "t_uint256", + "contract": "DataFeed", + "src": "contracts/feeds/DataFeed.sol:27" + }, + { + "label": "minExpectedAnswer", + "offset": 0, + "slot": "53", + "type": "t_int256", + "contract": "DataFeed", + "src": "contracts/feeds/DataFeed.sol:32" + }, + { + "label": "maxExpectedAnswer", + "offset": 0, + "slot": "54", + "type": "t_int256", + "contract": "DataFeed", + "src": "contracts/feeds/DataFeed.sol:37" + }, + { + "label": "__gap", + "offset": 0, + "slot": "55", + "type": "t_array(t_uint256)50_storage", + "contract": "DataFeed", + "src": "contracts/feeds/DataFeed.sol:42" + }, + { + "label": "__gap", + "offset": 0, + "slot": "105", + "type": "t_array(t_uint256)50_storage", + "contract": "MGloEuroDataFeed", + "src": "contracts/products/mGLOeuro/MGloEuroDataFeed.sol:16" + } + ], + "types": { + "t_array(t_uint256)50_storage": { + "label": "uint256[50]", + "numberOfBytes": "1600" + }, + "t_bool": { + "label": "bool", + "numberOfBytes": "1" + }, + "t_contract(AggregatorV3Interface)45": { + "label": "contract AggregatorV3Interface", + "numberOfBytes": "20" + }, + "t_contract(MidasAccessControl)17280": { + "label": "contract MidasAccessControl", + "numberOfBytes": "20" + }, + "t_int256": { + "label": "int256", + "numberOfBytes": "32" + }, + "t_uint256": { + "label": "uint256", + "numberOfBytes": "32" + }, + "t_uint8": { + "label": "uint8", + "numberOfBytes": "1" + } + } + } + }, + "a77e77c5b9296e786ff1e44e54b0e72f58d640c67d409fc104690c9fdeab9e6f": { + "address": "0x370e3933Ce6a8C710455AFFBA984c1104e42Fb59", + "txHash": "0x20344318993757b11ad88337767c1345d864a4fca1a6a7d6cc7d68a55cb36024", + "layout": { + "solcVersion": "0.8.9", + "storage": [ + { + "label": "_initialized", + "offset": 0, + "slot": "0", + "type": "t_uint8", + "contract": "Initializable", + "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:63", + "retypedFrom": "bool" + }, + { + "label": "_initializing", + "offset": 1, + "slot": "0", + "type": "t_bool", + "contract": "Initializable", + "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:68" + }, + { + "label": "accessControl", + "offset": 2, + "slot": "0", + "type": "t_contract(MidasAccessControl)17280", + "contract": "WithMidasAccessControl", + "src": "contracts/access/WithMidasAccessControl.sol:24" + }, + { + "label": "__gap", + "offset": 0, + "slot": "1", + "type": "t_array(t_uint256)50_storage", + "contract": "WithMidasAccessControl", + "src": "contracts/access/WithMidasAccessControl.sol:29" + }, + { + "label": "__gap", + "offset": 0, + "slot": "51", + "type": "t_array(t_uint256)50_storage", + "contract": "ContextUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/utils/ContextUpgradeable.sol:36" + }, + { + "label": "_paused", + "offset": 0, + "slot": "101", + "type": "t_bool", + "contract": "PausableUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/security/PausableUpgradeable.sol:29" + }, + { + "label": "__gap", + "offset": 0, + "slot": "102", + "type": "t_array(t_uint256)49_storage", + "contract": "PausableUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/security/PausableUpgradeable.sol:116" + }, + { + "label": "fnPaused", + "offset": 0, + "slot": "151", + "type": "t_mapping(t_bytes4,t_bool)", + "contract": "Pausable", + "src": "contracts/access/Pausable.sol:14" + }, + { + "label": "__gap", + "offset": 0, + "slot": "152", + "type": "t_array(t_uint256)50_storage", + "contract": "Pausable", + "src": "contracts/access/Pausable.sol:19" + }, + { + "label": "__gap", + "offset": 0, + "slot": "202", + "type": "t_array(t_uint256)50_storage", + "contract": "Blacklistable", + "src": "contracts/access/Blacklistable.sol:16" + }, + { + "label": "greenlistEnabled", + "offset": 0, + "slot": "252", + "type": "t_bool", + "contract": "Greenlistable", + "src": "contracts/access/Greenlistable.sol:16" + }, + { + "label": "__gap", + "offset": 0, + "slot": "253", + "type": "t_array(t_uint256)50_storage", + "contract": "Greenlistable", + "src": "contracts/access/Greenlistable.sol:21" + }, + { + "label": "sanctionsList", + "offset": 0, + "slot": "303", + "type": "t_address", + "contract": "WithSanctionsList", + "src": "contracts/abstract/WithSanctionsList.sol:18" + }, + { + "label": "__gap", + "offset": 0, + "slot": "304", + "type": "t_array(t_uint256)50_storage", + "contract": "WithSanctionsList", + "src": "contracts/abstract/WithSanctionsList.sol:23" + }, + { + "label": "currentRequestId", + "offset": 0, + "slot": "354", + "type": "t_struct(Counter)8079_storage", + "contract": "ManageableVault", + "src": "contracts/abstract/ManageableVault.sol:53" + }, + { + "label": "mToken", + "offset": 0, + "slot": "355", + "type": "t_contract(IMToken)20085", + "contract": "ManageableVault", + "src": "contracts/abstract/ManageableVault.sol:66" + }, + { + "label": "mTokenDataFeed", + "offset": 0, + "slot": "356", + "type": "t_contract(IDataFeed)19792", + "contract": "ManageableVault", + "src": "contracts/abstract/ManageableVault.sol:71" + }, + { + "label": "tokensReceiver", + "offset": 0, + "slot": "357", + "type": "t_address", + "contract": "ManageableVault", + "src": "contracts/abstract/ManageableVault.sol:76" + }, + { + "label": "instantFee", + "offset": 0, + "slot": "358", + "type": "t_uint256", + "contract": "ManageableVault", + "src": "contracts/abstract/ManageableVault.sol:81" + }, + { + "label": "instantDailyLimit", + "offset": 0, + "slot": "359", + "type": "t_uint256", + "contract": "ManageableVault", + "src": "contracts/abstract/ManageableVault.sol:88" + }, + { + "label": "dailyLimits", + "offset": 0, + "slot": "360", + "type": "t_mapping(t_uint256,t_uint256)", + "contract": "ManageableVault", + "src": "contracts/abstract/ManageableVault.sol:93" + }, + { + "label": "feeReceiver", + "offset": 0, + "slot": "361", + "type": "t_address", + "contract": "ManageableVault", + "src": "contracts/abstract/ManageableVault.sol:98" + }, + { + "label": "variationTolerance", + "offset": 0, + "slot": "362", + "type": "t_uint256", + "contract": "ManageableVault", + "src": "contracts/abstract/ManageableVault.sol:103" + }, + { + "label": "waivedFeeRestriction", + "offset": 0, + "slot": "363", + "type": "t_mapping(t_address,t_bool)", + "contract": "ManageableVault", + "src": "contracts/abstract/ManageableVault.sol:108" + }, + { + "label": "_paymentTokens", + "offset": 0, + "slot": "364", + "type": "t_struct(AddressSet)3891_storage", + "contract": "ManageableVault", + "src": "contracts/abstract/ManageableVault.sol:113" + }, + { + "label": "tokensConfig", + "offset": 0, + "slot": "366", + "type": "t_mapping(t_address,t_struct(TokenConfig)20098_storage)", + "contract": "ManageableVault", + "src": "contracts/abstract/ManageableVault.sol:118" + }, + { + "label": "minAmount", + "offset": 0, + "slot": "367", + "type": "t_uint256", + "contract": "ManageableVault", + "src": "contracts/abstract/ManageableVault.sol:123" + }, + { + "label": "isFreeFromMinAmount", + "offset": 0, + "slot": "368", + "type": "t_mapping(t_address,t_bool)", + "contract": "ManageableVault", + "src": "contracts/abstract/ManageableVault.sol:128" + }, + { + "label": "__gap", + "offset": 0, + "slot": "369", + "type": "t_array(t_uint256)50_storage", + "contract": "ManageableVault", + "src": "contracts/abstract/ManageableVault.sol:133" + }, + { + "label": "minMTokenAmountForFirstDeposit", + "offset": 0, + "slot": "419", + "type": "t_uint256", + "contract": "DepositVault", + "src": "contracts/DepositVault.sol:78" + }, + { + "label": "mintRequests", + "offset": 0, + "slot": "420", + "type": "t_mapping(t_uint256,t_struct(Request)19809_storage)", + "contract": "DepositVault", + "src": "contracts/DepositVault.sol:83" + }, + { + "label": "totalMinted", + "offset": 0, + "slot": "421", + "type": "t_mapping(t_address,t_uint256)", + "contract": "DepositVault", + "src": "contracts/DepositVault.sol:88" + }, + { + "label": "maxSupplyCap", + "offset": 0, + "slot": "422", + "type": "t_uint256", + "contract": "DepositVault", + "src": "contracts/DepositVault.sol:95" + }, + { + "label": "__gap", + "offset": 0, + "slot": "423", + "type": "t_array(t_uint256)49_storage", + "contract": "DepositVault", + "src": "contracts/DepositVault.sol:103" + }, + { + "label": "morphoVaults", + "offset": 0, + "slot": "472", + "type": "t_mapping(t_address,t_contract(IMorphoVault)20791)", + "contract": "DepositVaultWithMorpho", + "src": "contracts/DepositVaultWithMorpho.sol:24" + }, + { + "label": "morphoDepositsEnabled", + "offset": 0, + "slot": "473", + "type": "t_bool", + "contract": "DepositVaultWithMorpho", + "src": "contracts/DepositVaultWithMorpho.sol:30" + }, + { + "label": "autoInvestFallbackEnabled", + "offset": 1, + "slot": "473", + "type": "t_bool", + "contract": "DepositVaultWithMorpho", + "src": "contracts/DepositVaultWithMorpho.sol:36" + }, + { + "label": "__gap", + "offset": 0, + "slot": "474", + "type": "t_array(t_uint256)50_storage", + "contract": "DepositVaultWithMorpho", + "src": "contracts/DepositVaultWithMorpho.sol:41" + }, + { + "label": "__gap", + "offset": 0, + "slot": "524", + "type": "t_array(t_uint256)50_storage", + "contract": "MGloEuroDepositVaultWithMorpho", + "src": "contracts/products/mGLOeuro/MGloEuroDepositVaultWithMorpho.sol:19" + } + ], + "types": { + "t_address": { + "label": "address", + "numberOfBytes": "20" + }, + "t_array(t_bytes32)dyn_storage": { + "label": "bytes32[]", + "numberOfBytes": "32" + }, + "t_array(t_uint256)49_storage": { + "label": "uint256[49]", + "numberOfBytes": "1568" + }, + "t_array(t_uint256)50_storage": { + "label": "uint256[50]", + "numberOfBytes": "1600" + }, + "t_bool": { + "label": "bool", + "numberOfBytes": "1" + }, + "t_bytes32": { + "label": "bytes32", + "numberOfBytes": "32" + }, + "t_bytes4": { + "label": "bytes4", + "numberOfBytes": "4" + }, + "t_contract(IDataFeed)19792": { + "label": "contract IDataFeed", + "numberOfBytes": "20" + }, + "t_contract(IMToken)20085": { + "label": "contract IMToken", + "numberOfBytes": "20" + }, + "t_contract(IMorphoVault)20791": { + "label": "contract IMorphoVault", + "numberOfBytes": "20" + }, + "t_contract(MidasAccessControl)17280": { + "label": "contract MidasAccessControl", + "numberOfBytes": "20" + }, + "t_enum(RequestStatus)20102": { + "label": "enum RequestStatus", + "members": ["Pending", "Processed", "Canceled"], + "numberOfBytes": "1" + }, + "t_mapping(t_address,t_bool)": { + "label": "mapping(address => bool)", + "numberOfBytes": "32" + }, + "t_mapping(t_address,t_contract(IMorphoVault)20791)": { + "label": "mapping(address => contract IMorphoVault)", + "numberOfBytes": "32" + }, + "t_mapping(t_address,t_struct(TokenConfig)20098_storage)": { + "label": "mapping(address => struct TokenConfig)", + "numberOfBytes": "32" + }, + "t_mapping(t_address,t_uint256)": { + "label": "mapping(address => uint256)", + "numberOfBytes": "32" + }, + "t_mapping(t_bytes32,t_uint256)": { + "label": "mapping(bytes32 => uint256)", + "numberOfBytes": "32" + }, + "t_mapping(t_bytes4,t_bool)": { + "label": "mapping(bytes4 => bool)", + "numberOfBytes": "32" + }, + "t_mapping(t_uint256,t_struct(Request)19809_storage)": { + "label": "mapping(uint256 => struct Request)", + "numberOfBytes": "32" + }, + "t_mapping(t_uint256,t_uint256)": { + "label": "mapping(uint256 => uint256)", + "numberOfBytes": "32" + }, + "t_struct(AddressSet)3891_storage": { + "label": "struct EnumerableSetUpgradeable.AddressSet", + "members": [ + { + "label": "_inner", + "type": "t_struct(Set)3576_storage", + "offset": 0, + "slot": "0" + } + ], + "numberOfBytes": "64" + }, + "t_struct(Counter)8079_storage": { + "label": "struct Counters.Counter", + "members": [ + { + "label": "_value", + "type": "t_uint256", + "offset": 0, + "slot": "0" + } + ], + "numberOfBytes": "32" + }, + "t_struct(Request)19809_storage": { + "label": "struct Request", + "members": [ + { + "label": "sender", + "type": "t_address", + "offset": 0, + "slot": "0" + }, + { + "label": "tokenIn", + "type": "t_address", + "offset": 0, + "slot": "1" + }, + { + "label": "status", + "type": "t_enum(RequestStatus)20102", + "offset": 20, + "slot": "1" + }, + { + "label": "depositedUsdAmount", + "type": "t_uint256", + "offset": 0, + "slot": "2" + }, + { + "label": "usdAmountWithoutFees", + "type": "t_uint256", + "offset": 0, + "slot": "3" + }, + { + "label": "tokenOutRate", + "type": "t_uint256", + "offset": 0, + "slot": "4" + } + ], + "numberOfBytes": "160" + }, + "t_struct(Set)3576_storage": { + "label": "struct EnumerableSetUpgradeable.Set", + "members": [ + { + "label": "_values", + "type": "t_array(t_bytes32)dyn_storage", + "offset": 0, + "slot": "0" + }, + { + "label": "_indexes", + "type": "t_mapping(t_bytes32,t_uint256)", + "offset": 0, + "slot": "1" + } + ], + "numberOfBytes": "64" + }, + "t_struct(TokenConfig)20098_storage": { + "label": "struct TokenConfig", + "members": [ + { + "label": "dataFeed", + "type": "t_address", + "offset": 0, + "slot": "0" + }, + { + "label": "fee", + "type": "t_uint256", + "offset": 0, + "slot": "1" + }, + { + "label": "allowance", + "type": "t_uint256", + "offset": 0, + "slot": "2" + }, + { + "label": "stable", + "type": "t_bool", + "offset": 0, + "slot": "3" + } + ], + "numberOfBytes": "128" + }, + "t_uint256": { + "label": "uint256", + "numberOfBytes": "32" + }, + "t_uint8": { + "label": "uint8", + "numberOfBytes": "1" + } + } + } + }, + "c48e51c6e9141222340dd06506758d1bac849758940302d57a6a93d125c1c317": { + "address": "0x71f0b214d6F552795737d37CBCCb148C2dE0bCeF", + "txHash": "0x93513d7142203e02d4ed69134cefcde02190320eab3200e2731ecc5a87faffba", + "layout": { + "solcVersion": "0.8.9", + "storage": [ + { + "label": "_initialized", + "offset": 0, + "slot": "0", + "type": "t_uint8", + "contract": "Initializable", + "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:63", + "retypedFrom": "bool" + }, + { + "label": "_initializing", + "offset": 1, + "slot": "0", + "type": "t_bool", + "contract": "Initializable", + "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:68" + }, + { + "label": "accessControl", + "offset": 2, + "slot": "0", + "type": "t_contract(MidasAccessControl)17280", + "contract": "WithMidasAccessControl", + "src": "contracts/access/WithMidasAccessControl.sol:24" + }, + { + "label": "__gap", + "offset": 0, + "slot": "1", + "type": "t_array(t_uint256)50_storage", + "contract": "WithMidasAccessControl", + "src": "contracts/access/WithMidasAccessControl.sol:29" + }, + { + "label": "__gap", + "offset": 0, + "slot": "51", + "type": "t_array(t_uint256)50_storage", + "contract": "ContextUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/utils/ContextUpgradeable.sol:36" + }, + { + "label": "_paused", + "offset": 0, + "slot": "101", + "type": "t_bool", + "contract": "PausableUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/security/PausableUpgradeable.sol:29" + }, + { + "label": "__gap", + "offset": 0, + "slot": "102", + "type": "t_array(t_uint256)49_storage", + "contract": "PausableUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/security/PausableUpgradeable.sol:116" + }, + { + "label": "fnPaused", + "offset": 0, + "slot": "151", + "type": "t_mapping(t_bytes4,t_bool)", + "contract": "Pausable", + "src": "contracts/access/Pausable.sol:14" + }, + { + "label": "__gap", + "offset": 0, + "slot": "152", + "type": "t_array(t_uint256)50_storage", + "contract": "Pausable", + "src": "contracts/access/Pausable.sol:19" + }, + { + "label": "__gap", + "offset": 0, + "slot": "202", + "type": "t_array(t_uint256)50_storage", + "contract": "Blacklistable", + "src": "contracts/access/Blacklistable.sol:16" + }, + { + "label": "greenlistEnabled", + "offset": 0, + "slot": "252", + "type": "t_bool", + "contract": "Greenlistable", + "src": "contracts/access/Greenlistable.sol:16" + }, + { + "label": "__gap", + "offset": 0, + "slot": "253", + "type": "t_array(t_uint256)50_storage", + "contract": "Greenlistable", + "src": "contracts/access/Greenlistable.sol:21" + }, + { + "label": "sanctionsList", + "offset": 0, + "slot": "303", + "type": "t_address", + "contract": "WithSanctionsList", + "src": "contracts/abstract/WithSanctionsList.sol:18" + }, + { + "label": "__gap", + "offset": 0, + "slot": "304", + "type": "t_array(t_uint256)50_storage", + "contract": "WithSanctionsList", + "src": "contracts/abstract/WithSanctionsList.sol:23" + }, + { + "label": "currentRequestId", + "offset": 0, + "slot": "354", + "type": "t_struct(Counter)8079_storage", + "contract": "ManageableVault", + "src": "contracts/abstract/ManageableVault.sol:53" + }, + { + "label": "mToken", + "offset": 0, + "slot": "355", + "type": "t_contract(IMToken)20085", + "contract": "ManageableVault", + "src": "contracts/abstract/ManageableVault.sol:66" + }, + { + "label": "mTokenDataFeed", + "offset": 0, + "slot": "356", + "type": "t_contract(IDataFeed)19792", + "contract": "ManageableVault", + "src": "contracts/abstract/ManageableVault.sol:71" + }, + { + "label": "tokensReceiver", + "offset": 0, + "slot": "357", + "type": "t_address", + "contract": "ManageableVault", + "src": "contracts/abstract/ManageableVault.sol:76" + }, + { + "label": "instantFee", + "offset": 0, + "slot": "358", + "type": "t_uint256", + "contract": "ManageableVault", + "src": "contracts/abstract/ManageableVault.sol:81" + }, + { + "label": "instantDailyLimit", + "offset": 0, + "slot": "359", + "type": "t_uint256", + "contract": "ManageableVault", + "src": "contracts/abstract/ManageableVault.sol:88" + }, + { + "label": "dailyLimits", + "offset": 0, + "slot": "360", + "type": "t_mapping(t_uint256,t_uint256)", + "contract": "ManageableVault", + "src": "contracts/abstract/ManageableVault.sol:93" + }, + { + "label": "feeReceiver", + "offset": 0, + "slot": "361", + "type": "t_address", + "contract": "ManageableVault", + "src": "contracts/abstract/ManageableVault.sol:98" + }, + { + "label": "variationTolerance", + "offset": 0, + "slot": "362", + "type": "t_uint256", + "contract": "ManageableVault", + "src": "contracts/abstract/ManageableVault.sol:103" + }, + { + "label": "waivedFeeRestriction", + "offset": 0, + "slot": "363", + "type": "t_mapping(t_address,t_bool)", + "contract": "ManageableVault", + "src": "contracts/abstract/ManageableVault.sol:108" + }, + { + "label": "_paymentTokens", + "offset": 0, + "slot": "364", + "type": "t_struct(AddressSet)3891_storage", + "contract": "ManageableVault", + "src": "contracts/abstract/ManageableVault.sol:113" + }, + { + "label": "tokensConfig", + "offset": 0, + "slot": "366", + "type": "t_mapping(t_address,t_struct(TokenConfig)20098_storage)", + "contract": "ManageableVault", + "src": "contracts/abstract/ManageableVault.sol:118" + }, + { + "label": "minAmount", + "offset": 0, + "slot": "367", + "type": "t_uint256", + "contract": "ManageableVault", + "src": "contracts/abstract/ManageableVault.sol:123" + }, + { + "label": "isFreeFromMinAmount", + "offset": 0, + "slot": "368", + "type": "t_mapping(t_address,t_bool)", + "contract": "ManageableVault", + "src": "contracts/abstract/ManageableVault.sol:128" + }, + { + "label": "__gap", + "offset": 0, + "slot": "369", + "type": "t_array(t_uint256)50_storage", + "contract": "ManageableVault", + "src": "contracts/abstract/ManageableVault.sol:133" + }, + { + "label": "minFiatRedeemAmount", + "offset": 0, + "slot": "419", + "type": "t_uint256", + "contract": "RedemptionVault", + "src": "contracts/RedemptionVault.sol:69" + }, + { + "label": "fiatAdditionalFee", + "offset": 0, + "slot": "420", + "type": "t_uint256", + "contract": "RedemptionVault", + "src": "contracts/RedemptionVault.sol:74" + }, + { + "label": "fiatFlatFee", + "offset": 0, + "slot": "421", + "type": "t_uint256", + "contract": "RedemptionVault", + "src": "contracts/RedemptionVault.sol:79" + }, + { + "label": "redeemRequests", + "offset": 0, + "slot": "422", + "type": "t_mapping(t_uint256,t_struct(Request)20366_storage)", + "contract": "RedemptionVault", + "src": "contracts/RedemptionVault.sol:84" + }, + { + "label": "requestRedeemer", + "offset": 0, + "slot": "423", + "type": "t_address", + "contract": "RedemptionVault", + "src": "contracts/RedemptionVault.sol:89" + }, + { + "label": "__gap", + "offset": 0, + "slot": "424", + "type": "t_array(t_uint256)50_storage", + "contract": "RedemptionVault", + "src": "contracts/RedemptionVault.sol:94" + }, + { + "label": "morphoVaults", + "offset": 0, + "slot": "474", + "type": "t_mapping(t_address,t_contract(IMorphoVault)20791)", + "contract": "RedemptionVaultWithMorpho", + "src": "contracts/RedemptionVaultWithMorpho.sol:25" + }, + { + "label": "__gap", + "offset": 0, + "slot": "475", + "type": "t_array(t_uint256)50_storage", + "contract": "RedemptionVaultWithMorpho", + "src": "contracts/RedemptionVaultWithMorpho.sol:30" + }, + { + "label": "__gap", + "offset": 0, + "slot": "525", + "type": "t_array(t_uint256)50_storage", + "contract": "MGloEuroRedemptionVaultWithMorpho", + "src": "contracts/products/mGLOeuro/MGloEuroRedemptionVaultWithMorpho.sol:19" + } + ], + "types": { + "t_address": { + "label": "address", + "numberOfBytes": "20" + }, + "t_array(t_bytes32)dyn_storage": { + "label": "bytes32[]", + "numberOfBytes": "32" + }, + "t_array(t_uint256)49_storage": { + "label": "uint256[49]", + "numberOfBytes": "1568" + }, + "t_array(t_uint256)50_storage": { + "label": "uint256[50]", + "numberOfBytes": "1600" + }, + "t_bool": { + "label": "bool", + "numberOfBytes": "1" + }, + "t_bytes32": { + "label": "bytes32", + "numberOfBytes": "32" + }, + "t_bytes4": { + "label": "bytes4", + "numberOfBytes": "4" + }, + "t_contract(IDataFeed)19792": { + "label": "contract IDataFeed", + "numberOfBytes": "20" + }, + "t_contract(IMToken)20085": { + "label": "contract IMToken", + "numberOfBytes": "20" + }, + "t_contract(IMorphoVault)20791": { + "label": "contract IMorphoVault", + "numberOfBytes": "20" + }, + "t_contract(MidasAccessControl)17280": { + "label": "contract MidasAccessControl", + "numberOfBytes": "20" + }, + "t_enum(RequestStatus)20102": { + "label": "enum RequestStatus", + "members": ["Pending", "Processed", "Canceled"], + "numberOfBytes": "1" + }, + "t_mapping(t_address,t_bool)": { + "label": "mapping(address => bool)", + "numberOfBytes": "32" + }, + "t_mapping(t_address,t_contract(IMorphoVault)20791)": { + "label": "mapping(address => contract IMorphoVault)", + "numberOfBytes": "32" + }, + "t_mapping(t_address,t_struct(TokenConfig)20098_storage)": { + "label": "mapping(address => struct TokenConfig)", + "numberOfBytes": "32" + }, + "t_mapping(t_bytes32,t_uint256)": { + "label": "mapping(bytes32 => uint256)", + "numberOfBytes": "32" + }, + "t_mapping(t_bytes4,t_bool)": { + "label": "mapping(bytes4 => bool)", + "numberOfBytes": "32" + }, + "t_mapping(t_uint256,t_struct(Request)20366_storage)": { + "label": "mapping(uint256 => struct Request)", + "numberOfBytes": "32" + }, + "t_mapping(t_uint256,t_uint256)": { + "label": "mapping(uint256 => uint256)", + "numberOfBytes": "32" + }, + "t_struct(AddressSet)3891_storage": { + "label": "struct EnumerableSetUpgradeable.AddressSet", + "members": [ + { + "label": "_inner", + "type": "t_struct(Set)3576_storage", + "offset": 0, + "slot": "0" + } + ], + "numberOfBytes": "64" + }, + "t_struct(Counter)8079_storage": { + "label": "struct Counters.Counter", + "members": [ + { + "label": "_value", + "type": "t_uint256", + "offset": 0, + "slot": "0" + } + ], + "numberOfBytes": "32" + }, + "t_struct(Request)20366_storage": { + "label": "struct Request", + "members": [ + { + "label": "sender", + "type": "t_address", + "offset": 0, + "slot": "0" + }, + { + "label": "tokenOut", + "type": "t_address", + "offset": 0, + "slot": "1" + }, + { + "label": "status", + "type": "t_enum(RequestStatus)20102", + "offset": 20, + "slot": "1" + }, + { + "label": "amountMToken", + "type": "t_uint256", + "offset": 0, + "slot": "2" + }, + { + "label": "mTokenRate", + "type": "t_uint256", + "offset": 0, + "slot": "3" + }, + { + "label": "tokenOutRate", + "type": "t_uint256", + "offset": 0, + "slot": "4" + } + ], + "numberOfBytes": "160" + }, + "t_struct(Set)3576_storage": { + "label": "struct EnumerableSetUpgradeable.Set", + "members": [ + { + "label": "_values", + "type": "t_array(t_bytes32)dyn_storage", + "offset": 0, + "slot": "0" + }, + { + "label": "_indexes", + "type": "t_mapping(t_bytes32,t_uint256)", + "offset": 0, + "slot": "1" + } + ], + "numberOfBytes": "64" + }, + "t_struct(TokenConfig)20098_storage": { + "label": "struct TokenConfig", + "members": [ + { + "label": "dataFeed", + "type": "t_address", + "offset": 0, + "slot": "0" + }, + { + "label": "fee", + "type": "t_uint256", + "offset": 0, + "slot": "1" + }, + { + "label": "allowance", + "type": "t_uint256", + "offset": 0, + "slot": "2" + }, + { + "label": "stable", + "type": "t_bool", + "offset": 0, + "slot": "3" + } + ], + "numberOfBytes": "128" + }, + "t_uint256": { + "label": "uint256", + "numberOfBytes": "32" + }, + "t_uint8": { + "label": "uint8", + "numberOfBytes": "1" + } + } + } } } } diff --git a/ROLES.md b/ROLES.md index 6ab88016..6b037ed7 100644 --- a/ROLES.md +++ b/ROLES.md @@ -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 @@ -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` | diff --git a/config/constants/addresses.ts b/config/constants/addresses.ts index 9a3beb49..71762298 100644 --- a/config/constants/addresses.ts +++ b/config/constants/addresses.ts @@ -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', @@ -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', diff --git a/config/types/tokens.ts b/config/types/tokens.ts index bbe16ecb..5260dee7 100644 --- a/config/types/tokens.ts +++ b/config/types/tokens.ts @@ -80,6 +80,7 @@ export enum MTokenNameEnum { sGold = 'sGold', turtlePST = 'turtlePST', mM1BTC = 'mM1BTC', + mGLOeuro = 'mGLOeuro', } export type MTokenName = keyof typeof MTokenNameEnum; @@ -136,6 +137,7 @@ export enum PaymentTokenNameEnum { winj = 'winj', yinj = 'yinj', eurc = 'eurc', + eurcv = 'eurcv', usdg = 'usdg', pyusd = 'pyusd', ausd = 'ausd', diff --git a/contracts/products/mGLOeuro/MGloEuroCustomAggregatorFeed.sol b/contracts/products/mGLOeuro/MGloEuroCustomAggregatorFeed.sol new file mode 100644 index 00000000..7f0b106b --- /dev/null +++ b/contracts/products/mGLOeuro/MGloEuroCustomAggregatorFeed.sol @@ -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; + } +} diff --git a/contracts/products/mGLOeuro/MGloEuroDataFeed.sol b/contracts/products/mGLOeuro/MGloEuroDataFeed.sol new file mode 100644 index 00000000..7c8f2857 --- /dev/null +++ b/contracts/products/mGLOeuro/MGloEuroDataFeed.sol @@ -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; + } +} diff --git a/contracts/products/mGLOeuro/MGloEuroDepositVaultWithMorpho.sol b/contracts/products/mGLOeuro/MGloEuroDepositVaultWithMorpho.sol new file mode 100644 index 00000000..4a3349a1 --- /dev/null +++ b/contracts/products/mGLOeuro/MGloEuroDepositVaultWithMorpho.sol @@ -0,0 +1,34 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.9; + +import "../../DepositVaultWithMorpho.sol"; +import "./MGloEuroMidasAccessControlRoles.sol"; + +/** + * @title MGloEuroDepositVaultWithMorpho + * @notice Smart contract that handles mGLOeuro minting with Morpho auto-invest + * @author RedDuck Software + */ +contract MGloEuroDepositVaultWithMorpho is + DepositVaultWithMorpho, + MGloEuroMidasAccessControlRoles +{ + /** + * @dev leaving a storage gap for futures updates + */ + uint256[50] private __gap; + + /** + * @inheritdoc ManageableVault + */ + function vaultRole() public pure override returns (bytes32) { + return M_GLO_EURO_DEPOSIT_VAULT_ADMIN_ROLE; + } + + /** + * @inheritdoc Greenlistable + */ + function greenlistedRole() public pure override returns (bytes32) { + return M_GLOBAL_GREENLISTED_ROLE; + } +} diff --git a/contracts/products/mGLOeuro/MGloEuroMidasAccessControlRoles.sol b/contracts/products/mGLOeuro/MGloEuroMidasAccessControlRoles.sol new file mode 100644 index 00000000..43b8f2ed --- /dev/null +++ b/contracts/products/mGLOeuro/MGloEuroMidasAccessControlRoles.sol @@ -0,0 +1,33 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.9; + +/** + * @title MGloEuroMidasAccessControlRoles + * @notice Base contract that stores all roles descriptors for mGLOeuro contracts + * @author RedDuck Software + */ +abstract contract MGloEuroMidasAccessControlRoles { + /** + * @notice actor that can manage MGloEuroDepositVault + */ + bytes32 public constant M_GLO_EURO_DEPOSIT_VAULT_ADMIN_ROLE = + keccak256("M_GLO_EURO_DEPOSIT_VAULT_ADMIN_ROLE"); + + /** + * @notice actor that can manage MGloEuroRedemptionVault + */ + bytes32 public constant M_GLO_EURO_REDEMPTION_VAULT_ADMIN_ROLE = + keccak256("M_GLO_EURO_REDEMPTION_VAULT_ADMIN_ROLE"); + + /** + * @notice actor that can manage MGloEuroCustomAggregatorFeed and MGloEuroDataFeed + */ + bytes32 public constant M_GLO_EURO_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE = + keccak256("M_GLO_EURO_CUSTOM_AGGREGATOR_FEED_ADMIN_ROLE"); + + /** + * @notice greenlist role for mGLOeuro + */ + bytes32 public constant M_GLOBAL_GREENLISTED_ROLE = + keccak256("M_GLOBAL_GREENLISTED_ROLE"); +} diff --git a/contracts/products/mGLOeuro/MGloEuroRedemptionVaultWithMorpho.sol b/contracts/products/mGLOeuro/MGloEuroRedemptionVaultWithMorpho.sol new file mode 100644 index 00000000..867a369e --- /dev/null +++ b/contracts/products/mGLOeuro/MGloEuroRedemptionVaultWithMorpho.sol @@ -0,0 +1,34 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.9; + +import "../../RedemptionVaultWithMorpho.sol"; +import "./MGloEuroMidasAccessControlRoles.sol"; + +/** + * @title MGloEuroRedemptionVaultWithMorpho + * @notice Smart contract that handles mGLOeuro redemptions via Morpho Vault + * @author RedDuck Software + */ +contract MGloEuroRedemptionVaultWithMorpho is + RedemptionVaultWithMorpho, + MGloEuroMidasAccessControlRoles +{ + /** + * @dev leaving a storage gap for futures updates + */ + uint256[50] private __gap; + + /** + * @inheritdoc ManageableVault + */ + function vaultRole() public pure override returns (bytes32) { + return M_GLO_EURO_REDEMPTION_VAULT_ADMIN_ROLE; + } + + /** + * @inheritdoc Greenlistable + */ + function greenlistedRole() public pure override returns (bytes32) { + return M_GLOBAL_GREENLISTED_ROLE; + } +} diff --git a/contracts/products/mGLOeuro/mGLOeuro.sol b/contracts/products/mGLOeuro/mGLOeuro.sol new file mode 100644 index 00000000..00455b31 --- /dev/null +++ b/contracts/products/mGLOeuro/mGLOeuro.sol @@ -0,0 +1,80 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.9; + +import "../../mTokenMinBalance.sol"; + +/** + * @title mGLOeuro + * @author RedDuck Software + */ +//solhint-disable contract-name-camelcase +contract mGLOeuro is mTokenMinBalance { + /** + * @notice actor that can mint mGLOeuro + */ + bytes32 public constant M_GLO_EURO_MINT_OPERATOR_ROLE = + keccak256("M_GLO_EURO_MINT_OPERATOR_ROLE"); + + /** + * @notice actor that can burn mGLOeuro + */ + bytes32 public constant M_GLO_EURO_BURN_OPERATOR_ROLE = + keccak256("M_GLO_EURO_BURN_OPERATOR_ROLE"); + + /** + * @notice actor that can pause mGLOeuro + */ + bytes32 public constant M_GLO_EURO_PAUSE_OPERATOR_ROLE = + keccak256("M_GLO_EURO_PAUSE_OPERATOR_ROLE"); + + /** + * @notice actor that is exempt from mGLOeuro min balance checks + */ + bytes32 public constant M_GLO_EURO_MIN_BALANCE_EXEMPT_ROLE = + keccak256("M_GLO_EURO_MIN_BALANCE_EXEMPT_ROLE"); + + /** + * @dev leaving a storage gap for futures updates + */ + uint256[50] private __gap; + + /** + * @inheritdoc mToken + */ + function _getNameSymbol() + internal + pure + override + returns (string memory, string memory) + { + return ("Midas Fasanara Global Euro", "mGLOeuro"); + } + + /** + * @dev AC role, owner of which can mint mGLOeuro token + */ + function _minterRole() internal pure override returns (bytes32) { + return M_GLO_EURO_MINT_OPERATOR_ROLE; + } + + /** + * @dev AC role, owner of which can burn mGLOeuro token + */ + function _burnerRole() internal pure override returns (bytes32) { + return M_GLO_EURO_BURN_OPERATOR_ROLE; + } + + /** + * @dev AC role, owner of which can pause mGLOeuro token + */ + function _pauserRole() internal pure override returns (bytes32) { + return M_GLO_EURO_PAUSE_OPERATOR_ROLE; + } + + /** + * @inheritdoc mTokenMinBalance + */ + function _minBalanceExemptRole() internal pure override returns (bytes32) { + return M_GLO_EURO_MIN_BALANCE_EXEMPT_ROLE; + } +} diff --git a/helpers/contracts.ts b/helpers/contracts.ts index c0bbfac0..51f4a742 100644 --- a/helpers/contracts.ts +++ b/helpers/contracts.ts @@ -144,6 +144,7 @@ export const contractNamesPrefixes: Record = { sGold: 'SGold', turtlePST: 'TurtlePst', mM1BTC: 'MM1Btc', + mGLOeuro: 'MGloEuro', }; export const getCommonContractNames = (): CommonContractNames => { diff --git a/helpers/mtokens-metadata.ts b/helpers/mtokens-metadata.ts index 241dcc9c..bee621b2 100644 --- a/helpers/mtokens-metadata.ts +++ b/helpers/mtokens-metadata.ts @@ -338,4 +338,9 @@ export const mTokensMetadata: Record< name: 'Midas M1 BTC Market Neutral', symbol: 'mM1-BTC', }, + mGLOeuro: { + name: 'Midas Fasanara Global Euro', + symbol: 'mGLOeuro', + isMinBalance: true, + }, }; diff --git a/helpers/roles.ts b/helpers/roles.ts index 86d1cefd..29d323ca 100644 --- a/helpers/roles.ts +++ b/helpers/roles.ts @@ -86,6 +86,7 @@ export const prefixes: Record = { sGold: 'S_GOLD', turtlePST: 'TURTLE_PST', mM1BTC: 'M_M1_BTC', + mGLOeuro: 'M_GLO_EURO', }; const mappedTokenNames: Partial> = { @@ -105,6 +106,7 @@ export const tokenLevelGreenlistTokens: MTokenName[] = [ 'mWIN', 'qHVNUSD', 'mGLO', + 'mGLOeuro', ]; /** @@ -118,6 +120,7 @@ export const sharedGreenlistRoleSource: Partial< Record > = { mGLO: 'mGLOBAL', + mGLOeuro: 'mGLOBAL', }; const getGreenlistRoleName = (token: MTokenName): string => { diff --git a/scripts/deploy/common/roles.ts b/scripts/deploy/common/roles.ts index 388c88d6..cc4dc410 100644 --- a/scripts/deploy/common/roles.ts +++ b/scripts/deploy/common/roles.ts @@ -26,6 +26,7 @@ export type GrantAllTokenRolesConfig = { tokenManagerAddress: Address; vaultsManagerAddress?: Address; oracleManagerAddress: Address; + minBalanceExemptAddresses?: Address[]; }; const acAdminAddress = '0xd4195CF4df289a4748C1A7B6dDBE770e27bA1227'; @@ -72,6 +73,11 @@ export const grantAllProductRoles = async ( ]; const oracleManagerRoles = [tokenRoles.customFeedAdmin!]; + const minBalanceExemptAddresses = + networkConfig.minBalanceExemptAddresses ?? []; + const minBalanceExemptRoles = minBalanceExemptAddresses.map( + () => tokenRoles.minBalanceExempt, + ); const defaultManager = provider.address; @@ -100,6 +106,7 @@ export const grantAllProductRoles = async ( ...tokenManagerRoles, ...vaultManagerRoles, ...oracleManagerRoles, + ...minBalanceExemptRoles, ...contractsRoles, ]; const grantAddresses = [ @@ -108,6 +115,7 @@ export const grantAllProductRoles = async ( () => networkConfig.vaultsManagerAddress ?? defaultManager, ), ...oracleManagerRoles.map(() => networkConfig.oracleManagerAddress), + ...minBalanceExemptAddresses, ...contractsAddresses, ]; diff --git a/scripts/deploy/configs/index.ts b/scripts/deploy/configs/index.ts index 4f5aa1f5..2b9991bd 100644 --- a/scripts/deploy/configs/index.ts +++ b/scripts/deploy/configs/index.ts @@ -35,6 +35,7 @@ import { mFARMDeploymentConfig } from './mFARM'; import { mFONEDeploymentConfig } from './mFONE'; import { mGLODeploymentConfig } from './mGLO'; import { mGLOBALDeploymentConfig } from './mGLOBAL'; +import { mGLOeuroDeploymentConfig } from './mGLOeuro'; import { mHYPERDeploymentConfig } from './mHYPER'; import { mHyperBTCDeploymentConfig } from './mHyperBTC'; import { mHyperETHDeploymentConfig } from './mHyperETH'; @@ -165,4 +166,5 @@ export const configsPerToken: Record = { sGold: sGoldDeploymentConfig, turtlePST: turtlePSTDeploymentConfig, mM1BTC: mM1BTCDeploymentConfig, + mGLOeuro: mGLOeuroDeploymentConfig, }; diff --git a/scripts/deploy/configs/mGLOeuro.ts b/scripts/deploy/configs/mGLOeuro.ts new file mode 100644 index 00000000..1e8e322b --- /dev/null +++ b/scripts/deploy/configs/mGLOeuro.ts @@ -0,0 +1,132 @@ +import { constants } from 'ethers'; +import { parseUnits } from 'ethers/lib/utils'; + +import { chainIds } from '../../../config'; +import { DeploymentConfig } from '../common/types'; + +export const mGLOeuroDeploymentConfig: DeploymentConfig = { + genericConfigs: { + customAggregator: { + maxAnswerDeviation: parseUnits('1', 8), + description: 'mGLOeuro/EUR', + minAnswer: parseUnits('90000', 8), + maxAnswer: parseUnits('110000', 8), + }, + dataFeed: { + minAnswer: parseUnits('90000', 8), + maxAnswer: parseUnits('110000', 8), + }, + customAggregatorAdjustedDv: { + adjustmentPercentage: parseUnits('7', 8), + underlyingFeed: 'customFeed', + }, + customAggregatorAdjustedRv: { + adjustmentPercentage: parseUnits('-7', 8), + underlyingFeed: 'customFeed', + }, + }, + networkConfigs: { + [chainIds.main]: { + dvMorpho: { + type: 'MORPHO', + enableSanctionsList: true, + feeReceiver: '0xC903840d5E314caA407C6Bc5792746b5282BBFa7', + tokensReceiver: '0x67887dd84E4778d8372BCD296E00995c59C00e52', + instantDailyLimit: parseUnits('10000000', 18), + instantFee: parseUnits('0', 2), + variationTolerance: parseUnits('2', 2), + minAmount: parseUnits('1.1', 18), + minMTokenAmountForFirstDeposit: parseUnits('0', 18), + maxSupplyCap: constants.MaxUint256, + }, + rvMorpho: { + type: 'MORPHO', + feeReceiver: '0x67887dd84E4778d8372BCD296E00995c59C00e52', + tokensReceiver: '0x67887dd84E4778d8372BCD296E00995c59C00e52', + requestRedeemer: '0x7Ec5C012d2f140BE7f55c43777B399442ec8AF51', + instantDailyLimit: parseUnits('500000', 18), + instantFee: parseUnits('0.5', 2), + variationTolerance: parseUnits('2', 2), + minAmount: parseUnits('1.1', 18), + enableSanctionsList: true, + fiatAdditionalFee: parseUnits('0.1', 2), + fiatFlatFee: parseUnits('30', 18), + minFiatRedeemAmount: parseUnits('1000', 18), + }, + postDeploy: { + addPaymentTokens: { + vaults: [ + { + paymentTokens: [ + { + token: 'eurc', + allowance: parseUnits('1000000000', 18), + isStable: true, + fee: parseUnits('0', 2), + }, + { + token: 'eurcv', + allowance: parseUnits('500000000', 18), + isStable: true, + fee: parseUnits('0', 2), + }, + ], + type: 'depositVaultMorpho', + }, + { + paymentTokens: [ + { + token: 'eurc', + allowance: parseUnits('1000000000', 18), + isStable: true, + fee: parseUnits('0', 2), + }, + { + token: 'eurcv', + allowance: parseUnits('500000000', 18), + isStable: true, + fee: parseUnits('0', 2), + }, + ], + type: 'redemptionVaultMorpho', + }, + ], + }, + grantRoles: { + tokenManagerAddress: '0x462C735196AF2277deE9f15Ac60B45E7Fe8415AA', + vaultsManagerAddress: '0x2ACB4BdCbEf02f81BF713b696Ac26390d7f79A12', + oracleManagerAddress: '0xa301F0eD658f72e0520fc47b42888bc985eF600c', + minBalanceExemptAddresses: [ + '0x67887dd84E4778d8372BCD296E00995c59C00e52', + ], + }, + greenlist: { + depositVaultMorpho: true, + redemptionVaultMorpho: true, + }, + pauseFunctions: { + depositVaultMorpho: [ + 'depositRequest', + 'depositRequestWithCustomRecipient', + ], + redemptionVaultMorpho: ['redeemFiatRequest'], + }, + setRoundData: { + data: parseUnits('100000', 8), + }, + setMorphoConfig: [ + { + type: 'depositVaultMorpho', + vaults: [], + depositsEnabled: false, + autoInvestFallbackEnabled: true, + }, + { + type: 'redemptionVaultMorpho', + vaults: [], + }, + ], + }, + }, + }, +}; diff --git a/scripts/deploy/configs/payment-tokens.ts b/scripts/deploy/configs/payment-tokens.ts index 4de42d13..54e52e05 100644 --- a/scripts/deploy/configs/payment-tokens.ts +++ b/scripts/deploy/configs/payment-tokens.ts @@ -382,6 +382,41 @@ export const paymentTokenDeploymentConfigs: PaymentTokenDeploymentConfig = { maxAnswer: parseUnits('1.250000', 6), }, }, + eurc: { + dataFeed: { + // EURC/EUR = (USD per EURC) / (USD per EUR). + // Each wrapper converts its native 8-decimal Chainlink answer to 18 decimals. + numerator: { + healthyDiff: 24 * 60 * 60, + }, + denominator: { + healthyDiff: 24 * 60 * 60, + }, + feedType: 'composite', + // Composite output and bounds are EUR per EURC in 18 decimals (1e18 = 1 EUR per EURC). + // The standard stable-payment-token bounds are shared by the DV and RV. + minAnswer: parseUnits('0.997'), + maxAnswer: parseUnits('1.003'), + }, + }, + eurcv: { + customAggregator: { + description: 'EURCV/EUR', + minAnswer: parseUnits('0.9999', 8), + maxAnswer: parseUnits('1', 8), + maxAnswerDeviation: parseUnits('0', 8), + }, + dataFeed: { + healthyDiff: constants.MaxUint256, + minAnswer: parseUnits('0.9999', 8), + maxAnswer: parseUnits('1', 8), + }, + postDeploy: { + setRoundData: { + data: parseUnits('1', 8), + }, + }, + }, }, [chainIds.base]: { usdc: { diff --git a/scripts/dump_roles.ts b/scripts/dump_roles.ts index ae3c5668..0cf6aef4 100644 --- a/scripts/dump_roles.ts +++ b/scripts/dump_roles.ts @@ -1,5 +1,6 @@ import { writeFile } from 'fs/promises'; +import { mTokensMetadata } from '../helpers/mtokens-metadata'; import { getAllRoles } from '../helpers/roles'; const formatKey = (k: string) => { @@ -17,10 +18,17 @@ const func = async () => { const tokensTables = Object.entries(tokenRoles).reduce( (prev, [mToken, roles]) => { - const mdRows = Object.entries(roles).map(([role, value]) => [ - formatKey(role), - formatValue(value as string), - ]); + const mdRows = Object.entries(roles) + .filter( + ([role]) => + role !== 'minBalanceExempt' || + mTokensMetadata[mToken as keyof typeof mTokensMetadata] + ?.isMinBalance, + ) + .map(([role, value]) => [ + formatKey(role), + formatValue(value as string), + ]); prev += `### ${mToken} Roles\n\n`; diff --git a/test/common/token.tests.ts b/test/common/token.tests.ts index 3902bf14..12951267 100644 --- a/test/common/token.tests.ts +++ b/test/common/token.tests.ts @@ -110,7 +110,8 @@ export const tokenContractsTests = (token: MTokenName) => { }; const isPermissioned = !!mTokensMetadata[token]?.isPermissioned; - const unitAmount = mTokensMetadata[token]?.isMinBalance ? parseUnits('1') : 1; + const isMinBalance = !!mTokensMetadata[token]?.isMinBalance; + const unitAmount = isMinBalance ? parseUnits('1') : 1; const deployMTokenWithFixture = async () => { const fixture = await loadFixture(defaultDeploy); @@ -350,6 +351,11 @@ export const tokenContractsTests = (token: MTokenName) => { expect(await contract[tokenRoleNames.burner]()).eq(tokenRoles.burner); expect(await contract[tokenRoleNames.minter]()).eq(tokenRoles.minter); expect(await contract[tokenRoleNames.pauser]()).eq(tokenRoles.pauser); + if (isMinBalance) { + expect(await contract[tokenRoleNames.minBalanceExempt]()).eq( + tokenRoles.minBalanceExempt, + ); + } expect(await contract[allRoleNames.defaultAdmin]()).eq( allRoles.common.defaultAdmin, From 9ed3453b8fa85859c929417d221bf0807d311176 Mon Sep 17 00:00:00 2001 From: Dmytro Horbatenko Date: Thu, 30 Jul 2026 16:46:00 +0300 Subject: [PATCH 19/19] feat: mGLO on Optimism --- .openzeppelin/optimism.json | 1552 ++++++++++++++++++++++++++++++++ config/constants/addresses.ts | 10 + scripts/deploy/configs/mGLO.ts | 72 ++ 3 files changed, 1634 insertions(+) diff --git a/.openzeppelin/optimism.json b/.openzeppelin/optimism.json index ded6c36b..bc934382 100644 --- a/.openzeppelin/optimism.json +++ b/.openzeppelin/optimism.json @@ -149,6 +149,36 @@ "address": "0x12Ae90dCe5C2a4Ee5141FBfc408ff1022D051F42", "txHash": "0xeb76c96323a1e0daabfb430315f5d5f65fe7173509df08f86a1dda380f4cce11", "kind": "transparent" + }, + { + "address": "0x1Eaf7cceAA5DC6d605760718722fbcC58579b3Ff", + "txHash": "0xf6452cae920dceb114e18256ecbf87492d64f583f5f29d818c91b902c6ee7318", + "kind": "transparent" + }, + { + "address": "0x4c8f5CEd7F3211ad1D70B6F33e585309Fb1AcA75", + "txHash": "0x5c87a8a048b835f26fecf6e97541dc59ed8d3ffb3714f1fbf5985037265a95c3", + "kind": "transparent" + }, + { + "address": "0xAf91e7754E12c90F3F07a1719E39a65Fc58204ff", + "txHash": "0x12128927cec5f04ffe5f96f6cd817f8a2e145d4e983a48dbd59dfa20bc41eeb8", + "kind": "transparent" + }, + { + "address": "0xB391486b0d058176A845aCcF2Cb160A2be9CfCb8", + "txHash": "0x38513b651d0cc93471575a3bc5f88aa8a7439671256b3c90475bf68dae7191ce", + "kind": "transparent" + }, + { + "address": "0xa5d0E53059b648B7615B67602De4489079EAe2B4", + "txHash": "0xbc48f2a325486db704f878ff854d75bc035a91768dbbfe29d14ff02fbe14b34b", + "kind": "transparent" + }, + { + "address": "0x99656E9753047F2769D10fa65B9D8ebfE44B35ab", + "txHash": "0x6848e1b7fd38c3037445257391b42f81b3aac1f6f005a5037647f5a2b016f871", + "kind": "transparent" } ], "impls": { @@ -7161,6 +7191,1528 @@ } } } + }, + "bd43bec9d71009d2d5057a25b7b2ff2271c3c1265815d8930c185e8c634ef4cd": { + "address": "0x6a2A00B07FEF2beeCa67c454b093963789D9d194", + "txHash": "0xa7465366951c068f4d2096d91562ed3bf3b38430249358008eb23191761215e0", + "layout": { + "solcVersion": "0.8.9", + "storage": [ + { + "label": "_initialized", + "offset": 0, + "slot": "0", + "type": "t_uint8", + "contract": "Initializable", + "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:63", + "retypedFrom": "bool" + }, + { + "label": "_initializing", + "offset": 1, + "slot": "0", + "type": "t_bool", + "contract": "Initializable", + "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:68" + }, + { + "label": "__gap", + "offset": 0, + "slot": "1", + "type": "t_array(t_uint256)50_storage", + "contract": "ContextUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/utils/ContextUpgradeable.sol:36" + }, + { + "label": "_balances", + "offset": 0, + "slot": "51", + "type": "t_mapping(t_address,t_uint256)", + "contract": "ERC20Upgradeable", + "src": "@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol:40" + }, + { + "label": "_allowances", + "offset": 0, + "slot": "52", + "type": "t_mapping(t_address,t_mapping(t_address,t_uint256))", + "contract": "ERC20Upgradeable", + "src": "@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol:42" + }, + { + "label": "_totalSupply", + "offset": 0, + "slot": "53", + "type": "t_uint256", + "contract": "ERC20Upgradeable", + "src": "@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol:44" + }, + { + "label": "_name", + "offset": 0, + "slot": "54", + "type": "t_string_storage", + "contract": "ERC20Upgradeable", + "src": "@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol:46" + }, + { + "label": "_symbol", + "offset": 0, + "slot": "55", + "type": "t_string_storage", + "contract": "ERC20Upgradeable", + "src": "@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol:47" + }, + { + "label": "__gap", + "offset": 0, + "slot": "56", + "type": "t_array(t_uint256)45_storage", + "contract": "ERC20Upgradeable", + "src": "@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol:376" + }, + { + "label": "_paused", + "offset": 0, + "slot": "101", + "type": "t_bool", + "contract": "PausableUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/security/PausableUpgradeable.sol:29" + }, + { + "label": "__gap", + "offset": 0, + "slot": "102", + "type": "t_array(t_uint256)49_storage", + "contract": "PausableUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/security/PausableUpgradeable.sol:116" + }, + { + "label": "__gap", + "offset": 0, + "slot": "151", + "type": "t_array(t_uint256)50_storage", + "contract": "ERC20PausableUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/token/ERC20/extensions/ERC20PausableUpgradeable.sol:48" + }, + { + "label": "accessControl", + "offset": 0, + "slot": "201", + "type": "t_contract(MidasAccessControl)17280", + "contract": "WithMidasAccessControl", + "src": "contracts/access/WithMidasAccessControl.sol:24" + }, + { + "label": "__gap", + "offset": 0, + "slot": "202", + "type": "t_array(t_uint256)50_storage", + "contract": "WithMidasAccessControl", + "src": "contracts/access/WithMidasAccessControl.sol:29" + }, + { + "label": "__gap", + "offset": 0, + "slot": "252", + "type": "t_array(t_uint256)50_storage", + "contract": "Blacklistable", + "src": "contracts/access/Blacklistable.sol:16" + }, + { + "label": "metadata", + "offset": 0, + "slot": "302", + "type": "t_mapping(t_bytes32,t_bytes_storage)", + "contract": "mToken", + "src": "contracts/mToken.sol:18" + }, + { + "label": "__gap", + "offset": 0, + "slot": "303", + "type": "t_array(t_uint256)50_storage", + "contract": "mToken", + "src": "contracts/mToken.sol:23" + }, + { + "label": "__gap", + "offset": 0, + "slot": "353", + "type": "t_array(t_uint256)50_storage", + "contract": "mGLO", + "src": "contracts/products/mGLO/mGLO.sol:33" + } + ], + "types": { + "t_address": { + "label": "address", + "numberOfBytes": "20" + }, + "t_array(t_uint256)45_storage": { + "label": "uint256[45]", + "numberOfBytes": "1440" + }, + "t_array(t_uint256)49_storage": { + "label": "uint256[49]", + "numberOfBytes": "1568" + }, + "t_array(t_uint256)50_storage": { + "label": "uint256[50]", + "numberOfBytes": "1600" + }, + "t_bool": { + "label": "bool", + "numberOfBytes": "1" + }, + "t_bytes32": { + "label": "bytes32", + "numberOfBytes": "32" + }, + "t_bytes_storage": { + "label": "bytes", + "numberOfBytes": "32" + }, + "t_contract(MidasAccessControl)17280": { + "label": "contract MidasAccessControl", + "numberOfBytes": "20" + }, + "t_mapping(t_address,t_mapping(t_address,t_uint256))": { + "label": "mapping(address => mapping(address => uint256))", + "numberOfBytes": "32" + }, + "t_mapping(t_address,t_uint256)": { + "label": "mapping(address => uint256)", + "numberOfBytes": "32" + }, + "t_mapping(t_bytes32,t_bytes_storage)": { + "label": "mapping(bytes32 => bytes)", + "numberOfBytes": "32" + }, + "t_string_storage": { + "label": "string", + "numberOfBytes": "32" + }, + "t_uint256": { + "label": "uint256", + "numberOfBytes": "32" + }, + "t_uint8": { + "label": "uint8", + "numberOfBytes": "1" + } + } + } + }, + "8fa3463fdea2a95bc34777b60b21a82202e4b168b2fc9422f0e6dc55ca35556a": { + "address": "0x4640878904492027faB0edd8d5193E054aB37F62", + "txHash": "0x0ef551572603b3dec0ea8f04d7b0778ea2d56d89fc3299fb2d13e411ba4d5bac", + "layout": { + "solcVersion": "0.8.9", + "storage": [ + { + "label": "_initialized", + "offset": 0, + "slot": "0", + "type": "t_uint8", + "contract": "Initializable", + "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:63", + "retypedFrom": "bool" + }, + { + "label": "_initializing", + "offset": 1, + "slot": "0", + "type": "t_bool", + "contract": "Initializable", + "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:68" + }, + { + "label": "accessControl", + "offset": 2, + "slot": "0", + "type": "t_contract(MidasAccessControl)17280", + "contract": "WithMidasAccessControl", + "src": "contracts/access/WithMidasAccessControl.sol:24" + }, + { + "label": "__gap", + "offset": 0, + "slot": "1", + "type": "t_array(t_uint256)50_storage", + "contract": "WithMidasAccessControl", + "src": "contracts/access/WithMidasAccessControl.sol:29" + }, + { + "label": "description", + "offset": 0, + "slot": "51", + "type": "t_string_storage", + "contract": "CustomAggregatorV3CompatibleFeed", + "src": "contracts/feeds/CustomAggregatorV3CompatibleFeed.sol:31" + }, + { + "label": "latestRound", + "offset": 0, + "slot": "52", + "type": "t_uint80", + "contract": "CustomAggregatorV3CompatibleFeed", + "src": "contracts/feeds/CustomAggregatorV3CompatibleFeed.sol:36" + }, + { + "label": "maxAnswerDeviation", + "offset": 0, + "slot": "53", + "type": "t_uint256", + "contract": "CustomAggregatorV3CompatibleFeed", + "src": "contracts/feeds/CustomAggregatorV3CompatibleFeed.sol:42" + }, + { + "label": "minAnswer", + "offset": 0, + "slot": "54", + "type": "t_int192", + "contract": "CustomAggregatorV3CompatibleFeed", + "src": "contracts/feeds/CustomAggregatorV3CompatibleFeed.sol:47" + }, + { + "label": "maxAnswer", + "offset": 0, + "slot": "55", + "type": "t_int192", + "contract": "CustomAggregatorV3CompatibleFeed", + "src": "contracts/feeds/CustomAggregatorV3CompatibleFeed.sol:52" + }, + { + "label": "_roundData", + "offset": 0, + "slot": "56", + "type": "t_mapping(t_uint80,t_struct(RoundData)17975_storage)", + "contract": "CustomAggregatorV3CompatibleFeed", + "src": "contracts/feeds/CustomAggregatorV3CompatibleFeed.sol:57" + }, + { + "label": "__gap", + "offset": 0, + "slot": "57", + "type": "t_array(t_uint256)50_storage", + "contract": "MGloCustomAggregatorFeed", + "src": "contracts/products/mGLO/MGloCustomAggregatorFeed.sol:20" + } + ], + "types": { + "t_array(t_uint256)50_storage": { + "label": "uint256[50]", + "numberOfBytes": "1600" + }, + "t_bool": { + "label": "bool", + "numberOfBytes": "1" + }, + "t_contract(MidasAccessControl)17280": { + "label": "contract MidasAccessControl", + "numberOfBytes": "20" + }, + "t_int192": { + "label": "int192", + "numberOfBytes": "24" + }, + "t_int256": { + "label": "int256", + "numberOfBytes": "32" + }, + "t_mapping(t_uint80,t_struct(RoundData)17975_storage)": { + "label": "mapping(uint80 => struct CustomAggregatorV3CompatibleFeed.RoundData)", + "numberOfBytes": "32" + }, + "t_string_storage": { + "label": "string", + "numberOfBytes": "32" + }, + "t_struct(RoundData)17975_storage": { + "label": "struct CustomAggregatorV3CompatibleFeed.RoundData", + "members": [ + { + "label": "roundId", + "type": "t_uint80", + "offset": 0, + "slot": "0" + }, + { + "label": "answer", + "type": "t_int256", + "offset": 0, + "slot": "1" + }, + { + "label": "startedAt", + "type": "t_uint256", + "offset": 0, + "slot": "2" + }, + { + "label": "updatedAt", + "type": "t_uint256", + "offset": 0, + "slot": "3" + }, + { + "label": "answeredInRound", + "type": "t_uint80", + "offset": 0, + "slot": "4" + } + ], + "numberOfBytes": "160" + }, + "t_uint256": { + "label": "uint256", + "numberOfBytes": "32" + }, + "t_uint8": { + "label": "uint8", + "numberOfBytes": "1" + }, + "t_uint80": { + "label": "uint80", + "numberOfBytes": "10" + } + } + } + }, + "1c466f9d3d59dc637bb3ab9803405d25102d82828a68bfb578c4915a4586949b": { + "address": "0x760B413c8dF6f2C5fdCa80aB58b43e7389C41A83", + "txHash": "0x50ee552138998a4a7cae7177f1f7a4db46a12791bf599fdc40b2f79210332194", + "layout": { + "solcVersion": "0.8.9", + "storage": [ + { + "label": "_initialized", + "offset": 0, + "slot": "0", + "type": "t_uint8", + "contract": "Initializable", + "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:63", + "retypedFrom": "bool" + }, + { + "label": "_initializing", + "offset": 1, + "slot": "0", + "type": "t_bool", + "contract": "Initializable", + "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:68" + }, + { + "label": "accessControl", + "offset": 2, + "slot": "0", + "type": "t_contract(MidasAccessControl)17280", + "contract": "WithMidasAccessControl", + "src": "contracts/access/WithMidasAccessControl.sol:24" + }, + { + "label": "__gap", + "offset": 0, + "slot": "1", + "type": "t_array(t_uint256)50_storage", + "contract": "WithMidasAccessControl", + "src": "contracts/access/WithMidasAccessControl.sol:29" + }, + { + "label": "aggregator", + "offset": 0, + "slot": "51", + "type": "t_contract(AggregatorV3Interface)45", + "contract": "DataFeed", + "src": "contracts/feeds/DataFeed.sol:22" + }, + { + "label": "healthyDiff", + "offset": 0, + "slot": "52", + "type": "t_uint256", + "contract": "DataFeed", + "src": "contracts/feeds/DataFeed.sol:27" + }, + { + "label": "minExpectedAnswer", + "offset": 0, + "slot": "53", + "type": "t_int256", + "contract": "DataFeed", + "src": "contracts/feeds/DataFeed.sol:32" + }, + { + "label": "maxExpectedAnswer", + "offset": 0, + "slot": "54", + "type": "t_int256", + "contract": "DataFeed", + "src": "contracts/feeds/DataFeed.sol:37" + }, + { + "label": "__gap", + "offset": 0, + "slot": "55", + "type": "t_array(t_uint256)50_storage", + "contract": "DataFeed", + "src": "contracts/feeds/DataFeed.sol:42" + }, + { + "label": "__gap", + "offset": 0, + "slot": "105", + "type": "t_array(t_uint256)50_storage", + "contract": "MGloDataFeed", + "src": "contracts/products/mGLO/MGloDataFeed.sol:16" + } + ], + "types": { + "t_array(t_uint256)50_storage": { + "label": "uint256[50]", + "numberOfBytes": "1600" + }, + "t_bool": { + "label": "bool", + "numberOfBytes": "1" + }, + "t_contract(AggregatorV3Interface)45": { + "label": "contract AggregatorV3Interface", + "numberOfBytes": "20" + }, + "t_contract(MidasAccessControl)17280": { + "label": "contract MidasAccessControl", + "numberOfBytes": "20" + }, + "t_int256": { + "label": "int256", + "numberOfBytes": "32" + }, + "t_uint256": { + "label": "uint256", + "numberOfBytes": "32" + }, + "t_uint8": { + "label": "uint8", + "numberOfBytes": "1" + } + } + } + }, + "99af436b93e7b8e3b548df81a443da1feae190805ed7f81051bad1f11e0610a3": { + "address": "0xC8259d5F756c8440B8762C6da3Bb1Fd018d23430", + "txHash": "0x858ca35450fcf46e5c820ffbcb2621792a7f92f5c26de961dba063ec6eaf8bc4", + "layout": { + "solcVersion": "0.8.9", + "storage": [ + { + "label": "_initialized", + "offset": 0, + "slot": "0", + "type": "t_uint8", + "contract": "Initializable", + "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:63", + "retypedFrom": "bool" + }, + { + "label": "_initializing", + "offset": 1, + "slot": "0", + "type": "t_bool", + "contract": "Initializable", + "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:68" + }, + { + "label": "accessControl", + "offset": 2, + "slot": "0", + "type": "t_contract(MidasAccessControl)17280", + "contract": "WithMidasAccessControl", + "src": "contracts/access/WithMidasAccessControl.sol:24" + }, + { + "label": "__gap", + "offset": 0, + "slot": "1", + "type": "t_array(t_uint256)50_storage", + "contract": "WithMidasAccessControl", + "src": "contracts/access/WithMidasAccessControl.sol:29" + }, + { + "label": "__gap", + "offset": 0, + "slot": "51", + "type": "t_array(t_uint256)50_storage", + "contract": "ContextUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/utils/ContextUpgradeable.sol:36" + }, + { + "label": "_paused", + "offset": 0, + "slot": "101", + "type": "t_bool", + "contract": "PausableUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/security/PausableUpgradeable.sol:29" + }, + { + "label": "__gap", + "offset": 0, + "slot": "102", + "type": "t_array(t_uint256)49_storage", + "contract": "PausableUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/security/PausableUpgradeable.sol:116" + }, + { + "label": "fnPaused", + "offset": 0, + "slot": "151", + "type": "t_mapping(t_bytes4,t_bool)", + "contract": "Pausable", + "src": "contracts/access/Pausable.sol:14" + }, + { + "label": "__gap", + "offset": 0, + "slot": "152", + "type": "t_array(t_uint256)50_storage", + "contract": "Pausable", + "src": "contracts/access/Pausable.sol:19" + }, + { + "label": "__gap", + "offset": 0, + "slot": "202", + "type": "t_array(t_uint256)50_storage", + "contract": "Blacklistable", + "src": "contracts/access/Blacklistable.sol:16" + }, + { + "label": "greenlistEnabled", + "offset": 0, + "slot": "252", + "type": "t_bool", + "contract": "Greenlistable", + "src": "contracts/access/Greenlistable.sol:16" + }, + { + "label": "__gap", + "offset": 0, + "slot": "253", + "type": "t_array(t_uint256)50_storage", + "contract": "Greenlistable", + "src": "contracts/access/Greenlistable.sol:21" + }, + { + "label": "sanctionsList", + "offset": 0, + "slot": "303", + "type": "t_address", + "contract": "WithSanctionsList", + "src": "contracts/abstract/WithSanctionsList.sol:18" + }, + { + "label": "__gap", + "offset": 0, + "slot": "304", + "type": "t_array(t_uint256)50_storage", + "contract": "WithSanctionsList", + "src": "contracts/abstract/WithSanctionsList.sol:23" + }, + { + "label": "currentRequestId", + "offset": 0, + "slot": "354", + "type": "t_struct(Counter)8079_storage", + "contract": "ManageableVault", + "src": "contracts/abstract/ManageableVault.sol:53" + }, + { + "label": "mToken", + "offset": 0, + "slot": "355", + "type": "t_contract(IMToken)20085", + "contract": "ManageableVault", + "src": "contracts/abstract/ManageableVault.sol:66" + }, + { + "label": "mTokenDataFeed", + "offset": 0, + "slot": "356", + "type": "t_contract(IDataFeed)19792", + "contract": "ManageableVault", + "src": "contracts/abstract/ManageableVault.sol:71" + }, + { + "label": "tokensReceiver", + "offset": 0, + "slot": "357", + "type": "t_address", + "contract": "ManageableVault", + "src": "contracts/abstract/ManageableVault.sol:76" + }, + { + "label": "instantFee", + "offset": 0, + "slot": "358", + "type": "t_uint256", + "contract": "ManageableVault", + "src": "contracts/abstract/ManageableVault.sol:81" + }, + { + "label": "instantDailyLimit", + "offset": 0, + "slot": "359", + "type": "t_uint256", + "contract": "ManageableVault", + "src": "contracts/abstract/ManageableVault.sol:88" + }, + { + "label": "dailyLimits", + "offset": 0, + "slot": "360", + "type": "t_mapping(t_uint256,t_uint256)", + "contract": "ManageableVault", + "src": "contracts/abstract/ManageableVault.sol:93" + }, + { + "label": "feeReceiver", + "offset": 0, + "slot": "361", + "type": "t_address", + "contract": "ManageableVault", + "src": "contracts/abstract/ManageableVault.sol:98" + }, + { + "label": "variationTolerance", + "offset": 0, + "slot": "362", + "type": "t_uint256", + "contract": "ManageableVault", + "src": "contracts/abstract/ManageableVault.sol:103" + }, + { + "label": "waivedFeeRestriction", + "offset": 0, + "slot": "363", + "type": "t_mapping(t_address,t_bool)", + "contract": "ManageableVault", + "src": "contracts/abstract/ManageableVault.sol:108" + }, + { + "label": "_paymentTokens", + "offset": 0, + "slot": "364", + "type": "t_struct(AddressSet)3891_storage", + "contract": "ManageableVault", + "src": "contracts/abstract/ManageableVault.sol:113" + }, + { + "label": "tokensConfig", + "offset": 0, + "slot": "366", + "type": "t_mapping(t_address,t_struct(TokenConfig)20098_storage)", + "contract": "ManageableVault", + "src": "contracts/abstract/ManageableVault.sol:118" + }, + { + "label": "minAmount", + "offset": 0, + "slot": "367", + "type": "t_uint256", + "contract": "ManageableVault", + "src": "contracts/abstract/ManageableVault.sol:123" + }, + { + "label": "isFreeFromMinAmount", + "offset": 0, + "slot": "368", + "type": "t_mapping(t_address,t_bool)", + "contract": "ManageableVault", + "src": "contracts/abstract/ManageableVault.sol:128" + }, + { + "label": "__gap", + "offset": 0, + "slot": "369", + "type": "t_array(t_uint256)50_storage", + "contract": "ManageableVault", + "src": "contracts/abstract/ManageableVault.sol:133" + }, + { + "label": "minMTokenAmountForFirstDeposit", + "offset": 0, + "slot": "419", + "type": "t_uint256", + "contract": "DepositVault", + "src": "contracts/DepositVault.sol:78" + }, + { + "label": "mintRequests", + "offset": 0, + "slot": "420", + "type": "t_mapping(t_uint256,t_struct(Request)19809_storage)", + "contract": "DepositVault", + "src": "contracts/DepositVault.sol:83" + }, + { + "label": "totalMinted", + "offset": 0, + "slot": "421", + "type": "t_mapping(t_address,t_uint256)", + "contract": "DepositVault", + "src": "contracts/DepositVault.sol:88" + }, + { + "label": "maxSupplyCap", + "offset": 0, + "slot": "422", + "type": "t_uint256", + "contract": "DepositVault", + "src": "contracts/DepositVault.sol:95" + }, + { + "label": "__gap", + "offset": 0, + "slot": "423", + "type": "t_array(t_uint256)49_storage", + "contract": "DepositVault", + "src": "contracts/DepositVault.sol:103" + }, + { + "label": "__gap", + "offset": 0, + "slot": "472", + "type": "t_array(t_uint256)50_storage", + "contract": "MGloDepositVault", + "src": "contracts/products/mGLO/MGloDepositVault.sol:16" + } + ], + "types": { + "t_address": { + "label": "address", + "numberOfBytes": "20" + }, + "t_array(t_bytes32)dyn_storage": { + "label": "bytes32[]", + "numberOfBytes": "32" + }, + "t_array(t_uint256)49_storage": { + "label": "uint256[49]", + "numberOfBytes": "1568" + }, + "t_array(t_uint256)50_storage": { + "label": "uint256[50]", + "numberOfBytes": "1600" + }, + "t_bool": { + "label": "bool", + "numberOfBytes": "1" + }, + "t_bytes32": { + "label": "bytes32", + "numberOfBytes": "32" + }, + "t_bytes4": { + "label": "bytes4", + "numberOfBytes": "4" + }, + "t_contract(IDataFeed)19792": { + "label": "contract IDataFeed", + "numberOfBytes": "20" + }, + "t_contract(IMToken)20085": { + "label": "contract IMToken", + "numberOfBytes": "20" + }, + "t_contract(MidasAccessControl)17280": { + "label": "contract MidasAccessControl", + "numberOfBytes": "20" + }, + "t_enum(RequestStatus)20102": { + "label": "enum RequestStatus", + "members": ["Pending", "Processed", "Canceled"], + "numberOfBytes": "1" + }, + "t_mapping(t_address,t_bool)": { + "label": "mapping(address => bool)", + "numberOfBytes": "32" + }, + "t_mapping(t_address,t_struct(TokenConfig)20098_storage)": { + "label": "mapping(address => struct TokenConfig)", + "numberOfBytes": "32" + }, + "t_mapping(t_address,t_uint256)": { + "label": "mapping(address => uint256)", + "numberOfBytes": "32" + }, + "t_mapping(t_bytes32,t_uint256)": { + "label": "mapping(bytes32 => uint256)", + "numberOfBytes": "32" + }, + "t_mapping(t_bytes4,t_bool)": { + "label": "mapping(bytes4 => bool)", + "numberOfBytes": "32" + }, + "t_mapping(t_uint256,t_struct(Request)19809_storage)": { + "label": "mapping(uint256 => struct Request)", + "numberOfBytes": "32" + }, + "t_mapping(t_uint256,t_uint256)": { + "label": "mapping(uint256 => uint256)", + "numberOfBytes": "32" + }, + "t_struct(AddressSet)3891_storage": { + "label": "struct EnumerableSetUpgradeable.AddressSet", + "members": [ + { + "label": "_inner", + "type": "t_struct(Set)3576_storage", + "offset": 0, + "slot": "0" + } + ], + "numberOfBytes": "64" + }, + "t_struct(Counter)8079_storage": { + "label": "struct Counters.Counter", + "members": [ + { + "label": "_value", + "type": "t_uint256", + "offset": 0, + "slot": "0" + } + ], + "numberOfBytes": "32" + }, + "t_struct(Request)19809_storage": { + "label": "struct Request", + "members": [ + { + "label": "sender", + "type": "t_address", + "offset": 0, + "slot": "0" + }, + { + "label": "tokenIn", + "type": "t_address", + "offset": 0, + "slot": "1" + }, + { + "label": "status", + "type": "t_enum(RequestStatus)20102", + "offset": 20, + "slot": "1" + }, + { + "label": "depositedUsdAmount", + "type": "t_uint256", + "offset": 0, + "slot": "2" + }, + { + "label": "usdAmountWithoutFees", + "type": "t_uint256", + "offset": 0, + "slot": "3" + }, + { + "label": "tokenOutRate", + "type": "t_uint256", + "offset": 0, + "slot": "4" + } + ], + "numberOfBytes": "160" + }, + "t_struct(Set)3576_storage": { + "label": "struct EnumerableSetUpgradeable.Set", + "members": [ + { + "label": "_values", + "type": "t_array(t_bytes32)dyn_storage", + "offset": 0, + "slot": "0" + }, + { + "label": "_indexes", + "type": "t_mapping(t_bytes32,t_uint256)", + "offset": 0, + "slot": "1" + } + ], + "numberOfBytes": "64" + }, + "t_struct(TokenConfig)20098_storage": { + "label": "struct TokenConfig", + "members": [ + { + "label": "dataFeed", + "type": "t_address", + "offset": 0, + "slot": "0" + }, + { + "label": "fee", + "type": "t_uint256", + "offset": 0, + "slot": "1" + }, + { + "label": "allowance", + "type": "t_uint256", + "offset": 0, + "slot": "2" + }, + { + "label": "stable", + "type": "t_bool", + "offset": 0, + "slot": "3" + } + ], + "numberOfBytes": "128" + }, + "t_uint256": { + "label": "uint256", + "numberOfBytes": "32" + }, + "t_uint8": { + "label": "uint8", + "numberOfBytes": "1" + } + } + } + }, + "6d87829d923eaee078c11820df3f6ef282b4fa138c0179e1010fce7f88e60671": { + "address": "0xE6DcC8b0EA00b202224B2fe519E6677BEA3aC381", + "txHash": "0xd1b5d0670daafc5ac610f984d1ca03630277d495a08eae0d15ff5e504895b0e8", + "layout": { + "solcVersion": "0.8.9", + "storage": [ + { + "label": "_initialized", + "offset": 0, + "slot": "0", + "type": "t_uint8", + "contract": "Initializable", + "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:63", + "retypedFrom": "bool" + }, + { + "label": "_initializing", + "offset": 1, + "slot": "0", + "type": "t_bool", + "contract": "Initializable", + "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:68" + }, + { + "label": "accessControl", + "offset": 2, + "slot": "0", + "type": "t_contract(MidasAccessControl)17280", + "contract": "WithMidasAccessControl", + "src": "contracts/access/WithMidasAccessControl.sol:24" + }, + { + "label": "__gap", + "offset": 0, + "slot": "1", + "type": "t_array(t_uint256)50_storage", + "contract": "WithMidasAccessControl", + "src": "contracts/access/WithMidasAccessControl.sol:29" + }, + { + "label": "__gap", + "offset": 0, + "slot": "51", + "type": "t_array(t_uint256)50_storage", + "contract": "ContextUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/utils/ContextUpgradeable.sol:36" + }, + { + "label": "_paused", + "offset": 0, + "slot": "101", + "type": "t_bool", + "contract": "PausableUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/security/PausableUpgradeable.sol:29" + }, + { + "label": "__gap", + "offset": 0, + "slot": "102", + "type": "t_array(t_uint256)49_storage", + "contract": "PausableUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/security/PausableUpgradeable.sol:116" + }, + { + "label": "fnPaused", + "offset": 0, + "slot": "151", + "type": "t_mapping(t_bytes4,t_bool)", + "contract": "Pausable", + "src": "contracts/access/Pausable.sol:14" + }, + { + "label": "__gap", + "offset": 0, + "slot": "152", + "type": "t_array(t_uint256)50_storage", + "contract": "Pausable", + "src": "contracts/access/Pausable.sol:19" + }, + { + "label": "__gap", + "offset": 0, + "slot": "202", + "type": "t_array(t_uint256)50_storage", + "contract": "Blacklistable", + "src": "contracts/access/Blacklistable.sol:16" + }, + { + "label": "greenlistEnabled", + "offset": 0, + "slot": "252", + "type": "t_bool", + "contract": "Greenlistable", + "src": "contracts/access/Greenlistable.sol:16" + }, + { + "label": "__gap", + "offset": 0, + "slot": "253", + "type": "t_array(t_uint256)50_storage", + "contract": "Greenlistable", + "src": "contracts/access/Greenlistable.sol:21" + }, + { + "label": "sanctionsList", + "offset": 0, + "slot": "303", + "type": "t_address", + "contract": "WithSanctionsList", + "src": "contracts/abstract/WithSanctionsList.sol:18" + }, + { + "label": "__gap", + "offset": 0, + "slot": "304", + "type": "t_array(t_uint256)50_storage", + "contract": "WithSanctionsList", + "src": "contracts/abstract/WithSanctionsList.sol:23" + }, + { + "label": "currentRequestId", + "offset": 0, + "slot": "354", + "type": "t_struct(Counter)8079_storage", + "contract": "ManageableVault", + "src": "contracts/abstract/ManageableVault.sol:53" + }, + { + "label": "mToken", + "offset": 0, + "slot": "355", + "type": "t_contract(IMToken)20085", + "contract": "ManageableVault", + "src": "contracts/abstract/ManageableVault.sol:66" + }, + { + "label": "mTokenDataFeed", + "offset": 0, + "slot": "356", + "type": "t_contract(IDataFeed)19792", + "contract": "ManageableVault", + "src": "contracts/abstract/ManageableVault.sol:71" + }, + { + "label": "tokensReceiver", + "offset": 0, + "slot": "357", + "type": "t_address", + "contract": "ManageableVault", + "src": "contracts/abstract/ManageableVault.sol:76" + }, + { + "label": "instantFee", + "offset": 0, + "slot": "358", + "type": "t_uint256", + "contract": "ManageableVault", + "src": "contracts/abstract/ManageableVault.sol:81" + }, + { + "label": "instantDailyLimit", + "offset": 0, + "slot": "359", + "type": "t_uint256", + "contract": "ManageableVault", + "src": "contracts/abstract/ManageableVault.sol:88" + }, + { + "label": "dailyLimits", + "offset": 0, + "slot": "360", + "type": "t_mapping(t_uint256,t_uint256)", + "contract": "ManageableVault", + "src": "contracts/abstract/ManageableVault.sol:93" + }, + { + "label": "feeReceiver", + "offset": 0, + "slot": "361", + "type": "t_address", + "contract": "ManageableVault", + "src": "contracts/abstract/ManageableVault.sol:98" + }, + { + "label": "variationTolerance", + "offset": 0, + "slot": "362", + "type": "t_uint256", + "contract": "ManageableVault", + "src": "contracts/abstract/ManageableVault.sol:103" + }, + { + "label": "waivedFeeRestriction", + "offset": 0, + "slot": "363", + "type": "t_mapping(t_address,t_bool)", + "contract": "ManageableVault", + "src": "contracts/abstract/ManageableVault.sol:108" + }, + { + "label": "_paymentTokens", + "offset": 0, + "slot": "364", + "type": "t_struct(AddressSet)3891_storage", + "contract": "ManageableVault", + "src": "contracts/abstract/ManageableVault.sol:113" + }, + { + "label": "tokensConfig", + "offset": 0, + "slot": "366", + "type": "t_mapping(t_address,t_struct(TokenConfig)20098_storage)", + "contract": "ManageableVault", + "src": "contracts/abstract/ManageableVault.sol:118" + }, + { + "label": "minAmount", + "offset": 0, + "slot": "367", + "type": "t_uint256", + "contract": "ManageableVault", + "src": "contracts/abstract/ManageableVault.sol:123" + }, + { + "label": "isFreeFromMinAmount", + "offset": 0, + "slot": "368", + "type": "t_mapping(t_address,t_bool)", + "contract": "ManageableVault", + "src": "contracts/abstract/ManageableVault.sol:128" + }, + { + "label": "__gap", + "offset": 0, + "slot": "369", + "type": "t_array(t_uint256)50_storage", + "contract": "ManageableVault", + "src": "contracts/abstract/ManageableVault.sol:133" + }, + { + "label": "minFiatRedeemAmount", + "offset": 0, + "slot": "419", + "type": "t_uint256", + "contract": "RedemptionVault", + "src": "contracts/RedemptionVault.sol:69" + }, + { + "label": "fiatAdditionalFee", + "offset": 0, + "slot": "420", + "type": "t_uint256", + "contract": "RedemptionVault", + "src": "contracts/RedemptionVault.sol:74" + }, + { + "label": "fiatFlatFee", + "offset": 0, + "slot": "421", + "type": "t_uint256", + "contract": "RedemptionVault", + "src": "contracts/RedemptionVault.sol:79" + }, + { + "label": "redeemRequests", + "offset": 0, + "slot": "422", + "type": "t_mapping(t_uint256,t_struct(Request)20366_storage)", + "contract": "RedemptionVault", + "src": "contracts/RedemptionVault.sol:84" + }, + { + "label": "requestRedeemer", + "offset": 0, + "slot": "423", + "type": "t_address", + "contract": "RedemptionVault", + "src": "contracts/RedemptionVault.sol:89" + }, + { + "label": "__gap", + "offset": 0, + "slot": "424", + "type": "t_array(t_uint256)50_storage", + "contract": "RedemptionVault", + "src": "contracts/RedemptionVault.sol:94" + }, + { + "label": "___gap", + "offset": 0, + "slot": "474", + "type": "t_array(t_uint256)50_storage", + "contract": "RedemptionVaultWithSwapper", + "src": "contracts/RedemptionVaultWithSwapper.sol:34" + }, + { + "label": "mTbillRedemptionVault", + "offset": 0, + "slot": "524", + "type": "t_contract(IRedemptionVault)20603", + "contract": "RedemptionVaultWithSwapper", + "src": "contracts/RedemptionVaultWithSwapper.sol:41" + }, + { + "label": "liquidityProvider", + "offset": 0, + "slot": "525", + "type": "t_address", + "contract": "RedemptionVaultWithSwapper", + "src": "contracts/RedemptionVaultWithSwapper.sol:43" + }, + { + "label": "__gap", + "offset": 0, + "slot": "526", + "type": "t_array(t_uint256)50_storage", + "contract": "RedemptionVaultWithSwapper", + "src": "contracts/RedemptionVaultWithSwapper.sol:48" + }, + { + "label": "__gap", + "offset": 0, + "slot": "576", + "type": "t_array(t_uint256)50_storage", + "contract": "MGloRedemptionVaultWithSwapper", + "src": "contracts/products/mGLO/MGloRedemptionVaultWithSwapper.sol:19" + } + ], + "types": { + "t_address": { + "label": "address", + "numberOfBytes": "20" + }, + "t_array(t_bytes32)dyn_storage": { + "label": "bytes32[]", + "numberOfBytes": "32" + }, + "t_array(t_uint256)49_storage": { + "label": "uint256[49]", + "numberOfBytes": "1568" + }, + "t_array(t_uint256)50_storage": { + "label": "uint256[50]", + "numberOfBytes": "1600" + }, + "t_bool": { + "label": "bool", + "numberOfBytes": "1" + }, + "t_bytes32": { + "label": "bytes32", + "numberOfBytes": "32" + }, + "t_bytes4": { + "label": "bytes4", + "numberOfBytes": "4" + }, + "t_contract(IDataFeed)19792": { + "label": "contract IDataFeed", + "numberOfBytes": "20" + }, + "t_contract(IMToken)20085": { + "label": "contract IMToken", + "numberOfBytes": "20" + }, + "t_contract(IRedemptionVault)20603": { + "label": "contract IRedemptionVault", + "numberOfBytes": "20" + }, + "t_contract(MidasAccessControl)17280": { + "label": "contract MidasAccessControl", + "numberOfBytes": "20" + }, + "t_enum(RequestStatus)20102": { + "label": "enum RequestStatus", + "members": ["Pending", "Processed", "Canceled"], + "numberOfBytes": "1" + }, + "t_mapping(t_address,t_bool)": { + "label": "mapping(address => bool)", + "numberOfBytes": "32" + }, + "t_mapping(t_address,t_struct(TokenConfig)20098_storage)": { + "label": "mapping(address => struct TokenConfig)", + "numberOfBytes": "32" + }, + "t_mapping(t_bytes32,t_uint256)": { + "label": "mapping(bytes32 => uint256)", + "numberOfBytes": "32" + }, + "t_mapping(t_bytes4,t_bool)": { + "label": "mapping(bytes4 => bool)", + "numberOfBytes": "32" + }, + "t_mapping(t_uint256,t_struct(Request)20366_storage)": { + "label": "mapping(uint256 => struct Request)", + "numberOfBytes": "32" + }, + "t_mapping(t_uint256,t_uint256)": { + "label": "mapping(uint256 => uint256)", + "numberOfBytes": "32" + }, + "t_struct(AddressSet)3891_storage": { + "label": "struct EnumerableSetUpgradeable.AddressSet", + "members": [ + { + "label": "_inner", + "type": "t_struct(Set)3576_storage", + "offset": 0, + "slot": "0" + } + ], + "numberOfBytes": "64" + }, + "t_struct(Counter)8079_storage": { + "label": "struct Counters.Counter", + "members": [ + { + "label": "_value", + "type": "t_uint256", + "offset": 0, + "slot": "0" + } + ], + "numberOfBytes": "32" + }, + "t_struct(Request)20366_storage": { + "label": "struct Request", + "members": [ + { + "label": "sender", + "type": "t_address", + "offset": 0, + "slot": "0" + }, + { + "label": "tokenOut", + "type": "t_address", + "offset": 0, + "slot": "1" + }, + { + "label": "status", + "type": "t_enum(RequestStatus)20102", + "offset": 20, + "slot": "1" + }, + { + "label": "amountMToken", + "type": "t_uint256", + "offset": 0, + "slot": "2" + }, + { + "label": "mTokenRate", + "type": "t_uint256", + "offset": 0, + "slot": "3" + }, + { + "label": "tokenOutRate", + "type": "t_uint256", + "offset": 0, + "slot": "4" + } + ], + "numberOfBytes": "160" + }, + "t_struct(Set)3576_storage": { + "label": "struct EnumerableSetUpgradeable.Set", + "members": [ + { + "label": "_values", + "type": "t_array(t_bytes32)dyn_storage", + "offset": 0, + "slot": "0" + }, + { + "label": "_indexes", + "type": "t_mapping(t_bytes32,t_uint256)", + "offset": 0, + "slot": "1" + } + ], + "numberOfBytes": "64" + }, + "t_struct(TokenConfig)20098_storage": { + "label": "struct TokenConfig", + "members": [ + { + "label": "dataFeed", + "type": "t_address", + "offset": 0, + "slot": "0" + }, + { + "label": "fee", + "type": "t_uint256", + "offset": 0, + "slot": "1" + }, + { + "label": "allowance", + "type": "t_uint256", + "offset": 0, + "slot": "2" + }, + { + "label": "stable", + "type": "t_bool", + "offset": 0, + "slot": "3" + } + ], + "numberOfBytes": "128" + }, + "t_uint256": { + "label": "uint256", + "numberOfBytes": "32" + }, + "t_uint8": { + "label": "uint8", + "numberOfBytes": "1" + } + } + } } } } diff --git a/config/constants/addresses.ts b/config/constants/addresses.ts index 71762298..5e75eac5 100644 --- a/config/constants/addresses.ts +++ b/config/constants/addresses.ts @@ -1627,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', diff --git a/scripts/deploy/configs/mGLO.ts b/scripts/deploy/configs/mGLO.ts index 23848512..ecf61a85 100644 --- a/scripts/deploy/configs/mGLO.ts +++ b/scripts/deploy/configs/mGLO.ts @@ -265,5 +265,77 @@ export const mGLODeploymentConfig: DeploymentConfig = { }, }, }, + [chainIds.optimism]: { + dv: { + type: 'REGULAR', + enableSanctionsList: true, + feeReceiver: '0x6b5067C1D71e1Ad7e5Fbe85A8af04868B2e70a1B', + tokensReceiver: '0x83BfD9233DC281E7BA1311B1245cb2f891a94E56', + instantDailyLimit: parseUnits('30000000', 18), + instantFee: parseUnits('0', 2), + variationTolerance: parseUnits('2', 2), + minAmount: parseUnits('0', 18), + minMTokenAmountForFirstDeposit: parseUnits('0', 18), + maxSupplyCap: constants.MaxUint256, + }, + rvSwapper: { + type: 'SWAPPER', + feeReceiver: '0x83BfD9233DC281E7BA1311B1245cb2f891a94E56', + tokensReceiver: '0x83BfD9233DC281E7BA1311B1245cb2f891a94E56', + requestRedeemer: '0x27c41C320066e92688799b3cd0014992Da7f2f0C', + instantDailyLimit: parseUnits('200000', 18), + instantFee: parseUnits('0.5', 2), + variationTolerance: parseUnits('2', 2), + minAmount: parseUnits('1', 18), + fiatFlatFee: parseUnits('30', 18), + fiatAdditionalFee: parseUnits('0.1', 2), + minFiatRedeemAmount: parseUnits('1000', 18), + enableSanctionsList: true, + liquidityProvider: 'dummy', + swapperVault: 'dummy', + }, + postDeploy: { + addPaymentTokens: { + vaults: [ + { + paymentTokens: [ + { + token: 'usdc', + allowance: parseUnits('1000000000', 18), + isStable: true, + }, + ], + type: 'depositVault', + }, + { + paymentTokens: [ + { + token: 'usdc', + allowance: parseUnits('1000000000', 18), + isStable: true, + }, + ], + type: 'redemptionVaultSwapper', + }, + ], + }, + grantRoles: { + tokenManagerAddress: '0xA13f82F679E24ad08E014F8af6EcE32023b14F07', + vaultsManagerAddress: '0x2ACB4BdCbEf02f81BF713b696Ac26390d7f79A12', + oracleManagerAddress: '0x83b573AA8C4b567c0466c9d5e32D6513676d795b', + }, + greenlist: { + depositVault: true, + redemptionVaultSwapper: true, + }, + pauseFunctions: { + depositVault: ['depositRequest', 'depositRequestWithCustomRecipient'], + redemptionVaultSwapper: ['redeemFiatRequest'], + }, + setRoundData: { + data: parseUnits('1', 8), + }, + }, + }, }, };