Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
},
"raw": {
"0xae05cd22df81871bc7cc2a04becfb516bfe332c8": {
"label": null,
"label": "AaveV3EthereumHorizon.POOL",
"contract": "lib/aave-umbrella/lib/aave-v3-origin/lib/solidity-utils/lib/openzeppelin-contracts-upgradeable/lib/openzeppelin-contracts/contracts/proxy/transparent/TransparentUpgradeableProxy.sol:TransparentUpgradeableProxy",
"balanceDiff": null,
"nonceDiff": null,
Expand Down

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import {EthereumScript} from 'solidity-utils/contracts/utils/ScriptUtils.sol';
import {AaveV3Horizon_mGLOBALListing_20260616} from './AaveV3Horizon_mGLOBALListing_20260616.sol';

/**
* @dev Deploy Horizon mGLOBAL Listing payload.
* deploy-command: make deploy-payload
*/
contract DeployEthereum is EthereumScript {
function run() external broadcast {
new AaveV3Horizon_mGLOBALListing_20260616();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import {AaveV3EthereumHorizonCustom} from 'src/utils/AaveV3EthereumHorizonCustom.sol';
import {AaveV3EthereumHorizonAssets} from 'aave-address-book-latest/AaveV3EthereumHorizon.sol';
import {AaveV3PayloadHorizonEthereum} from 'src/utils/AaveV3PayloadHorizonEthereum.sol';
import {IAaveV3ConfigEngine as IEngine} from 'aave-v3-origin/contracts/extensions/v3-config-engine/IAaveV3ConfigEngine.sol';
import {EngineFlags} from 'aave-v3-origin/contracts/extensions/v3-config-engine/EngineFlags.sol';

/**
* @title Horizon Listing — mGLOBAL
* @author Aave Labs
Comment thread
DhairyaSethi marked this conversation as resolved.
* @dev Lists mGLOBAL (Midas Global Diversified Alternative Debt Fund) as an RWA collateral asset
* on the Horizon pool. The default reserve config uses near-zero LTV/LT so that borrowing
* against mGLOBAL outside eMode is negligible; an mGLOBAL/stablecoins eMode (USDC, RLUSD)
* provides the intended collateral parameters.
*/
contract AaveV3Horizon_mGLOBALListing_20260616 is AaveV3PayloadHorizonEthereum {
address public constant MGLOBAL = AaveV3EthereumHorizonCustom.MGLOBAL_UNDERLYING;
address public constant MGLOBAL_PRICE_FEED = AaveV3EthereumHorizonCustom.MGLOBAL_PRICE_FEED;
uint8 public constant MGLOBAL_EMODE_CATEGORY = 5;

function newListingsCustom()
public
pure
override
returns (IEngine.ListingWithCustomImpl[] memory)
{
IEngine.ListingWithCustomImpl[] memory listingsCustom = new IEngine.ListingWithCustomImpl[](1);

listingsCustom[0] = IEngine.ListingWithCustomImpl(
IEngine.Listing({
asset: MGLOBAL,
assetSymbol: 'mGLOBAL',
priceFeed: MGLOBAL_PRICE_FEED,
rateStrategyParams: AaveV3EthereumHorizonCustom.defaultRwaInterestRateInputData(),
enabledToBorrow: EngineFlags.DISABLED,
borrowableInIsolation: EngineFlags.DISABLED,
withSiloedBorrowing: EngineFlags.DISABLED,
flashloanable: EngineFlags.DISABLED,
ltv: 5,
liqThreshold: 10,
liqBonus: 6_00,
reserveFactor: EngineFlags.KEEP_CURRENT,
supplyCap: 50_000_000,
borrowCap: 0,
debtCeiling: 0,
liqProtocolFee: 0
Comment thread
miguelmtzinf marked this conversation as resolved.
}),
IEngine.TokenImplementations({
aToken: AaveV3EthereumHorizonCustom.RWA_A_TOKEN_IMPL,
vToken: AaveV3EthereumHorizonCustom.DEFAULT_VARIABLE_DEBT_TOKEN_IMPL
})
);

return listingsCustom;
}

function assetsEModeUpdates() public pure override returns (IEngine.AssetEModeUpdate[] memory) {
IEngine.AssetEModeUpdate[] memory assetsEMode = new IEngine.AssetEModeUpdate[](3);

// mGLOBAL as collateral in the stablecoins eMode
assetsEMode[0] = IEngine.AssetEModeUpdate({
asset: MGLOBAL,
eModeCategory: MGLOBAL_EMODE_CATEGORY,
collateral: EngineFlags.ENABLED,
borrowable: EngineFlags.DISABLED
});

// USDC as borrowable in the stablecoins eMode
assetsEMode[1] = IEngine.AssetEModeUpdate({
asset: AaveV3EthereumHorizonAssets.USDC_UNDERLYING,
eModeCategory: MGLOBAL_EMODE_CATEGORY,
collateral: EngineFlags.DISABLED,
borrowable: EngineFlags.ENABLED
});

// RLUSD as borrowable in the stablecoins eMode
assetsEMode[2] = IEngine.AssetEModeUpdate({
asset: AaveV3EthereumHorizonAssets.RLUSD_UNDERLYING,
eModeCategory: MGLOBAL_EMODE_CATEGORY,
collateral: EngineFlags.DISABLED,
borrowable: EngineFlags.ENABLED
});

return assetsEMode;
}

function eModeCategoriesUpdates()
public
pure
override
returns (IEngine.EModeCategoryUpdate[] memory)
{
IEngine.EModeCategoryUpdate[] memory eModeCategories = new IEngine.EModeCategoryUpdate[](1);

// mGLOBAL Stablecoins
eModeCategories[0] = IEngine.EModeCategoryUpdate({
eModeCategory: MGLOBAL_EMODE_CATEGORY,
ltv: 75_00,
liqThreshold: 80_00,
liqBonus: 6_00,
Comment thread
miguelmtzinf marked this conversation as resolved.
label: 'mGLOBAL Stablecoins'
});

return eModeCategories;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import {IPool} from 'aave-v3-origin/contracts/interfaces/IPool.sol';
import {IERC20} from 'aave-v3-origin/contracts/dependencies/openzeppelin/contracts/IERC20.sol';
import {IDefaultInterestRateStrategyV2} from 'aave-v3-origin/contracts/interfaces/IDefaultInterestRateStrategyV2.sol';
import {Errors} from 'src/dependencies/Errors.sol';
import {ProtocolV3HorizonTestBase, ReserveConfig} from 'tests/utils/ProtocolV3HorizonTestBase.sol';
import {HorizonConfigAssertionHelper} from 'tests/utils/HorizonConfigAssertionHelper.sol';
import {AaveV3Horizon_mGLOBALListing_20260616} from 'src/AaveV3Horizon_mGLOBALListing_20260616/AaveV3Horizon_mGLOBALListing_20260616.sol';
import {AaveV3EthereumHorizonCustom} from 'src/utils/AaveV3EthereumHorizonCustom.sol';
import {AaveV3EthereumHorizonAssets} from 'aave-address-book-latest/AaveV3EthereumHorizon.sol';

abstract contract AaveV3Horizon_mGLOBALListing_20260616_TestBase is ProtocolV3HorizonTestBase {
AaveV3Horizon_mGLOBALListing_20260616 internal proposal;

ExpectedAssetConfig internal expectedAssetConfig;
ExpectedEModeConfig internal expectedEModeConfig;

function setUp() public virtual {
_setExpectedConfig();
}

function _setExpectedConfig() internal virtual override {
expectedAssetConfig = ExpectedAssetConfig({
underlying: AaveV3EthereumHorizonCustom.MGLOBAL_UNDERLYING,
isRwa: true,
oracle: AaveV3EthereumHorizonCustom.MGLOBAL_PRICE_FEED,
aTokenName: 'Aave Horizon RWA mGLOBAL',
aTokenSymbol: 'aHorRwamGLOBAL',
variableDebtTokenName: 'Aave Horizon RWA Variable Debt mGLOBAL',
variableDebtTokenSymbol: 'variableDebtHorRwamGLOBAL',
supplyCap: 50_000_000,
borrowCap: 0,
reserveFactor: 0,
borrowingEnabled: false,
flashloanable: false,
ltv: 5,
liquidationThreshold: 10,
liquidationBonus: 100_00 + 6_00,
debtCeiling: 0,
liqProtocolFee: 0,
rateData: IDefaultInterestRateStrategyV2.InterestRateData({
optimalUsageRatio: 99_00,
baseVariableBorrowRate: 0,
variableRateSlope1: 0,
variableRateSlope2: 0
})
});
expectedEModeConfig = ExpectedEModeConfig({
eModeCategory: 5,
ltv: 75_00,
liquidationThreshold: 80_00,
liquidationBonus: 100_00 + 6_00,
label: 'mGLOBAL Stablecoins',
collateralAssets: _toAddressArray(AaveV3EthereumHorizonCustom.MGLOBAL_UNDERLYING),
borrowableAssets: _toAddressArray(
AaveV3EthereumHorizonAssets.USDC_UNDERLYING,
AaveV3EthereumHorizonAssets.RLUSD_UNDERLYING
)
});
}
}

/**
* @dev Test for Horizon mGLOBAL listing (pre-execution).
* command: FOUNDRY_PROFILE=test forge test --match-contract AaveV3Horizon_mGLOBALListing_20260616_Test -vv
*/
contract AaveV3Horizon_mGLOBALListing_20260616_Test is

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Why is the abstract then test contract split needed here ?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

bc below we also have a fork test for post tx execution, follows same pattern we used for other payloads here as well

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

we should switch to permissioned payloads controller model asap, this separate test is v stupid lol and ppc absorbs most footguns w current flow

AaveV3Horizon_mGLOBALListing_20260616_TestBase
{
function setUp() public virtual override {
super.setUp();
vm.createSelectFork(vm.rpcUrl('mainnet'), 25352313);
proposal = new AaveV3Horizon_mGLOBALListing_20260616();
}

/**
* @dev executes the generic test suite including e2e and config snapshots
*/
function _executeMGLOBALListing() internal {
_executeHorizonPayload(address(proposal));
}

function test_defaultProposalExecution() public virtual {
defaultTest_v3_3('AaveV3Horizon_mGLOBALListing_20260616', _pool(), _executeMGLOBALListing);
}

/**
* @dev verifies the exact config values set by the mGLOBAL listing payload
*/
function test_mglobalConfig() public virtual {
IPool pool = _pool();
uint8 eModeCategory = proposal.MGLOBAL_EMODE_CATEGORY();

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

can you also test this is the _nextEmodeCategory() bc 5 is hardcoded right now

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

i would rather fetch it dynamically in the proposal tbh but nbd

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

it's not I think, 5 is a deprecate emode

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

i confirm we must use 5 to replace existing emode that was deprecated in a previous gov action


// the eMode category must be unused before execution (no collateral/borrowable assets, no label)
assertEq(pool.getEModeCategoryCollateralBitmap(eModeCategory), 0, 'emode collateral not empty');
assertEq(pool.getEModeCategoryBorrowableBitmap(eModeCategory), 0, 'emode borrowable not empty');
assertEq(bytes(pool.getEModeCategoryLabel(eModeCategory)).length, 0, 'emode label not empty');

// execute payload
_executeHorizonPayload(address(proposal));

// verify mGLOBAL asset config
_assertAssetConfig(pool, expectedAssetConfig);

// verify mGLOBAL Stablecoins eMode
_assertEModeConfig(pool, expectedEModeConfig);
}

/**
* @dev mGLOBAL's near-zero default params must prevent borrowing a meaningful amount of a
* stablecoin against it outside of the eMode.
*/
function test_mglobalDefaultModeBorrowReverts() public {
IPool pool = _pool();
_executeHorizonPayload(address(proposal));

_initTestActors();
_whitelistRwaUsers(_testActorsArray());
_whitelistRwaPool(pool);

address user = regularCollateralSupplier;
address mGlobal = AaveV3EthereumHorizonCustom.MGLOBAL_UNDERLYING;
uint256 supplyAmount = 100_000e18;

// supply mGLOBAL as collateral in the default eMode (category 0)
deal(mGlobal, user, supplyAmount);
vm.startPrank(user);
IERC20(mGlobal).approve(address(pool), supplyAmount);
pool.supply(mGlobal, supplyAmount, user, 0);

// borrowing a stablecoin against it in default mode must revert (near-zero default LTV)
vm.expectRevert(bytes(Errors.COLLATERAL_CANNOT_COVER_NEW_BORROW));
pool.borrow(AaveV3EthereumHorizonAssets.USDC_UNDERLYING, 10_000e6, 2, 0, user);

vm.expectRevert(bytes(Errors.COLLATERAL_CANNOT_COVER_NEW_BORROW));
pool.borrow(AaveV3EthereumHorizonAssets.GHO_UNDERLYING, 10_000e18, 2, 0, user);
vm.stopPrank();
}
}

/**
* @dev Post-execution fork test. Run after the payload has been executed on mainnet
* to validate the live state matches expected config and run full E2E.
* command: FOUNDRY_PROFILE=test forge test --match-contract AaveV3Horizon_mGLOBALListing_20260616_PostExecution_Test -vv
*/
contract AaveV3Horizon_mGLOBALListing_20260616_PostExecution_Test is
AaveV3Horizon_mGLOBALListing_20260616_TestBase
{
function setUp() public virtual override {
super.setUp();
vm.createSelectFork(vm.rpcUrl('mainnet'), 25380179);
}

function test_defaultProposalExecution() public {
defaultTest_v3_3_postExecution(_pool());
}

function test_mglobalConfig() public {
_assertAssetConfig(_pool(), expectedAssetConfig);
_assertEModeConfig(_pool(), expectedEModeConfig);
}
}
6 changes: 6 additions & 0 deletions src/utils/AaveV3EthereumHorizonCustom.sol
Original file line number Diff line number Diff line change
Expand Up @@ -54,4 +54,10 @@ library AaveV3EthereumHorizonCustom {
address public constant ACRED_UNDERLYING = 0x17418038ecF73BA4026c4f428547BF099706F27B;
// https://etherscan.io/address/0x60AEd7d20AC6328f7BA771aD58931c996aff30E8
address public constant ACRED_PRICE_FEED = 0x60AEd7d20AC6328f7BA771aD58931c996aff30E8;

// https://etherscan.io/address/0x7433806912Eae67919e66aea853d46Fa0aef98A8
address public constant MGLOBAL_UNDERLYING = 0x7433806912Eae67919e66aea853d46Fa0aef98A8;
// TODO: replace with the LlamaGuard-bounded NAV feed once deployed; this is the unbounded Chainlink feed
// https://etherscan.io/address/0xB92A68763b2F83e094595c7B41a7FB9D0f8Da193
address public constant MGLOBAL_PRICE_FEED = 0xB92A68763b2F83e094595c7B41a7FB9D0f8Da193;

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

TODO

}
3 changes: 3 additions & 0 deletions tests/utils/HorizonConfigAssertionHelper.sol
Original file line number Diff line number Diff line change
Expand Up @@ -406,6 +406,9 @@ abstract contract HorizonConfigAssertionHelper is Test {
if (underlying == AaveV3EthereumHorizonCustom.ACRED_UNDERLYING) {
return AaveV3EthereumHorizonCustom.ACRED_PRICE_FEED;
}
if (underlying == AaveV3EthereumHorizonCustom.MGLOBAL_UNDERLYING) {
return AaveV3EthereumHorizonCustom.MGLOBAL_PRICE_FEED;
}
revert('_expectedPriceFeed: unknown underlying');
}

Expand Down
34 changes: 34 additions & 0 deletions tests/utils/HorizonRwaWhitelistHelper.sol
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {AaveV3EthereumHorizonAssets} from 'aave-address-book-latest/AaveV3Ethere
* - Centrifuge (JTRSY, JAAA) — RestrictionManager.endorse
* - Circle/USYC — RolesAuthority.setUserRole
* - Securitize (VBILL, ACRED) — RegistryService.addWallet
* - Midas (mGLOBAL) — MidasAccessControl GREENLISTED_ROLE
*
* Override `_whitelistRwaActors` in concrete tests to add whitelisting for
* newly listed assets.
Expand All @@ -37,6 +38,14 @@ abstract contract HorizonRwaWhitelistHelper is Test {
string internal constant VBILL_SECURITIZE_FUND_ID = 'f27e20ca73314651b387da0aa9116f30';
string internal constant ACRED_SECURITIZE_FUND_ID = '69023a78d57776eca9542d33';

// ── Midas (mGLOBAL) ──────────────────────────────────────────────────
// Shared Midas access control (mGLOBAL.accessControl()). Transfers require the
// sender and recipient to hold mGLOBAL's product-specific greenlist role.
address internal constant MIDAS_ACCESS_CONTROL = 0x0312A9D1Ff2372DDEdCBB21e4B6389aFc919aC4B;
bytes32 internal constant MGLOBAL_GREENLISTED_ROLE = keccak256('M_GLOBAL_GREENLISTED_ROLE');
// Slot of the OZ `_roles` mapping (role => account => bool) in MidasAccessControl.
uint256 internal constant MIDAS_ROLES_SLOT = 101;

// ── Whale addresses for tokens incompatible with foundry `deal` ────
// Securitize DS-protocol tokens store balances in an external data store,
// so cannot use native foundry deal. Transfer from a real holder instead.
Expand All @@ -61,6 +70,8 @@ abstract contract HorizonRwaWhitelistHelper is Test {
_whitelistVbillRwa(actors[i]);
// Securitize (ACRED) — only if listed (aToken exists)
_whitelistAcredRwa(actors[i]);
// Midas (mGLOBAL)
_whitelistMidasRwa(actors[i]);
}
}

Expand All @@ -79,6 +90,8 @@ abstract contract HorizonRwaWhitelistHelper is Test {
_whitelistVbillRwa(_requireAToken(pool, AaveV3EthereumHorizonAssets.VBILL_UNDERLYING));
// Securitize (ACRED)
_whitelistAcredRwa(_requireAToken(pool, AaveV3EthereumHorizonCustom.ACRED_UNDERLYING));
// Midas (mGLOBAL)
_whitelistMidasRwa(_requireAToken(pool, AaveV3EthereumHorizonCustom.MGLOBAL_UNDERLYING));
}

function _requireAToken(IPool pool, address underlying) internal view returns (address aToken) {
Expand Down Expand Up @@ -176,6 +189,27 @@ abstract contract HorizonRwaWhitelistHelper is Test {
);
}

/// @dev Midas whitelisting: grants mGLOBAL's greenlist role on the shared MidasAccessControl.
function _whitelistMidasRwa(address addressToWhitelist) internal {
bytes32 slot = keccak256(
abi.encode(
addressToWhitelist,
keccak256(abi.encode(MGLOBAL_GREENLISTED_ROLE, MIDAS_ROLES_SLOT))
)
);
vm.store(MIDAS_ACCESS_CONTROL, slot, bytes32(uint256(1)));

// Verify whitelisted
(bool success, bytes memory data) = MIDAS_ACCESS_CONTROL.call(
abi.encodeWithSignature(
'hasRole(bytes32,address)',
MGLOBAL_GREENLISTED_ROLE,
addressToWhitelist
)
);
require(success && abi.decode(data, (bool)), 'Midas: address not greenlisted');
Comment on lines +203 to +210

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

why not do a high level call with a casted interface if we're reverting on failure anyway, no need to do .call here

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

This is how we do it in this entire file. The reason is becase I'd thought it's cleaner like this than defining specific interfaces for each issuer, wdyt

@DhairyaSethi DhairyaSethi Jun 22, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

hasRole is in IAccessManager from oz, nothing custom needed here

in general it's totally fine to add vender specific interfaces, proposals repo (v3) has always done that in src/interfaces

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

i would prefer to add those interfaces actually, trimmed from source with their natspec over this in general also

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

wdyt

}

/**
* @dev Generic Securitize whitelisting via RegistryService.addWallet.
* Reuse for any Securitize DS-protocol token (VBILL, ACRED, etc.) by supplying
Expand Down
Loading
Loading