diff --git a/foundry.toml b/foundry.toml index 4fdd4cf5b..7a4ff471b 100644 --- a/foundry.toml +++ b/foundry.toml @@ -14,13 +14,13 @@ gas_limit = 1099511627776 dynamic_test_linking = true additional_compiler_profiles = [ - { name = "hub", optimizer = true, via_ir = true, optimizer_runs = 22_300 }, + { name = "hub", optimizer = true, via_ir = true, optimizer_runs = 21_200 }, { name = "spoke", optimizer = true, via_ir = true, optimizer_runs = 750 }, { name = "tests", optimizer = true, via_ir = false, optimizer_runs = 444444444444 }, ] compilation_restrictions = [ - { paths = "src/hub/Hub.sol", optimizer = true, via_ir = true, optimizer_runs = 22_300 }, + { paths = "src/hub/Hub.sol", optimizer = true, via_ir = true, optimizer_runs = 21_200 }, { paths = "src/spoke/instances/SpokeInstance.sol", optimizer = true, via_ir = true, optimizer_runs = 750 }, { paths = "tests/**", optimizer = true, via_ir = false, optimizer_runs = 444444444444 }, ] diff --git a/snapshots/Hub.Operations.json b/snapshots/Hub.Operations.json index 806923a0e..034680f78 100644 --- a/snapshots/Hub.Operations.json +++ b/snapshots/Hub.Operations.json @@ -1,18 +1,17 @@ { - "add": "86703", - "add: with transfer": "108000", - "draw": "104159", - "eliminateDeficit: full": "72578", - "eliminateDeficit: partial": "82183", - "mintFeeShares": "82752", - "payFee": "70816", - "refreshPremium": "70373", - "remove: full": "75607", - "remove: partial": "80745", - "reportDeficit": "111893", - "restore: full": "76563", - "restore: full - with transfer": "169172", - "restore: partial": "85273", - "restore: partial - with transfer": "143253", - "transferShares": "69630" + "add": "86399", + "add: with transfer": "107696", + "draw": "103937", + "eliminateDeficit: full": "72238", + "eliminateDeficit: partial": "81843", + "payFee": "73729", + "refreshPremium": "69963", + "remove: full": "75134", + "remove: partial": "80405", + "reportDeficit": "121927", + "restore: full": "89397", + "restore: full - with transfer": "182373", + "restore: partial": "98107", + "restore: partial - with transfer": "156087", + "transferShares": "79343" } \ No newline at end of file diff --git a/snapshots/NativeTokenGateway.Operations.json b/snapshots/NativeTokenGateway.Operations.json index d813bf259..9e4feb4ea 100644 --- a/snapshots/NativeTokenGateway.Operations.json +++ b/snapshots/NativeTokenGateway.Operations.json @@ -1,8 +1,8 @@ { - "borrowNative": "228656", - "repayNative": "166471", - "supplyAsCollateralNative": "160133", - "supplyNative": "135762", - "withdrawNative: full": "125557", - "withdrawNative: partial": "136746" + "borrowNative": "228379", + "repayNative": "166556", + "supplyAsCollateralNative": "159829", + "supplyNative": "135519", + "withdrawNative: full": "125324", + "withdrawNative: partial": "136454" } \ No newline at end of file diff --git a/snapshots/SignatureGateway.Operations.json b/snapshots/SignatureGateway.Operations.json index 2a210113e..04ae1ce95 100644 --- a/snapshots/SignatureGateway.Operations.json +++ b/snapshots/SignatureGateway.Operations.json @@ -1,10 +1,10 @@ { - "borrowWithSig": "213889", - "repayWithSig": "186743", + "borrowWithSig": "213612", + "repayWithSig": "186828", "setSelfAsUserPositionManagerWithSig": "75385", "setUsingAsCollateralWithSig": "85387", - "supplyWithSig": "151994", + "supplyWithSig": "151751", "updateUserDynamicConfigWithSig": "63120", "updateUserRiskPremiumWithSig": "62090", - "withdrawWithSig": "130812" + "withdrawWithSig": "130578" } \ No newline at end of file diff --git a/snapshots/Spoke.Getters.json b/snapshots/Spoke.Getters.json index 6950235b1..d46c1293b 100644 --- a/snapshots/Spoke.Getters.json +++ b/snapshots/Spoke.Getters.json @@ -1,7 +1,7 @@ { "getUserAccountData: supplies: 0, borrows: 0": "13014", - "getUserAccountData: supplies: 1, borrows: 0": "49426", - "getUserAccountData: supplies: 2, borrows: 0": "81102", - "getUserAccountData: supplies: 2, borrows: 1": "101531", - "getUserAccountData: supplies: 2, borrows: 2": "120791" + "getUserAccountData: supplies: 1, borrows: 0": "49474", + "getUserAccountData: supplies: 2, borrows: 0": "81198", + "getUserAccountData: supplies: 2, borrows: 1": "101714", + "getUserAccountData: supplies: 2, borrows: 2": "121061" } \ No newline at end of file diff --git a/snapshots/Spoke.Operations.ZeroRiskPremium.json b/snapshots/Spoke.Operations.ZeroRiskPremium.json index 63ef7a407..66be21a1c 100644 --- a/snapshots/Spoke.Operations.ZeroRiskPremium.json +++ b/snapshots/Spoke.Operations.ZeroRiskPremium.json @@ -1,34 +1,34 @@ { - "borrow: first": "190384", - "borrow: second action, same reserve": "170250", - "liquidationCall (receiveShares): full": "303261", - "liquidationCall (receiveShares): partial": "302679", - "liquidationCall (reportDeficit): full": "366383", - "liquidationCall: full": "320897", - "liquidationCall: partial": "320315", - "permitReserve + repay (multicall)": "164576", - "permitReserve + supply (multicall)": "146756", - "permitReserve + supply + enable collateral (multicall)": "161207", - "repay: full": "123914", - "repay: partial": "128872", + "borrow: first": "203353", + "borrow: second action, same reserve": "183219", + "liquidationCall (receiveShares): full": "316089", + "liquidationCall (receiveShares): partial": "315507", + "liquidationCall (reportDeficit): full": "378782", + "liquidationCall: full": "333433", + "liquidationCall: partial": "332851", + "permitReserve + repay (multicall)": "164574", + "permitReserve + supply (multicall)": "146452", + "permitReserve + supply + enable collateral (multicall)": "160903", + "repay: full": "123912", + "repay: partial": "128870", "setUserPositionManagersWithSig: disable": "47039", "setUserPositionManagersWithSig: enable": "68951", - "supply + enable collateral (multicall)": "141409", - "supply: 0 borrows, collateral disabled": "122846", - "supply: 0 borrows, collateral enabled": "105817", - "supply: second action, same reserve": "105746", + "supply + enable collateral (multicall)": "141105", + "supply: 0 borrows, collateral disabled": "122542", + "supply: 0 borrows, collateral enabled": "105513", + "supply: second action, same reserve": "105442", "updateUserDynamicConfig: 1 collateral": "74545", "updateUserDynamicConfig: 2 collaterals": "89413", - "updateUserRiskPremium: 1 borrow": "95734", - "updateUserRiskPremium: 2 borrows": "105414", + "updateUserRiskPremium: 1 borrow": "95869", + "updateUserRiskPremium: 2 borrows": "106280", "usingAsCollateral: 0 borrows, enable": "59616", - "usingAsCollateral: 1 borrow, disable": "105778", + "usingAsCollateral: 1 borrow, disable": "105913", "usingAsCollateral: 1 borrow, enable": "42504", - "usingAsCollateral: 2 borrows, disable": "127327", + "usingAsCollateral: 2 borrows, disable": "127549", "usingAsCollateral: 2 borrows, enable": "42516", - "withdraw: 0 borrows, full": "127955", - "withdraw: 0 borrows, partial": "132851", - "withdraw: 1 borrow, partial": "159982", - "withdraw: 2 borrows, partial": "174540", - "withdraw: non collateral": "105902" + "withdraw: 0 borrows, full": "127530", + "withdraw: 0 borrows, partial": "132607", + "withdraw: 1 borrow, partial": "159692", + "withdraw: 2 borrows, partial": "174470", + "withdraw: non collateral": "105610" } \ No newline at end of file diff --git a/snapshots/Spoke.Operations.json b/snapshots/Spoke.Operations.json index 208dc6489..628706a8e 100644 --- a/snapshots/Spoke.Operations.json +++ b/snapshots/Spoke.Operations.json @@ -1,34 +1,34 @@ { - "borrow: first": "259319", - "borrow: second action, same reserve": "202185", - "liquidationCall (receiveShares): full": "335305", - "liquidationCall (receiveShares): partial": "334723", - "liquidationCall (reportDeficit): full": "361000", - "liquidationCall: full": "352941", - "liquidationCall: partial": "352359", - "permitReserve + repay (multicall)": "162044", - "permitReserve + supply (multicall)": "146756", - "permitReserve + supply + enable collateral (multicall)": "161207", - "repay: full": "117993", - "repay: partial": "137351", + "borrow: first": "271965", + "borrow: second action, same reserve": "214831", + "liquidationCall (receiveShares): full": "347810", + "liquidationCall (receiveShares): partial": "347228", + "liquidationCall (reportDeficit): full": "373399", + "liquidationCall: full": "365154", + "liquidationCall: partial": "364572", + "permitReserve + repay (multicall)": "162043", + "permitReserve + supply (multicall)": "146452", + "permitReserve + supply + enable collateral (multicall)": "160903", + "repay: full": "117991", + "repay: partial": "137349", "setUserPositionManagersWithSig: disable": "47039", "setUserPositionManagersWithSig: enable": "68951", - "supply + enable collateral (multicall)": "141409", - "supply: 0 borrows, collateral disabled": "122846", - "supply: 0 borrows, collateral enabled": "105817", - "supply: second action, same reserve": "105746", + "supply + enable collateral (multicall)": "141105", + "supply: 0 borrows, collateral disabled": "122542", + "supply: 0 borrows, collateral enabled": "105513", + "supply: second action, same reserve": "105442", "updateUserDynamicConfig: 1 collateral": "74545", "updateUserDynamicConfig: 2 collaterals": "89413", - "updateUserRiskPremium: 1 borrow": "149093", - "updateUserRiskPremium: 2 borrows": "199355", + "updateUserRiskPremium: 1 borrow": "159028", + "updateUserRiskPremium: 2 borrows": "219821", "usingAsCollateral: 0 borrows, enable": "59616", - "usingAsCollateral: 1 borrow, disable": "159134", + "usingAsCollateral: 1 borrow, disable": "169069", "usingAsCollateral: 1 borrow, enable": "42504", - "usingAsCollateral: 2 borrows, disable": "229264", + "usingAsCollateral: 2 borrows, disable": "249086", "usingAsCollateral: 2 borrows, enable": "42516", - "withdraw: 0 borrows, full": "127955", - "withdraw: 0 borrows, partial": "132851", - "withdraw: 1 borrow, partial": "210836", - "withdraw: 2 borrows, partial": "257012", - "withdraw: non collateral": "105902" + "withdraw: 0 borrows, full": "127530", + "withdraw: 0 borrows, partial": "132607", + "withdraw: 1 borrow, partial": "220346", + "withdraw: 2 borrows, partial": "256296", + "withdraw: non collateral": "105610" } \ No newline at end of file diff --git a/src/hub/Hub.sol b/src/hub/Hub.sol index c77d1a76c..136857ad5 100644 --- a/src/hub/Hub.sol +++ b/src/hub/Hub.sol @@ -98,6 +98,7 @@ contract Hub is IHub, AccessManaged { liquidity: 0, deficitRay: 0, swept: 0, + realizedFees: 0, addedShares: 0, drawnShares: 0, premiumShares: 0, @@ -108,7 +109,6 @@ contract Hub is IHub, AccessManaged { decimals: decimals, drawnRate: drawnRate.toUint96(), irStrategy: irStrategy, - realizedFees: 0, reinvestmentController: address(0), feeReceiver: feeReceiver, liquidityFee: 0 @@ -125,7 +125,7 @@ contract Hub is IHub, AccessManaged { reinvestmentController: address(0) }) ); - emit UpdateAsset(assetId, drawnIndex, drawnRate, 0); + emit UpdateAsset(assetId, drawnIndex, drawnRate); return assetId; } @@ -138,7 +138,7 @@ contract Hub is IHub, AccessManaged { ) external restricted { require(assetId < _assetCount, AssetNotListed()); Asset storage asset = _assets[assetId]; - asset.accrue(); + asset.accrue(_spokes, assetId); require(config.liquidityFee <= PercentageMath.PERCENTAGE_FACTOR, InvalidLiquidityFee()); require(config.feeReceiver != address(0) && config.irStrategy != address(0), InvalidAddress()); @@ -152,7 +152,6 @@ contract Hub is IHub, AccessManaged { address oldFeeReceiver = asset.feeReceiver; if (oldFeeReceiver != config.feeReceiver) { - _mintFeeShares(asset, assetId); IHub.SpokeConfig memory spokeConfig; spokeConfig.active = _spokes[assetId][oldFeeReceiver].active; spokeConfig.halted = _spokes[assetId][oldFeeReceiver].halted; @@ -200,27 +199,17 @@ contract Hub is IHub, AccessManaged { function setInterestRateData(uint256 assetId, bytes calldata irData) external restricted { require(assetId < _assetCount, AssetNotListed()); Asset storage asset = _assets[assetId]; - asset.accrue(); + asset.accrue(_spokes, assetId); IBasicInterestRateStrategy(asset.irStrategy).setInterestRateData(assetId, irData); asset.updateDrawnRate(assetId); } - /// @inheritdoc IHub - function mintFeeShares(uint256 assetId) external restricted returns (uint256) { - require(assetId < _assetCount, AssetNotListed()); - Asset storage asset = _assets[assetId]; - asset.accrue(); - uint256 feeShares = _mintFeeShares(asset, assetId); - asset.updateDrawnRate(assetId); - return feeShares; - } - /// @inheritdoc IHubBase function add(uint256 assetId, uint256 amount) external returns (uint256) { Asset storage asset = _assets[assetId]; SpokeData storage spoke = _spokes[assetId][msg.sender]; - asset.accrue(); + asset.accrue(_spokes, assetId); _validateAdd(asset, spoke, amount); uint256 liquidity = asset.liquidity + amount; @@ -244,7 +233,7 @@ contract Hub is IHub, AccessManaged { Asset storage asset = _assets[assetId]; SpokeData storage spoke = _spokes[assetId][msg.sender]; - asset.accrue(); + asset.accrue(_spokes, assetId); _validateRemove(spoke, amount, to); uint256 liquidity = asset.liquidity; @@ -269,7 +258,7 @@ contract Hub is IHub, AccessManaged { Asset storage asset = _assets[assetId]; SpokeData storage spoke = _spokes[assetId][msg.sender]; - asset.accrue(); + asset.accrue(_spokes, assetId); _validateDraw(asset, spoke, amount, to); uint256 liquidity = asset.liquidity; @@ -298,7 +287,7 @@ contract Hub is IHub, AccessManaged { Asset storage asset = _assets[assetId]; SpokeData storage spoke = _spokes[assetId][msg.sender]; - asset.accrue(); + asset.accrue(_spokes, assetId); _validateRestore(asset, spoke, drawnAmount, premiumDelta.restoredPremiumRay); uint120 drawnShares = asset.toDrawnSharesDown(drawnAmount).toUint120(); @@ -328,7 +317,7 @@ contract Hub is IHub, AccessManaged { Asset storage asset = _assets[assetId]; SpokeData storage spoke = _spokes[assetId][msg.sender]; - asset.accrue(); + asset.accrue(_spokes, assetId); _validateReportDeficit(asset, spoke, drawnAmount, premiumDelta.restoredPremiumRay); uint120 drawnShares = asset.toDrawnSharesDown(drawnAmount).toUint120(); @@ -358,7 +347,7 @@ contract Hub is IHub, AccessManaged { SpokeData storage callerSpoke = _spokes[assetId][msg.sender]; SpokeData storage coveredSpoke = _spokes[assetId][spoke]; - asset.accrue(); + asset.accrue(_spokes, assetId); uint256 deficitRay = coveredSpoke.deficitRay; uint256 deficitAmountRay = (amount < deficitRay.fromRayUp()) ? amount.toRay() : deficitRay; _validateEliminateDeficit(callerSpoke, deficitAmountRay); @@ -381,7 +370,7 @@ contract Hub is IHub, AccessManaged { Asset storage asset = _assets[assetId]; SpokeData storage spoke = _spokes[assetId][msg.sender]; - asset.accrue(); + asset.accrue(_spokes, assetId); require(spoke.active, SpokeNotActive()); // no premium change allowed require(premiumDelta.restoredPremiumRay == 0, InvalidPremiumChange()); @@ -398,7 +387,7 @@ contract Hub is IHub, AccessManaged { SpokeData storage receiver = _spokes[assetId][feeReceiver]; SpokeData storage sender = _spokes[assetId][msg.sender]; - asset.accrue(); + asset.accrue(_spokes, assetId); _validatePayFeeShares(sender, shares); _transferShares(sender, receiver, shares); asset.updateDrawnRate(assetId); @@ -412,7 +401,7 @@ contract Hub is IHub, AccessManaged { SpokeData storage sender = _spokes[assetId][msg.sender]; SpokeData storage receiver = _spokes[assetId][toSpoke]; - asset.accrue(); + asset.accrue(_spokes, assetId); _validateTransferShares(asset, sender, receiver, shares); _transferShares(sender, receiver, shares); asset.updateDrawnRate(assetId); @@ -425,7 +414,7 @@ contract Hub is IHub, AccessManaged { require(assetId < _assetCount, AssetNotListed()); Asset storage asset = _assets[assetId]; - asset.accrue(); + asset.accrue(_spokes, assetId); _validateSweep(asset, msg.sender, amount); uint256 liquidity = asset.liquidity; @@ -446,7 +435,7 @@ contract Hub is IHub, AccessManaged { require(assetId < _assetCount, AssetNotListed()); Asset storage asset = _assets[assetId]; - asset.accrue(); + asset.accrue(_spokes, assetId); _validateReclaim(asset, msg.sender, amount); uint256 liquidity = asset.liquidity + amount; @@ -529,12 +518,20 @@ contract Hub is IHub, AccessManaged { /// @inheritdoc IHubBase function getAddedAssets(uint256 assetId) external view returns (uint256) { - return _assets[assetId].totalAddedAssets(); + Asset storage asset = _assets[assetId]; + uint256 previousIndex = asset.drawnIndex; + uint256 drawnIndex = asset.getDrawnIndex(previousIndex); + (uint256 totalAssets, ) = asset.getTotalAssetsAndShares(drawnIndex, previousIndex); + return totalAssets; } /// @inheritdoc IHubBase function getAddedShares(uint256 assetId) external view returns (uint256) { - return _assets[assetId].addedShares; + Asset storage asset = _assets[assetId]; + uint256 previousIndex = asset.drawnIndex; + uint256 drawnIndex = asset.getDrawnIndex(previousIndex); + (, uint256 totalShares) = asset.getTotalAssetsAndShares(drawnIndex, previousIndex); + return totalShares; } /// @inheritdoc IHubBase @@ -599,12 +596,6 @@ contract Hub is IHub, AccessManaged { }); } - /// @inheritdoc IHub - function getAssetAccruedFees(uint256 assetId) external view returns (uint256) { - Asset storage asset = _assets[assetId]; - return asset.realizedFees + asset.getUnrealizedFees(asset.getDrawnIndex()); - } - /// @inheritdoc IHub function getAssetSwept(uint256 assetId) external view returns (uint256) { return _assets[assetId].swept; @@ -622,12 +613,22 @@ contract Hub is IHub, AccessManaged { /// @inheritdoc IHubBase function getSpokeAddedAssets(uint256 assetId, address spoke) external view returns (uint256) { - return _assets[assetId].toAddedAssetsDown(_spokes[assetId][spoke].addedShares); + Asset storage asset = _assets[assetId]; + uint256 addedShares = _spokes[assetId][spoke].addedShares; + if (asset.feeReceiver == spoke) { + addedShares += asset.unrealizedFeeShares(); + } + return _assets[assetId].toAddedAssetsDown(addedShares); } /// @inheritdoc IHubBase function getSpokeAddedShares(uint256 assetId, address spoke) external view returns (uint256) { - return _spokes[assetId][spoke].addedShares; + Asset storage asset = _assets[assetId]; + uint256 addedShares = _spokes[assetId][spoke].addedShares; + if (asset.feeReceiver == spoke) { + addedShares += asset.unrealizedFeeShares(); + } + return addedShares; } /// @inheritdoc IHubBase @@ -779,25 +780,6 @@ contract Hub is IHub, AccessManaged { ); } - function _mintFeeShares(Asset storage asset, uint256 assetId) internal returns (uint256) { - uint256 fees = asset.realizedFees; - uint120 shares = asset.toAddedSharesDown(fees).toUint120(); - if (shares == 0) { - return 0; - } - - address feeReceiver = asset.feeReceiver; - SpokeData storage feeReceiverSpoke = _spokes[assetId][feeReceiver]; - require(feeReceiverSpoke.active, SpokeNotActive()); - - asset.addedShares += shares; - feeReceiverSpoke.addedShares += shares; - asset.realizedFees = 0; - emit MintFeeShares(assetId, feeReceiver, shares, fees); - - return shares; - } - /// @dev Returns the spoke's drawn amount for a specified asset. function _getSpokeDrawn( Asset storage asset, diff --git a/src/hub/interfaces/IHub.sol b/src/hub/interfaces/IHub.sol index 60ee2ca83..336c4bee4 100644 --- a/src/hub/interfaces/IHub.sol +++ b/src/hub/interfaces/IHub.sol @@ -11,11 +11,12 @@ import {IHubBase} from 'src/hub/interfaces/IHubBase.sol'; interface IHub is IHubBase, IAccessManaged { /// @notice Asset position and configuration data. /// @dev liquidity The liquidity available to be accessed, expressed in asset units. - /// @dev realizedFees The amount of fees realized but not yet minted, expressed in asset units. + /// @dev swept The outstanding liquidity which has been invested by the reinvestment controller, expressed in asset units. /// @dev decimals The number of decimals of the underlying asset. /// @dev addedShares The total shares added across all spokes. - /// @dev swept The outstanding liquidity which has been invested by the reinvestment controller, expressed in asset units. + /// @dev realizedFees The total shares added across all spokes. /// @dev premiumOffsetRay The total premium offset across all spokes, used to calculate the premium, expressed in asset units and scaled by RAY. + /// @dev deficitRay The amount of outstanding bad debt across all spokes, expressed in asset units and scaled by RAY. /// @dev drawnShares The total drawn shares across all spokes. /// @dev premiumShares The total premium shares across all spokes. /// @dev liquidityFee The protocol fee charged on drawn and premium liquidity growth, expressed in BPS. @@ -26,7 +27,6 @@ interface IHub is IHubBase, IAccessManaged { /// @dev irStrategy The address of the interest rate strategy. /// @dev reinvestmentController The address of the reinvestment controller. /// @dev feeReceiver The address of the fee receiver spoke. - /// @dev deficitRay The amount of outstanding bad debt across all spokes, expressed in asset units and scaled by RAY. struct Asset { uint120 liquidity; uint120 realizedFees; @@ -37,6 +37,8 @@ interface IHub is IHubBase, IAccessManaged { // int200 premiumOffsetRay; // + uint200 deficitRay; + // uint120 drawnShares; uint120 premiumShares; uint16 liquidityFee; @@ -52,8 +54,6 @@ interface IHub is IHubBase, IAccessManaged { address reinvestmentController; // address feeReceiver; - // - uint200 deficitRay; } /// @notice Asset configuration. Subset of the `Asset` struct. @@ -110,13 +110,13 @@ interface IHub is IHubBase, IAccessManaged { /// @param assetId The identifier of the asset. /// @param drawnIndex The new drawn index of the asset. /// @param drawnRate The new drawn rate of the asset. - /// @param accruedFees The accrued fees of the asset since the last mint. - event UpdateAsset( - uint256 indexed assetId, - uint256 drawnIndex, - uint256 drawnRate, - uint256 accruedFees - ); + event UpdateAsset(uint256 indexed assetId, uint256 drawnIndex, uint256 drawnRate); + + /// @notice Emitted when fees are accrued to `feeReceiver`. + /// @param assetId The identifier of the asset. + /// @param spoke The address of the current feeReceiver. + /// @param shares The amount of shares accrued. + event AccrueFees(uint256 indexed assetId, address spoke, uint256 shares); /// @notice Emitted when an asset configuration is updated. /// @param assetId The identifier of the asset. @@ -269,7 +269,6 @@ interface IHub is IHubBase, IAccessManaged { /// @notice Updates the configuration of an asset. /// @dev If the fee receiver is updated, adds it as a new spoke with maximum add cap and zero draw cap, and sets old fee receiver caps to zero. - /// @dev If the fee receiver is updated, accrued fees are minted as shares before the update if their value exceeds one share. /// @dev If the interest rate strategy is updated, it is configured with `irData`. Otherwise, `irData` must be empty. /// @param assetId The identifier of the asset. /// @param config The new configuration for the asset. @@ -298,12 +297,6 @@ interface IHub is IHubBase, IAccessManaged { /// @param irData The interest rate data to apply to the given asset, encoded in bytes. function setInterestRateData(uint256 assetId, bytes calldata irData) external; - /// @notice Mints shares to the fee receiver from accrued fees. - /// @dev No op when fees are worth less than one share. - /// @param assetId The identifier of the asset. - /// @return The amount of shares minted. - function mintFeeShares(uint256 assetId) external returns (uint256); - /// @notice Eliminates deficit by removing supplied shares of caller spoke. /// @dev Only callable by active and authorized spokes. /// @param assetId The identifier of the asset. @@ -363,12 +356,6 @@ interface IHub is IHubBase, IAccessManaged { /// @return The asset configuration struct. function getAssetConfig(uint256 assetId) external view returns (AssetConfig memory); - /// @notice Returns the accrued fees for the asset, expressed in asset units. - /// @dev Accrued fees are excluded from total added assets. - /// @param assetId The identifier of the asset. - /// @return The amount of accrued fees. - function getAssetAccruedFees(uint256 assetId) external view returns (uint256); - /// @notice Returns the amount of liquidity swept by the reinvestment controller for the specified asset. /// @param assetId The identifier of the asset. /// @return The amount of liquidity swept. diff --git a/src/hub/libraries/AssetLogic.sol b/src/hub/libraries/AssetLogic.sol index 2b4f44cb4..66e5e0c39 100644 --- a/src/hub/libraries/AssetLogic.sol +++ b/src/hub/libraries/AssetLogic.sol @@ -71,14 +71,20 @@ library AssetLogic { .fromRayUp(); } - /// @notice Returns the total amount owed for the specified asset, including drawn and premium. + /// @notice Returns the total amount owed for the specified asset at specified drawnIndex. function totalOwed(IHub.Asset storage asset, uint256 drawnIndex) internal view returns (uint256) { return asset.drawn(drawnIndex) + asset.premium(drawnIndex); } - /// @notice Returns the total added assets for the specified asset. - function totalAddedAssets(IHub.Asset storage asset) internal view returns (uint256) { - uint256 drawnIndex = asset.getDrawnIndex(); + /// @notice Returns both total added assets and shares for the specified asset. + function getTotalAssetsAndShares( + IHub.Asset storage asset, + uint256 drawnIndex, + uint256 previousIndex + ) internal view returns (uint256, uint256) { + (uint256 feeAmount, uint256 feeShares) = asset.getFee(drawnIndex, previousIndex); + + uint256 realizedFees = feeShares > 0 ? 0 : feeAmount; uint256 aggregatedOwedRay = _calculateAggregatedOwedRay({ drawnShares: asset.drawnShares, @@ -88,12 +94,14 @@ library AssetLogic { drawnIndex: drawnIndex }); - return - asset.liquidity + + uint256 totalAssets = asset.liquidity + asset.swept + aggregatedOwedRay.fromRayUp() - - asset.realizedFees - - asset.getUnrealizedFees(drawnIndex); + realizedFees; + + uint256 totalShares = asset.addedShares + feeShares; + + return (totalAssets, totalShares); } /// @notice Converts an amount of shares to the equivalent amount of added assets, rounding up. @@ -101,7 +109,13 @@ library AssetLogic { IHub.Asset storage asset, uint256 shares ) internal view returns (uint256) { - return shares.toAssetsUp(asset.totalAddedAssets(), asset.addedShares); + uint256 previousIndex = asset.drawnIndex; + uint256 drawnIndex = asset.getDrawnIndex(previousIndex); + (uint256 totalAssets, uint256 totalShares) = asset.getTotalAssetsAndShares( + drawnIndex, + previousIndex + ); + return shares.toAssetsUp(totalAssets, totalShares); } /// @notice Converts an amount of shares to the equivalent amount of added assets, rounding down. @@ -109,7 +123,13 @@ library AssetLogic { IHub.Asset storage asset, uint256 shares ) internal view returns (uint256) { - return shares.toAssetsDown(asset.totalAddedAssets(), asset.addedShares); + uint256 previousIndex = asset.drawnIndex; + uint256 drawnIndex = asset.getDrawnIndex(previousIndex); + (uint256 totalAssets, uint256 totalShares) = asset.getTotalAssetsAndShares( + drawnIndex, + previousIndex + ); + return shares.toAssetsDown(totalAssets, totalShares); } /// @notice Converts an amount of added assets to the equivalent amount of shares, rounding up. @@ -117,7 +137,13 @@ library AssetLogic { IHub.Asset storage asset, uint256 assets ) internal view returns (uint256) { - return assets.toSharesUp(asset.totalAddedAssets(), asset.addedShares); + uint256 previousIndex = asset.drawnIndex; + uint256 drawnIndex = asset.getDrawnIndex(previousIndex); + (uint256 totalAssets, uint256 totalShares) = asset.getTotalAssetsAndShares( + drawnIndex, + previousIndex + ); + return assets.toSharesUp(totalAssets, totalShares); } /// @notice Converts an amount of added assets to the equivalent amount of shares, rounding down. @@ -125,7 +151,13 @@ library AssetLogic { IHub.Asset storage asset, uint256 assets ) internal view returns (uint256) { - return assets.toSharesDown(asset.totalAddedAssets(), asset.addedShares); + uint256 previousIndex = asset.drawnIndex; + uint256 drawnIndex = asset.getDrawnIndex(previousIndex); + (uint256 totalAssets, uint256 totalShares) = asset.getTotalAssetsAndShares( + drawnIndex, + previousIndex + ); + return assets.toSharesDown(totalAssets, totalShares); } /// @notice Updates the drawn rate of a specified asset. @@ -143,24 +175,47 @@ library AssetLogic { }); asset.drawnRate = newDrawnRate.toUint96(); - emit IHub.UpdateAsset(assetId, drawnIndex, newDrawnRate, asset.realizedFees); + emit IHub.UpdateAsset(assetId, drawnIndex, newDrawnRate); } /// @notice Accrues interest and fees for the specified asset. - function accrue(IHub.Asset storage asset) internal { + function accrue( + IHub.Asset storage asset, + mapping(uint256 => mapping(address => IHub.SpokeData)) storage spokes, + uint256 assetId + ) internal { if (asset.lastUpdateTimestamp == block.timestamp) { return; } - uint256 drawnIndex = asset.getDrawnIndex(); - asset.realizedFees += asset.getUnrealizedFees(drawnIndex).toUint120(); + uint256 previousIndex = asset.drawnIndex; + uint256 drawnIndex = asset.getDrawnIndex(previousIndex); + asset.drawnIndex = drawnIndex.toUint120(); asset.lastUpdateTimestamp = block.timestamp.toUint40(); + + (uint256 feeAmount, uint256 feeShares) = asset.getFee(drawnIndex, previousIndex); + if (feeShares > 0) { + address feeReceiver = asset.feeReceiver; + asset.realizedFees = 0; + asset.addedShares += feeShares.toUint120(); + spokes[assetId][feeReceiver].addedShares += feeShares.toUint120(); + emit IHub.AccrueFees(assetId, feeReceiver, feeShares); + } else { + asset.realizedFees = feeAmount.toUint120(); + } } - /// @notice Calculates the drawn index of a specified asset based on the existing drawn rate and index. + /// @notice Calculates the current drawnIndex based on stored drawnRate. function getDrawnIndex(IHub.Asset storage asset) internal view returns (uint256) { - uint256 previousIndex = asset.drawnIndex; + return asset.getDrawnIndex({previousIndex: asset.drawnIndex}); + } + + /// @notice Calculates the drawn index of a specified asset based on the existing drawn rate and index. + function getDrawnIndex( + IHub.Asset storage asset, + uint256 previousIndex + ) internal view returns (uint256) { uint40 lastUpdateTimestamp = asset.lastUpdateTimestamp; if ( lastUpdateTimestamp == block.timestamp || (asset.drawnShares == 0 && asset.premiumShares == 0) @@ -173,20 +228,27 @@ library AssetLogic { ); } - /// @notice Calculates the amount of fees derived from the index growth due to interest accrual. - /// @param drawnIndex The current drawn index. - function getUnrealizedFees( + /// @notice Calculates the amount of unrealized fee shares since last accrual. + function unrealizedFeeShares(IHub.Asset storage asset) internal view returns (uint256) { + (, uint256 feeShares) = asset.getFee(asset.getDrawnIndex(), asset.drawnIndex); + return feeShares; + } + + /// @notice Calculates the amount of fee shares derived from the index growth due to interest accrual. + /// @dev The true liquidity growth is always greater than accrued fees, even with 100.00% liquidity fee. + function getFee( IHub.Asset storage asset, - uint256 drawnIndex - ) internal view returns (uint256) { - uint256 previousIndex = asset.drawnIndex; - if (previousIndex == drawnIndex) { - return 0; + uint256 drawnIndex, + uint256 previousIndex + ) internal view returns (uint256, uint256) { + uint256 feeAmount = asset.realizedFees; + if (drawnIndex == previousIndex) { + return (feeAmount, 0); } uint256 liquidityFee = asset.liquidityFee; if (liquidityFee == 0) { - return 0; + return (feeAmount, 0); } uint120 drawnShares = asset.drawnShares; @@ -194,26 +256,31 @@ library AssetLogic { int256 premiumOffsetRay = asset.premiumOffsetRay; uint256 deficitRay = asset.deficitRay; - uint256 aggregatedOwedRayAfter = _calculateAggregatedOwedRay({ + uint256 aggregatedOwedAfter = _calculateAggregatedOwedRay({ drawnShares: drawnShares, premiumShares: premiumShares, premiumOffsetRay: premiumOffsetRay, deficitRay: deficitRay, drawnIndex: drawnIndex - }); + }).fromRayUp(); - uint256 aggregatedOwedRayBefore = _calculateAggregatedOwedRay({ + uint256 aggregatedOwedBefore = _calculateAggregatedOwedRay({ drawnShares: drawnShares, premiumShares: premiumShares, premiumOffsetRay: premiumOffsetRay, deficitRay: deficitRay, drawnIndex: previousIndex - }); + }).fromRayUp(); - return - (aggregatedOwedRayAfter.fromRayUp() - aggregatedOwedRayBefore.fromRayUp()).percentMulDown( - liquidityFee - ); + feeAmount += (aggregatedOwedAfter - aggregatedOwedBefore).percentMulDown(liquidityFee); + + return ( + feeAmount, + feeAmount.toSharesDown( + asset.liquidity + asset.swept + aggregatedOwedAfter - feeAmount, + asset.addedShares + ) + ); } /// @notice Calculates the aggregated owed amount for a specified asset, expressed in asset units and scaled by RAY. diff --git a/tests/Base.t.sol b/tests/Base.t.sol index 3594759fd..2011e3245 100644 --- a/tests/Base.t.sol +++ b/tests/Base.t.sol @@ -353,13 +353,12 @@ abstract contract Base is Test { } { - bytes4[] memory selectors = new bytes4[](6); + bytes4[] memory selectors = new bytes4[](5); selectors[0] = IHub.addAsset.selector; selectors[1] = IHub.updateAssetConfig.selector; selectors[2] = IHub.addSpoke.selector; selectors[3] = IHub.updateSpokeConfig.selector; selectors[4] = IHub.setInterestRateData.selector; - selectors[5] = IHub.mintFeeShares.selector; manager.setTargetFunctionRole(address(targetHub), selectors, Roles.HUB_ADMIN_ROLE); } @@ -2201,6 +2200,22 @@ abstract contract Base is Test { return (currentDrawnDebt - initialDrawnDebt).percentMulUp(userRiskPremium); } + /// @dev Calculate expected total debt (drawn + premium) based on specified borrow rate and risk premium + function _calculateExpectedTotalDebt( + uint256 initialDrawnDebt, + uint96 borrowRate, + uint40 startTime, + uint256 userRiskPremium + ) internal view returns (uint256) { + uint256 drawnDebt = _calculateExpectedDrawnDebt(initialDrawnDebt, borrowRate, startTime); + uint256 premiumDebt = _calculateExpectedPremiumDebt( + initialDrawnDebt, + drawnDebt, + userRiskPremium + ); + return drawnDebt + premiumDebt; + } + /// @dev Helper function to get asset drawn debt function getAssetDrawnDebt(uint256 assetId) internal view returns (uint256) { (uint256 drawn, ) = hub1.getAssetOwed(assetId); @@ -2213,6 +2228,15 @@ abstract contract Base is Test { hub.getAddedAssets(assetId) - hub.previewRemoveByShares(assetId, hub.getAddedShares(assetId)); } + /// @dev Helper function to calculate the expected dust (tokens left in hub) after full withdrawal. + /// @dev This equals burntInterest + realizedFees + function _calculateExpectedDustAfterFullWithdraw( + IHub hub, + uint256 assetId + ) internal view returns (uint256) { + return _calculateBurntInterest(hub, assetId) + hub.getAsset(assetId).realizedFees; + } + function _calculatePremiumDebt( IHub hub, uint256 assetId, @@ -2291,7 +2315,6 @@ abstract contract Base is Test { /// @dev Helper function to withdraw fees from the treasury spoke function _withdrawLiquidityFees(IHub hub, uint256 assetId, uint256 amount) internal { - Utils.mintFeeShares(hub, assetId, ADMIN); uint256 fees = hub.getSpokeAddedAssets(assetId, address(treasurySpoke)); if (amount > fees) { @@ -3132,15 +3155,23 @@ abstract contract Base is Test { IHub hub, uint256 assetId ) internal view returns (uint256) { - uint256 expectedFees = hub.getAsset(assetId).realizedFees + _calcUnrealizedFees(hub, assetId); - assertEq(expectedFees, hub.getAssetAccruedFees(assetId), 'asset accrued fees'); - return hub.getSpokeAddedAssets(assetId, hub.getAsset(assetId).feeReceiver) + expectedFees; + address feeReceiver = _getFeeReceiver(hub, assetId); + return hub.getSpokeAddedAssets(assetId, feeReceiver); } - function _getAddedAssetsWithFees(IHub hub, uint256 assetId) internal view returns (uint256) { - return - hub.getAddedAssets(assetId) + - hub.getAsset(assetId).realizedFees + - _calcUnrealizedFees(hub, assetId); + function _getFeeReceiverAddedAssets(IHub hub, uint256 assetId) internal view returns (uint256) { + address feeReceiver = hub.getAsset(assetId).feeReceiver; + return hub.getSpokeAddedAssets(assetId, feeReceiver); + } + + function _getFeeReceiverAddedShares(IHub hub, uint256 assetId) internal view returns (uint256) { + address feeReceiver = hub.getAsset(assetId).feeReceiver; + return hub.getSpokeAddedShares(assetId, feeReceiver); + } + + function _extrapolateFeeShares(IHub hub, uint256 assetId) internal view returns (uint256) { + uint256 extrapolatedBalance = hub.getAddedShares(assetId); + uint256 cachedBalance = hub.getAsset(assetId).addedShares; + return extrapolatedBalance - cachedBalance; } } diff --git a/tests/Utils.sol b/tests/Utils.sol index d02fb097c..4615b1305 100644 --- a/tests/Utils.sol +++ b/tests/Utils.sol @@ -174,11 +174,6 @@ library Utils { spoke.repay(reserveId, amount, onBehalfOf); } - function mintFeeShares(IHub hub, uint256 assetId, address caller) internal returns (uint256) { - vm.prank(caller); - return hub.mintFeeShares(assetId); - } - function approve(ISpoke spoke, uint256 reserveId, address owner, uint256 amount) internal { address underlying = spoke.getReserve(reserveId).underlying; _approve(IERC20(underlying), owner, address(spoke), amount); diff --git a/tests/gas/Hub.Operations.gas.t.sol b/tests/gas/Hub.Operations.gas.t.sol index a54761a0b..237c04015 100644 --- a/tests/gas/Hub.Operations.gas.t.sol +++ b/tests/gas/Hub.Operations.gas.t.sol @@ -207,24 +207,6 @@ contract HubOperations_Gas_Tests is Base { vm.snapshotGasLastCall('Hub.Operations', 'refreshPremium'); } - function test_mintFeeShares() public { - vm.startPrank(address(spoke2)); - tokenList.dai.transferFrom(alice, address(hub1), 1000e18); - hub1.add(daiAssetId, 1000e18); - vm.stopPrank(); - - vm.startPrank(address(spoke1)); - tokenList.usdx.transferFrom(alice, address(hub1), 1000e6); - hub1.add(usdxAssetId, 1000e6); - hub1.draw(daiAssetId, 500e18, alice); - vm.stopPrank(); - - skip(100); - - Utils.mintFeeShares(hub1, daiAssetId, ADMIN); - vm.snapshotGasLastCall('Hub.Operations', 'mintFeeShares'); - } - function test_payFee_transferShares() public { Utils.add({ hub: hub1, diff --git a/tests/unit/Hub/Hub.Accrual.t.sol b/tests/unit/Hub/Hub.Accrual.t.sol new file mode 100644 index 000000000..3096fce59 --- /dev/null +++ b/tests/unit/Hub/Hub.Accrual.t.sol @@ -0,0 +1,273 @@ +// SPDX-License-Identifier: UNLICENSED +// Copyright (c) 2025 Aave Labs +pragma solidity ^0.8.0; + +import 'tests/unit/Hub/HubBase.t.sol'; + +contract HubAccrualTest is HubBase { + using WadRayMath for uint256; + using PercentageMath for uint256; + using MathUtils for uint256; + + uint256 constant SUPPLY_AMOUNT = 1000e18; + uint256 constant BORROW_AMOUNT = 500e18; + + /// @dev Fuzz test for basic fee accrual with varying supply, borrow, fee, and time + function test_accrual_fuzz_basicAccrual( + uint256 supplyAmount, + uint256 borrowAmount, + uint256 liquidityFee, + uint256 skipTime + ) public { + supplyAmount = bound(supplyAmount, 2, MAX_SUPPLY_AMOUNT); + borrowAmount = bound(borrowAmount, 1, supplyAmount / 2); + liquidityFee = bound(liquidityFee, 0, PercentageMath.PERCENTAGE_FACTOR); + skipTime = bound(skipTime, 1, MAX_SKIP_TIME); + + updateLiquidityFee(hub1, daiAssetId, liquidityFee); + + uint256 bobShares = Utils.add({ + hub: hub1, + assetId: daiAssetId, + caller: address(spoke1), + amount: supplyAmount, + user: bob + }); + Utils.draw({ + hub: hub1, + assetId: daiAssetId, + to: bob, + caller: address(spoke1), + amount: borrowAmount + }); + + uint256 totalAssetsBefore = hub1.getAddedAssets(daiAssetId); + uint256 bobAssetsBefore = hub1.previewRemoveByShares(daiAssetId, bobShares); + assertEq(bobAssetsBefore, supplyAmount); + + uint256 treasuryAssetsBefore = _getFeeReceiverAddedAssets(hub1, daiAssetId); + assertEq(treasuryAssetsBefore, 0); + + uint96 drawnRate = hub1.getAsset(daiAssetId).drawnRate; + uint40 startTime = uint40(block.timestamp); + + skip(skipTime); + + uint256 totalInterest; + uint256 accruedFees; + uint256 expectedTotalInterest; + { + uint256 expectedTotalDebt = _calculateExpectedTotalDebt( + borrowAmount, + drawnRate, + startTime, + 0 + ); + expectedTotalInterest = expectedTotalDebt - borrowAmount; + uint256 totalDebt = hub1.getAssetTotalOwed(daiAssetId); + totalInterest = totalDebt - borrowAmount; + assertEq(totalInterest, expectedTotalInterest); + + accruedFees = _getFeeReceiverAddedAssets(hub1, daiAssetId); + } + + uint256 totalAssetsAfter = hub1.getAddedAssets(daiAssetId); + assertApproxEqAbs( + totalAssetsAfter, + totalAssetsBefore + totalInterest, + 1, + 'total added assets = initial + all interest' + ); + + uint256 bobAssetsAfter = hub1.previewRemoveByShares(daiAssetId, bobShares); + assertGe(bobAssetsAfter, bobAssetsBefore, 'bob assets increased'); + assertLe(accruedFees, totalInterest, 'fees do not exceed total interest'); + + { + assertApproxEqAbs( + totalInterest, + totalAssetsAfter - totalAssetsBefore, + 1, + 'total growth in supply matches total growth in debt' + ); + + assertApproxEqAbs( + _getFeeReceiverAddedAssets(hub1, daiAssetId), + expectedTotalInterest.percentMulDown(liquidityFee), + 2, + 'treasury spoke assets match expected fee split' + ); + + uint256 supplierInterest = totalInterest - accruedFees; + uint256 expectedBobGrowth = supplierInterest.mulDivDown( + bobShares, + bobShares + SharesMath.VIRTUAL_SHARES + ); + assertApproxEqAbs( + bobAssetsAfter - bobAssetsBefore, + expectedBobGrowth, + 1, + 'bob growth (supplier) matches expected fee split' + ); + } + } + + /// @dev Verifies protocol cut rounds to 0 when interest is tiny, all goes to suppliers + function test_accrual_roundsToZero() public { + uint256 liquidityFee = 1_00; // 1% + uint256 drawnAmount = 100; + + updateLiquidityFee(hub1, daiAssetId, liquidityFee); + + Utils.add({hub: hub1, assetId: daiAssetId, caller: address(spoke1), amount: 1000, user: bob}); + + uint256 initialSharePrice = hub1.previewAddByShares(daiAssetId, 1e18); + + Utils.draw({ + hub: hub1, + assetId: daiAssetId, + to: bob, + caller: address(spoke1), + amount: drawnAmount + }); + + skip(1 hours); + + (uint256 drawnDebt, ) = hub1.getAssetOwed(daiAssetId); + uint256 delta = drawnDebt - drawnAmount; + uint256 accruedFees = _getFeeReceiverAddedAssets(hub1, daiAssetId); + uint256 finalSharePrice = hub1.previewAddByShares(daiAssetId, 1e18); + + assertEq(delta.percentMulDown(liquidityFee), 0); + assertEq(accruedFees, 0); + assertEq(_calcUnrealizedFees(hub1, daiAssetId), 0); + assertGt(delta, 0); + _checkSupplyRateIncreasing(initialSharePrice, finalSharePrice, 'share price'); + } + + /// @dev Tests that when fees round to 0 shares, they accumulate in realizedFees, until they exceed 1 share. + function test_accrual_realizedFeesAccumulation() public { + uint256 liquidityFee = 50_00; + updateLiquidityFee(hub1, daiAssetId, liquidityFee); + + // Inflate share price + Utils.add({ + hub: hub1, + assetId: daiAssetId, + caller: address(spoke1), + amount: 1000e18, + user: bob + }); + Utils.draw({hub: hub1, assetId: daiAssetId, to: bob, caller: address(spoke1), amount: 500e18}); + + skip(5 * 365 days); + Utils.add({hub: hub1, assetId: daiAssetId, caller: address(spoke1), amount: 1e18, user: bob}); + + (uint256 currentDebt, ) = hub1.getAssetOwed(daiAssetId); + Utils.restoreDrawn({ + hub: hub1, + assetId: daiAssetId, + caller: address(spoke1), + drawnAmount: currentDebt, + restorer: bob + }); + + // Debt is cleared and share price inflated + (uint256 debtAfterRepay, ) = hub1.getAssetOwed(daiAssetId); + assertEq(debtAfterRepay, 0, 'debt is 0'); + uint256 sharePrice = hub1.previewAddByShares(daiAssetId, 1e18); + assertGt(sharePrice, 1e18, 'share price > 1'); + + // Small accrual where feeShares round to 0 + Utils.draw({hub: hub1, assetId: daiAssetId, to: bob, caller: address(spoke1), amount: 1e8}); + + uint256 treasurySharesBefore = _getFeeReceiverAddedShares(hub1, daiAssetId); + + skip(25 seconds); + Utils.add({hub: hub1, assetId: daiAssetId, caller: address(spoke1), amount: 1e15, user: bob}); + + IHub.Asset memory assetAfterFirst = hub1.getAsset(daiAssetId); + assertGt(assetAfterFirst.realizedFees, 0, 'realizedFees accumulated'); + assertEq( + _getFeeReceiverAddedShares(hub1, daiAssetId), + treasurySharesBefore, + 'no new fee shares' + ); + + // Second accrual causes realizedFees to become greater than 1 share, minting fee shares, and resetting realizedFees + skip(1 days); + Utils.add({hub: hub1, assetId: daiAssetId, caller: address(spoke1), amount: 1e15, user: bob}); + + uint256 treasurySharesAfterSecond = _getFeeReceiverAddedShares(hub1, daiAssetId); + assertGt(treasurySharesAfterSecond, treasurySharesBefore, 'shares minted'); + IHub.Asset memory assetAfterSecond = hub1.getAsset(daiAssetId); + assertEq(assetAfterSecond.realizedFees, 0, 'realizedFees reset'); + } + + /// @dev Tests that when fees exceed 1 share worth, the fractional remainder is donated to suppliers. + /// @dev If feeAmount = 1.2 shares worth, mint 1 share, remainder (0.2) goes to suppliers. + function test_accrual_fractionalFeeRemainderDonatedToSuppliers() public { + uint256 liquidityFee = 50_00; + updateLiquidityFee(hub1, daiAssetId, liquidityFee); + + // Inflate share price + Utils.add({ + hub: hub1, + assetId: daiAssetId, + caller: address(spoke1), + amount: 1000e18, + user: bob + }); + Utils.draw({hub: hub1, assetId: daiAssetId, to: bob, caller: address(spoke1), amount: 500e18}); + + skip(5 * 365 days); + Utils.add({hub: hub1, assetId: daiAssetId, caller: address(spoke1), amount: 1e18, user: bob}); + + (uint256 currentDebt, ) = hub1.getAssetOwed(daiAssetId); + Utils.restoreDrawn({ + hub: hub1, + assetId: daiAssetId, + caller: address(spoke1), + drawnAmount: currentDebt, + restorer: bob + }); + + // Accrue interest + Utils.draw({hub: hub1, assetId: daiAssetId, to: bob, caller: address(spoke1), amount: 1e8}); + + uint256 treasurySharesBefore = _getFeeReceiverAddedShares(hub1, daiAssetId); + uint256 totalAddedAssetsBefore = hub1.getAddedAssets(daiAssetId); + + skip(45 seconds); + + uint256 expectedFeeAmount = _calcUnrealizedFees(hub1, daiAssetId); + assertGt(expectedFeeAmount, 0, 'has unrealized fees'); + + // Expected fee amount is not perfect share amount + uint256 expectedMintedShares = hub1.previewAddByAssets(daiAssetId, expectedFeeAmount); + uint256 expectedMintedAssets = hub1.previewRemoveByShares(daiAssetId, expectedMintedShares); + assertLt(expectedMintedAssets, expectedFeeAmount, 'fee not perfect share amount'); + + // Trigger accrual + Utils.add({hub: hub1, assetId: daiAssetId, caller: address(spoke1), amount: 1e15, user: bob}); + + uint256 treasurySharesAfter = _getFeeReceiverAddedShares(hub1, daiAssetId); + uint256 sharesMinted = treasurySharesAfter - treasurySharesBefore; + + // Perfect fee share amount was minted, but rest of fees were donated + assertGt(sharesMinted, 0, 'fee shares minted'); + IHub.Asset memory assetAfter = hub1.getAsset(daiAssetId); + assertEq(assetAfter.realizedFees, 0, 'realizedFees reset'); + + // Total added assets increased appropriately (about twice expectedFeeAmount since liquidityFee = 50%) + uint256 totalAddedAssetsAfter = hub1.getAddedAssets(daiAssetId); + // Account for the 1e15 that bob just added + uint256 totalAddedAssetsIncrease = totalAddedAssetsAfter - totalAddedAssetsBefore - 1e15; + assertApproxEqAbs( + totalAddedAssetsIncrease, + expectedFeeAmount * 2, + 1, + 'total added assets increased correctly' + ); + } +} diff --git a/tests/unit/Hub/Hub.Config.t.sol b/tests/unit/Hub/Hub.Config.t.sol index 42cfde53b..5340cf128 100644 --- a/tests/unit/Hub/Hub.Config.t.sol +++ b/tests/unit/Hub/Hub.Config.t.sol @@ -330,7 +330,7 @@ contract HubConfigTest is HubBase { vm.expectEmit(address(hub1)); emit IHub.UpdateAssetConfig(expectedAssetId, expectedConfig); vm.expectEmit(address(hub1)); - emit IHub.UpdateAsset(expectedAssetId, WadRayMath.RAY, baseVariableBorrowRate.bpsToRay(), 0); + emit IHub.UpdateAsset(expectedAssetId, WadRayMath.RAY, baseVariableBorrowRate.bpsToRay()); uint256 assetId = Utils.addAsset( hub1, @@ -481,19 +481,15 @@ contract HubConfigTest is HubBase { address oldFeeReceiver = _getFeeReceiver(hub1, assetId); IHub.SpokeConfig memory oldFeeReceiverConfig = hub1.getSpokeConfig(assetId, oldFeeReceiver); + uint256 expectedFeeShares = _extrapolateFeeShares(hub1, assetId); + if (expectedFeeShares > 0) { + vm.expectEmit(address(hub1)); + emit IHub.AccrueFees(assetId, _getFeeReceiver(hub1, assetId), expectedFeeShares); + } + // new spoke is added only if it is different from the old one and not yet listed bool isNewFeeReceiver = newConfig.feeReceiver != _getFeeReceiver(hub1, assetId); if (isNewFeeReceiver && !hub1.isSpokeListed(assetId, newConfig.feeReceiver)) { - if (_calcUnrealizedFees(hub1, assetId) > 0) { - uint256 accruedFees = hub1.getAssetAccruedFees(assetId); - vm.expectEmit(address(hub1)); - emit IHub.MintFeeShares( - assetId, - _getFeeReceiver(hub1, assetId), - hub1.previewAddByAssets(assetId, accruedFees), - accruedFees - ); - } vm.expectEmit(address(hub1)); emit IHub.UpdateSpokeConfig( assetId, @@ -534,8 +530,7 @@ contract HubConfigTest is HubBase { drawn: drawn, deficit: 0, swept: 0 - }), - isNewFeeReceiver ? 0 : hub1.getAssetAccruedFees(assetId) + }) ); vm.expectEmit(address(hub1)); emit IHub.UpdateAssetConfig(assetId, newConfig); @@ -653,23 +648,6 @@ contract HubConfigTest is HubBase { ); } - /// Updates the fee receiver while the current fee receiver is not active - function test_updateAssetConfig_NewFeeReceiver_revertsWith_SpokeNotActive_noFees() public { - uint256 assetId = daiAssetId; - - uint256 amount = 1000e18; - _addLiquidity(assetId, amount); - _drawLiquidity(assetId, amount, true); - skip(365 days); - - _updateSpokeActive(hub1, assetId, _getFeeReceiver(hub1, assetId), false); - IHub.AssetConfig memory config = hub1.getAssetConfig(assetId); - config.feeReceiver = makeAddr('newFeeReceiver'); - - vm.expectRevert(IHub.SpokeNotActive.selector, address(hub1)); - Utils.updateAssetConfig(hub1, ADMIN, assetId, config, new bytes(0)); - } - /// Updates the fee receiver while the current fee receiver is not active and no fees are accrued function test_updateAssetConfig_NewFeeReceiver_noFees() public { uint256 assetId = daiAssetId; @@ -679,8 +657,6 @@ contract HubConfigTest is HubBase { _drawLiquidity(assetId, amount, true); skip(365 days); - Utils.mintFeeShares(hub1, assetId, ADMIN); - _updateSpokeActive(hub1, assetId, _getFeeReceiver(hub1, assetId), false); IHub.AssetConfig memory config = hub1.getAssetConfig(assetId); config.feeReceiver = makeAddr('newFeeReceiver'); @@ -699,7 +675,6 @@ contract HubConfigTest is HubBase { uint256 oldFees = hub1.getSpokeAddedShares(assetId, oldFeeReceiver); skip(365 days); - Utils.mintFeeShares(hub1, assetId, ADMIN); IHub.AssetConfig memory config = hub1.getAssetConfig(assetId); address newFeeReceiver = config.feeReceiver; diff --git a/tests/unit/Hub/Hub.Draw.t.sol b/tests/unit/Hub/Hub.Draw.t.sol index 1f7cdf63e..fb60f0af6 100644 --- a/tests/unit/Hub/Hub.Draw.t.sol +++ b/tests/unit/Hub/Hub.Draw.t.sol @@ -35,6 +35,11 @@ contract HubDrawTest is HubBase { ) ); + uint256 expectedFeeShares = _extrapolateFeeShares(hub1, assetId); + if (expectedFeeShares > 0) { + vm.expectEmit(address(hub1)); + emit IHub.AccrueFees(assetId, _getFeeReceiver(hub1, assetId), expectedFeeShares); + } vm.expectEmit(address(hub1)); emit IHub.UpdateAsset( assetId, @@ -45,8 +50,7 @@ contract HubDrawTest is HubBase { drawn: hub1.previewRestoreByShares(assetId, assetBefore.drawnShares + shares), deficit: assetBefore.deficitRay, swept: assetBefore.swept - }), - hub1.getAssetAccruedFees(assetId) + }) ); vm.expectEmit(address(hub1.getAsset(assetId).underlying)); emit IERC20.Transfer(address(hub1), alice, amount); @@ -121,6 +125,11 @@ contract HubDrawTest is HubBase { ) ); + uint256 expectedFeeShares = _extrapolateFeeShares(hub1, assetId); + if (expectedFeeShares > 0) { + vm.expectEmit(address(hub1)); + emit IHub.AccrueFees(assetId, _getFeeReceiver(hub1, assetId), expectedFeeShares); + } vm.expectEmit(address(hub1)); emit IHub.UpdateAsset( assetId, @@ -131,8 +140,7 @@ contract HubDrawTest is HubBase { drawn: hub1.previewRestoreByShares(assetId, assetBefore.drawnShares + shares), deficit: assetBefore.deficitRay, swept: assetBefore.swept - }), - hub1.getAssetAccruedFees(assetId) + }) ); vm.expectEmit(address(hub1.getAsset(assetId).underlying)); emit IERC20.Transfer(address(hub1), alice, amount); diff --git a/tests/unit/Hub/Hub.MintFeeShares.t.sol b/tests/unit/Hub/Hub.MintFeeShares.t.sol deleted file mode 100644 index 443adb4a1..000000000 --- a/tests/unit/Hub/Hub.MintFeeShares.t.sol +++ /dev/null @@ -1,156 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -// Copyright (c) 2025 Aave Labs -pragma solidity ^0.8.0; - -import 'tests/unit/Hub/HubBase.t.sol'; - -contract HubMintFeeSharesTest is HubBase { - function test_mintFeeShares_revertsWith_AccessManagedUnauthorized() public { - vm.expectRevert( - abi.encodeWithSelector(IAccessManaged.AccessManagedUnauthorized.selector, address(this)) - ); - Utils.mintFeeShares(hub1, daiAssetId, address(this)); - } - - function test_mintFeeShares_revertsWith_SpokeNotActive() public { - // Create debt to build up fees on the existing treasury spoke - _addAndDrawLiquidity({ - hub: hub1, - assetId: daiAssetId, - addUser: bob, - addSpoke: address(spoke1), - addAmount: 100e18, - drawUser: bob, - drawSpoke: address(spoke1), - drawAmount: 10e18, - skipTime: 365 days - }); - - _updateSpokeActive(hub1, daiAssetId, _getFeeReceiver(hub1, daiAssetId), false); - vm.expectRevert(IHub.SpokeNotActive.selector, address(hub1)); - Utils.mintFeeShares(hub1, daiAssetId, ADMIN); - } - - function test_mintFeeShares_revertsWith_AssetNotListed() public { - uint256 invalidAssetId = hub1.getAssetCount(); - vm.expectRevert(IHub.AssetNotListed.selector); - Utils.mintFeeShares(hub1, invalidAssetId, ADMIN); - } - - function test_mintFeeShares() public { - // Create debt to build up fees on the existing treasury spoke - _addAndDrawLiquidity({ - hub: hub1, - assetId: daiAssetId, - addUser: bob, - addSpoke: address(spoke1), - addAmount: 1000e18, - drawUser: bob, - drawSpoke: address(spoke1), - drawAmount: 100e18, - skipTime: 365 days - }); - - address feeReceiver = _getFeeReceiver(hub1, daiAssetId); - - // before mintFeeShares, the fee shares should be 0 - uint256 realizedFees = hub1.getAsset(daiAssetId).realizedFees; - assertEq(realizedFees, 0); - uint256 feeShares = hub1.getSpokeAddedShares(daiAssetId, feeReceiver); - assertEq(feeShares, 0); - - uint256 expectedMintedAssets = _getExpectedFeeReceiverAddedAssets(hub1, daiAssetId); - uint256 expectedMintedShares = hub1.previewAddByAssets(daiAssetId, expectedMintedAssets); - - IHub.Asset memory asset = hub1.getAsset(daiAssetId); - bytes memory irCalldata = abi.encodeCall( - IBasicInterestRateStrategy.calculateInterestRate, - ( - daiAssetId, - asset.liquidity, - hub1.previewRestoreByShares(daiAssetId, hub1.getAssetDrawnShares(daiAssetId)), - asset.deficitRay, - asset.swept - ) - ); - uint256 mockRate = 0.3e27; - vm.mockCall(address(irStrategy), irCalldata, abi.encode(mockRate)); - - // after mintFeeShares, the fee shares should be the amount of the fees - vm.expectEmit(address(hub1)); - emit IHub.MintFeeShares(daiAssetId, feeReceiver, expectedMintedShares, expectedMintedAssets); - vm.expectEmit(address(hub1)); - emit IHub.UpdateAsset(daiAssetId, hub1.getAssetDrawnIndex(daiAssetId), mockRate, 0); - - uint256 addedSharesBefore = hub1.getAddedShares(daiAssetId); - uint256 sharePriceBefore = hub1.previewAddByShares(daiAssetId, 1e18); - - vm.expectCall(address(irStrategy), irCalldata); - uint256 mintedShares = Utils.mintFeeShares(hub1, daiAssetId, ADMIN); - - assertEq(mintedShares, expectedMintedShares, 'minted shares'); - assertEq(hub1.getAsset(daiAssetId).realizedFees, 0, 'realized fees after'); - assertEq( - hub1.getSpokeAddedShares(daiAssetId, feeReceiver), - expectedMintedShares, - 'added shares' - ); - assertEq(mintedShares, hub1.getAddedShares(daiAssetId) - addedSharesBefore, 'minted shares'); - assertGe(hub1.previewAddByShares(daiAssetId, 1e18), sharePriceBefore, 'share price'); - } - - function test_mintFeeShares_noFees() public { - test_mintFeeShares(); - - IHub.Asset memory asset = hub1.getAsset(daiAssetId); - - // pausing the fee receiver does not revert the action since no shares are minted - _updateSpokeActive(hub1, daiAssetId, _getFeeReceiver(hub1, daiAssetId), false); - - vm.expectEmit(address(hub1)); - emit IHub.UpdateAsset(daiAssetId, asset.drawnIndex, asset.drawnRate, 0); - - vm.recordLogs(); - Utils.mintFeeShares(hub1, daiAssetId, ADMIN); - vm.getRecordedLogs(); - _assertEventNotEmitted(IHub.MintFeeShares.selector); - } - - function test_mintFeeShares_noShares() public { - updateLiquidityFee(hub1, daiAssetId, 0); - _mockInterestRateRay(2); - - // Create debt to build up fees on the existing treasury spoke - _addAndDrawLiquidity({ - hub: hub1, - assetId: daiAssetId, - addUser: bob, - addSpoke: address(spoke1), - addAmount: 3, - drawUser: bob, - drawSpoke: address(spoke1), - drawAmount: 1, - skipTime: 365 days - }); - - // drawn index is 1.0000...002 - assertEq(hub1.getAssetDrawnIndex(daiAssetId), 1e27 + 2); - - _mockInterestRateRay(1e27 - 3); - updateLiquidityFee(hub1, daiAssetId, PercentageMath.PERCENTAGE_FACTOR); - - // mint fee shares just to accrue (liquidity fee is 0, so no fees are minted) - Utils.mintFeeShares(hub1, daiAssetId, ADMIN); - skip(365 days); - - // drawn index is 2.000...001 - assertEq(hub1.getAssetDrawnIndex(daiAssetId), 2e27 + 1); - - vm.recordLogs(); - Utils.mintFeeShares(hub1, daiAssetId, ADMIN); - vm.getRecordedLogs(); - _assertEventNotEmitted(IHub.MintFeeShares.selector); - - assertEq(hub1.getAsset(daiAssetId).realizedFees, 1, 'realized fees after'); - } -} diff --git a/tests/unit/Hub/Hub.Remove.t.sol b/tests/unit/Hub/Hub.Remove.t.sol index 0eac78169..a7b96350d 100644 --- a/tests/unit/Hub/Hub.Remove.t.sol +++ b/tests/unit/Hub/Hub.Remove.t.sol @@ -146,11 +146,24 @@ contract HubRemoveTest is HubBase { SpokePosition memory spokePosition1 = getSpokePosition(spoke1, _daiReserveId); SpokePosition memory spokePosition2 = getSpokePosition(spoke2, _daiReserveId); + address feeReceiver = _getFeeReceiver(hub1, assetId); + // asset // only remaining added amount are fees assertEq( + assetData.addedAmount, + hub1.getSpokeAddedAssets(assetId, feeReceiver), + 'asset addedAmount after' + ); + assertEq( + assetData.addedShares, + hub1.getSpokeAddedShares(assetId, feeReceiver), + 'asset addedShares after' + ); + assertApproxEqAbs( assetData.liquidity, - hub1.getAsset(assetId).realizedFees + _calculateBurntInterest(hub1, assetId), + hub1.getSpokeAddedAssets(assetId, feeReceiver) + _calculateBurntInterest(hub1, assetId), + 1, 'asset liquidity after' ); assertEq( @@ -214,6 +227,14 @@ contract HubRemoveTest is HubBase { uint256 removeAmount = hub1.getSpokeAddedAssets(daiAssetId, address(spoke2)); uint256 daiBalanceBefore = tokenList.dai.balanceOf(bob); + uint256 feeAmount = hub1.getSpokeAddedAssets( + daiAssetId, + hub1.getAssetConfig(daiAssetId).feeReceiver + ); + uint256 feeShares = hub1.getSpokeAddedShares( + daiAssetId, + hub1.getAssetConfig(daiAssetId).feeReceiver + ); // removable amount should exceed initial added amount due to accrued interest assertTrue(removeAmount > addAmount); @@ -228,8 +249,8 @@ contract HubRemoveTest is HubBase { asset = getAssetPosition(hub1, daiAssetId); // hub - assertApproxEqAbs(asset.addedAmount, 0, 1, 'asset addedAmount'); - assertEq(asset.addedShares, 0, 'asset addedShares'); + assertApproxEqAbs(asset.addedAmount, feeAmount, 1, 'asset addedAmount'); + assertEq(asset.addedShares, feeShares, 'asset addedShares'); assertApproxEqAbs(asset.liquidity, initialLiquidity - removeAmount, 1, 'dai liquidity'); assertEq(asset.drawn, 0, 'dai drawn'); assertEq(asset.premium, 0, 'dai premium'); @@ -306,6 +327,14 @@ contract HubRemoveTest is HubBase { uint256 removeAmount = hub1.getSpokeAddedAssets(daiAssetId, address(spoke2)); uint256 daiBalanceBefore = tokenList.dai.balanceOf(bob); + uint256 feeAmount = hub1.getSpokeAddedAssets( + daiAssetId, + hub1.getAssetConfig(daiAssetId).feeReceiver + ); + uint256 feeShares = hub1.getSpokeAddedShares( + daiAssetId, + hub1.getAssetConfig(daiAssetId).feeReceiver + ); // bob removes all possible liquidity // some has gone to feeReceiver @@ -317,11 +346,11 @@ contract HubRemoveTest is HubBase { asset = getAssetPosition(hub1, daiAssetId); // hub - assertApproxEqAbs(asset.addedAmount, 0, 1, 'hub addedAmount'); - assertEq(asset.addedShares, 0, 'hub addedShares'); + assertApproxEqAbs(asset.addedAmount, feeAmount, 1, 'hub addedAmount'); + assertEq(asset.addedShares, feeShares, 'hub addedShares'); assertApproxEqAbs( asset.liquidity, - _calculateBurntInterest(hub1, daiAssetId) + hub1.getAsset(daiAssetId).realizedFees, + feeAmount + _calculateBurntInterest(hub1, daiAssetId), 1, 'dai liquidity' ); diff --git a/tests/unit/Hub/Hub.Rescue.t.sol b/tests/unit/Hub/Hub.Rescue.t.sol index 099b6fd1b..6781ebea7 100644 --- a/tests/unit/Hub/Hub.Rescue.t.sol +++ b/tests/unit/Hub/Hub.Rescue.t.sol @@ -183,7 +183,8 @@ contract HubRescueTest is HubBase { restorer: alice }); - uint256 liquidityFee = hub1.getAssetAccruedFees(daiAssetId); + address feeReceiver = _getFeeReceiver(hub1, daiAssetId); + uint256 liquidityFee = hub1.getSpokeAddedAssets(daiAssetId, feeReceiver); assertGt(liquidityFee, 0); // Cannot add liquidity fee amount without transferring underlying tokens @@ -192,7 +193,11 @@ contract HubRescueTest is HubBase { vm.prank(address(_rescueSpoke)); hub1.add(daiAssetId, liquidityFee); - assertEq(hub1.getAssetAccruedFees(daiAssetId), liquidityFee, 'accrued liquidity fee'); + assertEq( + hub1.getSpokeAddedAssets(daiAssetId, feeReceiver), + liquidityFee, + 'accrued liquidity fee' + ); } function _rescue( diff --git a/tests/unit/Hub/Hub.Restore.t.sol b/tests/unit/Hub/Hub.Restore.t.sol index 8c8f804e3..e17534b6b 100644 --- a/tests/unit/Hub/Hub.Restore.t.sol +++ b/tests/unit/Hub/Hub.Restore.t.sol @@ -708,11 +708,13 @@ contract HubRestoreTest is HubBase { vm.stopPrank(); AssetPosition memory daiData = getAssetPosition(hub1, daiAssetId); + address feeReceiver = _getFeeReceiver(hub1, daiAssetId); // hub assertApproxEqAbs( hub1.getAddedAssets(daiAssetId), hub1.getSpokeAddedAssets(daiAssetId, address(spoke2)) + + hub1.getSpokeAddedAssets(daiAssetId, feeReceiver) + _calculateBurntInterest(hub1, daiAssetId), 1, 'hub dai total addedAmount' @@ -890,6 +892,7 @@ contract HubRestoreTest is HubBase { vm.stopPrank(); AssetPosition memory daiData = getAssetPosition(hub1, daiAssetId); + address feeReceiver = _getFeeReceiver(hub1, daiAssetId); // asset assertEq(daiData.drawn, 0, 'asset drawn'); @@ -899,13 +902,15 @@ contract HubRestoreTest is HubBase { // spoke assertApproxEqAbs( daiData.addedAmount, - hub1.getSpokeAddedAssets(daiAssetId, address(spoke2)), + hub1.getSpokeAddedAssets(daiAssetId, feeReceiver) + + hub1.getSpokeAddedAssets(daiAssetId, address(spoke2)), 1, 'spoke addedAmount' ); assertApproxEqAbs( daiData.addedShares, - hub1.getSpokeAddedShares(daiAssetId, address(spoke2)), + hub1.getSpokeAddedShares(daiAssetId, feeReceiver) + + hub1.getSpokeAddedShares(daiAssetId, address(spoke2)), 1, 'spoke addedShares' ); @@ -992,6 +997,7 @@ contract HubRestoreTest is HubBase { vm.stopPrank(); AssetPosition memory daiData = getAssetPosition(hub1, daiAssetId); + address feeReceiver = _getFeeReceiver(hub1, daiAssetId); // asset assertEq(daiData.drawn, 0, 'asset drawn'); @@ -1001,13 +1007,15 @@ contract HubRestoreTest is HubBase { // spoke assertApproxEqAbs( daiData.addedAmount, - hub1.getSpokeAddedAssets(daiAssetId, address(spoke2)), + hub1.getSpokeAddedAssets(daiAssetId, feeReceiver) + + hub1.getSpokeAddedAssets(daiAssetId, address(spoke2)), 1, 'spoke addedAmount' ); assertApproxEqAbs( daiData.addedShares, - hub1.getSpokeAddedShares(daiAssetId, address(spoke2)), + hub1.getSpokeAddedShares(daiAssetId, feeReceiver) + + hub1.getSpokeAddedShares(daiAssetId, address(spoke2)), 1, 'spoke addedShares' ); diff --git a/tests/unit/Hub/Hub.SpokeConfig.t.sol b/tests/unit/Hub/Hub.SpokeConfig.t.sol index c79d7cc1f..615acb1b8 100644 --- a/tests/unit/Hub/Hub.SpokeConfig.t.sol +++ b/tests/unit/Hub/Hub.SpokeConfig.t.sol @@ -12,44 +12,6 @@ contract HubSpokeConfigTest is HubBase { _addLiquidity(usdxAssetId, MAX_SUPPLY_AMOUNT); } - function test_mintFeeShares_active_halted_scenarios() public { - address feeReceiver = _getFeeReceiver(hub1, usdxAssetId); - - // set spoke to active / halted; reverts - _accrueLiquidityFees(hub1, spoke1, usdxAssetId); - _updateSpokeHalted(hub1, usdxAssetId, feeReceiver, true); - _updateSpokeActive(hub1, usdxAssetId, feeReceiver, true); - - vm.prank(HUB_ADMIN); - hub1.mintFeeShares(usdxAssetId); - - // set spoke to inactive / halted; reverts - _accrueLiquidityFees(hub1, spoke1, usdxAssetId); - _updateSpokeHalted(hub1, usdxAssetId, feeReceiver, true); - _updateSpokeActive(hub1, usdxAssetId, feeReceiver, false); - - vm.expectRevert(IHub.SpokeNotActive.selector); - vm.prank(HUB_ADMIN); - hub1.mintFeeShares(usdxAssetId); - - // set spoke to active / not halted; succeeds - _accrueLiquidityFees(hub1, spoke1, usdxAssetId); - _updateSpokeHalted(hub1, usdxAssetId, feeReceiver, false); - _updateSpokeActive(hub1, usdxAssetId, feeReceiver, true); - - vm.prank(HUB_ADMIN); - hub1.mintFeeShares(usdxAssetId); - - // set spoke to inactive / not halted; reverts - _accrueLiquidityFees(hub1, spoke1, usdxAssetId); - _updateSpokeHalted(hub1, usdxAssetId, feeReceiver, false); - _updateSpokeActive(hub1, usdxAssetId, feeReceiver, false); - - vm.expectRevert(IHub.SpokeNotActive.selector); - vm.prank(HUB_ADMIN); - hub1.mintFeeShares(usdxAssetId); - } - function test_add_active_halted_scenarios() public { // set spoke to active / halted; reverts _updateSpokeHalted(hub1, usdxAssetId, address(spoke1), true); @@ -372,7 +334,7 @@ contract HubSpokeConfigTest is HubBase { skip(365 days); Utils.add(hub, assetId, address(spoke), 1e18, alice); - assertGt(hub.getAsset(assetId).realizedFees, 0); + assertGt(hub.getSpokeAddedAssets(assetId, _getFeeReceiver(hub, assetId)), 0); } function _createReportedDeficit(IHub hub, address spoke, uint256 assetId) internal { diff --git a/tests/unit/Hub/HubAccrueInterest.t.sol b/tests/unit/Hub/HubAccrueInterest.t.sol index 0a5aaa5e0..f2e3a4f8d 100644 --- a/tests/unit/Hub/HubAccrueInterest.t.sol +++ b/tests/unit/Hub/HubAccrueInterest.t.sol @@ -112,11 +112,7 @@ contract HubAccrueInterestTest is Base { assertEq(elapsed, daiInfo.lastUpdateTimestamp - startTime); assertEq(daiInfo.drawnIndex, expectedDrawnIndex1, 'drawnIndex'); - assertEq( - _getAddedAssetsWithFees(hub1, daiAssetId), - addAmount + addAmount2 + interest, - 'addAmount' - ); + assertEq(hub1.getAddedAssets(daiAssetId), addAmount + addAmount2 + interest, 'addAmount'); assertEq(getAssetDrawnDebt(daiAssetId), expectedDrawnDebt1, 'drawn'); startTime = vm.getBlockTimestamp().toUint40(); @@ -141,11 +137,7 @@ contract HubAccrueInterestTest is Base { // Timestamp does not update when no interest accrued assertEq(daiInfo.lastUpdateTimestamp, vm.getBlockTimestamp(), 'lastUpdateTimestamp'); assertEq(daiInfo.drawnIndex, expectedDrawnIndex2, 'drawnIndex2'); - assertEq( - _getAddedAssetsWithFees(hub1, daiAssetId), - addAmount + addAmount2 + interest, - 'addAmount' - ); + assertEq(hub1.getAddedAssets(daiAssetId), addAmount + addAmount2 + interest, 'addAmount'); assertEq(getAssetDrawnDebt(daiAssetId), 0, 'drawn'); // Time passes @@ -158,11 +150,7 @@ contract HubAccrueInterestTest is Base { assertEq(daiInfo.lastUpdateTimestamp, vm.getBlockTimestamp(), 'lastUpdateTimestamp'); assertEq(daiInfo.drawnIndex, expectedDrawnIndex2, 'drawnIndex2'); - assertEq( - _getAddedAssetsWithFees(hub1, daiAssetId), - addAmount + addAmount2 * 2 + interest, - 'addAmount' - ); + assertEq(hub1.getAddedAssets(daiAssetId), addAmount + addAmount2 * 2 + interest, 'addAmount'); assertEq(getAssetDrawnDebt(daiAssetId), 0, 'drawn'); } @@ -198,11 +186,7 @@ contract HubAccrueInterestTest is Base { assertEq(elapsed, daiInfo.lastUpdateTimestamp - startTime); assertEq(daiInfo.drawnIndex, expectedDrawnIndex, 'drawnIndex'); - assertEq( - _getAddedAssetsWithFees(hub1, daiAssetId), - addAmount + addAmount2 + interest, - 'addAmount' - ); + assertEq(hub1.getAddedAssets(daiAssetId), addAmount + addAmount2 + interest, 'addAmount'); assertEq(getAssetDrawnDebt(daiAssetId), expectedDrawnDebt, 'drawn'); } @@ -241,9 +225,10 @@ contract HubAccrueInterestTest is Base { assertEq(elapsed, daiInfo.lastUpdateTimestamp - startTime); assertEq(daiInfo.drawnIndex, expectedDrawnIndex, 'drawnIndex'); - assertEq( - _getAddedAssetsWithFees(hub1, daiAssetId), + assertApproxEqAbs( + hub1.getAddedAssets(daiAssetId), addAmount + addAmount2 + interest, + 1, 'addAmount' ); assertEq(getAssetDrawnDebt(daiAssetId), expectedDrawnDebt, 'drawn'); @@ -293,9 +278,10 @@ contract HubAccrueInterestTest is Base { assertEq(assetData.t1.lastUpdateTimestamp - timestamps.t0, elapsed, 'elapsed'); assertEq(assetData.t1.drawnIndex, cumulated.t1, 'drawnIndex'); - assertEq( - _getAddedAssetsWithFees(hub1, daiAssetId), + assertApproxEqAbs( + hub1.getAddedAssets(daiAssetId), spoke1Amounts.add0 + addAmount2 + interest1, + 1, 'addAmount' ); assertEq(getAssetDrawnDebt(daiAssetId), expectedDrawnDebt1, 'drawn'); @@ -326,9 +312,10 @@ contract HubAccrueInterestTest is Base { assertEq(assetData.t2.lastUpdateTimestamp - timestamps.t1, elapsed, 'elapsed'); assertEq(assetData.t2.drawnIndex, cumulated.t2, 'drawnIndex t2'); - assertEq( - _getAddedAssetsWithFees(hub1, daiAssetId), + assertApproxEqAbs( + hub1.getAddedAssets(daiAssetId), spoke1Amounts.add0 + addAmount2 * 3 + interest1 + interest2, + 2, 'addAmount t2' ); assertEq(getAssetDrawnDebt(daiAssetId), expectedDrawnDebt2, 'drawn t2'); diff --git a/tests/unit/HubConfigurator.t.sol b/tests/unit/HubConfigurator.t.sol index 6f9c87955..e6cf0d5d3 100644 --- a/tests/unit/HubConfigurator.t.sol +++ b/tests/unit/HubConfigurator.t.sol @@ -368,14 +368,13 @@ contract HubConfiguratorTest is HubBase { ); assertGe(treasurySpoke.getSuppliedShares(daiAssetId), 0); + uint256 fees = treasurySpoke.getSuppliedAmount(daiAssetId); // Change the fee receiver TreasurySpoke newTreasurySpoke = new TreasurySpoke(HUB_ADMIN, address(hub1)); vm.prank(HUB_CONFIGURATOR); hubConfigurator.updateFeeReceiver(address(hub1), daiAssetId, address(newTreasurySpoke)); - uint256 fees = treasurySpoke.getSuppliedAmount(daiAssetId); - assertEq( hub1.getAssetConfig(daiAssetId).feeReceiver, address(newTreasurySpoke), @@ -398,7 +397,6 @@ contract HubConfiguratorTest is HubBase { // Accrue more fees, this time to new fee receiver skip(365 days); - Utils.mintFeeShares(hub1, daiAssetId, ADMIN); assertGt( newTreasurySpoke.getSuppliedAmount(daiAssetId), @@ -429,7 +427,6 @@ contract HubConfiguratorTest is HubBase { 100e18, 365 days ); - Utils.mintFeeShares(hub1, daiAssetId, ADMIN); assertGe(treasurySpoke.getSuppliedShares(daiAssetId), 0); uint256 feeShares = treasurySpoke.getSuppliedShares(daiAssetId); @@ -466,7 +463,6 @@ contract HubConfiguratorTest is HubBase { // Accrue more fees, this time to new fee receiver skip(365 days); - Utils.mintFeeShares(hub1, daiAssetId, ADMIN); // Check that new fee receiver is getting the fees, and not old treasury spoke assertGt( diff --git a/tests/unit/Spoke/Spoke.AccrueLiquidityFee.EdgeCases.t.sol b/tests/unit/Spoke/Spoke.AccrueLiquidityFee.EdgeCases.t.sol index 4c142b33c..4b09bad64 100644 --- a/tests/unit/Spoke/Spoke.AccrueLiquidityFee.EdgeCases.t.sol +++ b/tests/unit/Spoke/Spoke.AccrueLiquidityFee.EdgeCases.t.sol @@ -41,7 +41,6 @@ contract SpokeAccrueLiquidityFeeEdgeCasesTest is SpokeBase { Utils.borrow(spoke1, reserveId, alice, borrowAmount, alice); skip(skipTime); - Utils.mintFeeShares(hub1, assetId, ADMIN); (, uint256 premiumDebt) = spoke1.getUserDebt(reserveId, alice); assertGt(premiumDebt, 0); @@ -94,7 +93,6 @@ contract SpokeAccrueLiquidityFeeEdgeCasesTest is SpokeBase { Utils.borrow(spoke1, reserveId, bob, borrowAmount2, bob); skip(skipTime); - Utils.mintFeeShares(hub1, assetId, ADMIN); assertApproxEqAbs( spoke1.getUserSuppliedAssets(reserveId, alice), diff --git a/tests/unit/Spoke/Spoke.AccrueLiquidityFee.t.sol b/tests/unit/Spoke/Spoke.AccrueLiquidityFee.t.sol index 79c2a1f8d..a0f0e890f 100644 --- a/tests/unit/Spoke/Spoke.AccrueLiquidityFee.t.sol +++ b/tests/unit/Spoke/Spoke.AccrueLiquidityFee.t.sol @@ -32,7 +32,7 @@ contract SpokeAccrueLiquidityFeeTest is SpokeBase { vm.recordLogs(); // Bob supplies through spoke 1 Utils.supply(spoke1, daiReserveId, bob, amount, bob); - _assertEventNotEmitted(IHub.MintFeeShares.selector); + _assertEventNotEmitted(IHub.AccrueFees.selector); // Skip time skip(skipTime); @@ -117,7 +117,6 @@ contract SpokeAccrueLiquidityFeeTest is SpokeBase { }) ); - Utils.mintFeeShares(hub1, assetId, ADMIN); assertApproxEqAbs( hub1.getSpokeAddedShares(assetId, address(treasurySpoke)), expectedFeeShares, @@ -155,7 +154,6 @@ contract SpokeAccrueLiquidityFeeTest is SpokeBase { }) ); - Utils.mintFeeShares(hub1, assetId, ADMIN); assertApproxEqAbs( hub1.getSpokeAddedShares(assetId, address(treasurySpoke)), expectedFeeShares, @@ -178,7 +176,6 @@ contract SpokeAccrueLiquidityFeeTest is SpokeBase { // treasury expectedFeeShares = 0; - Utils.mintFeeShares(hub1, assetId, ADMIN); assertApproxEqAbs( hub1.getSpokeAddedShares(assetId, address(treasurySpoke)), expectedFeeShares, @@ -212,7 +209,6 @@ contract SpokeAccrueLiquidityFeeTest is SpokeBase { Utils.borrow(spoke1, reserveId, alice, borrowAmount, alice); skip(365 days); - Utils.mintFeeShares(hub1, assetId, ADMIN); _assertSpokeDebt( spoke1, @@ -238,7 +234,7 @@ contract SpokeAccrueLiquidityFeeTest is SpokeBase { // withdraw any treasury fees to reset counter _withdrawLiquidityFees(hub1, assetId, UINT256_MAX); _assertEventNotEmitted(IHubBase.Add.selector); - _assertEventNotEmitted(IHub.MintFeeShares.selector); + _assertEventNotEmitted(IHub.AccrueFees.selector); expectedDrawnDebtAccrual = 750e18; // 50% of 1500 (drawn debt accrual) expectedDrawnDebt += expectedDrawnDebtAccrual; @@ -246,7 +242,6 @@ contract SpokeAccrueLiquidityFeeTest is SpokeBase { expectedTreasuryFees = 37.5e18; // 5% of 750 (liquidity fee on drawn debt) skip(365 days); - Utils.mintFeeShares(hub1, assetId, ADMIN); _assertSpokeDebt( spoke1, @@ -268,13 +263,13 @@ contract SpokeAccrueLiquidityFeeTest is SpokeBase { vm.recordLogs(); // Bob supplies 1 share to trigger interest accrual with new liquidity fee Utils.supply(spoke1, reserveId, bob, minimumAssetsPerAddedShare(hub1, assetId), bob); - _assertEventNotEmitted(IHub.MintFeeShares.selector); + _assertEventNotEmitted(IHub.AccrueFees.selector); vm.recordLogs(); // withdraw any treasury fees to reset counter _withdrawLiquidityFees(hub1, assetId, UINT256_MAX); _assertEventNotEmitted(IHubBase.Add.selector); - _assertEventNotEmitted(IHub.MintFeeShares.selector); + _assertEventNotEmitted(IHub.AccrueFees.selector); expectedDrawnDebtAccrual = 1125e18; // 50% of 2250 (drawn debt accrual) expectedDrawnDebt += expectedDrawnDebtAccrual; @@ -282,7 +277,6 @@ contract SpokeAccrueLiquidityFeeTest is SpokeBase { expectedTreasuryFees = 0; skip(365 days); - Utils.mintFeeShares(hub1, assetId, ADMIN); _assertSpokeDebt( spoke1, @@ -327,7 +321,6 @@ contract SpokeAccrueLiquidityFeeTest is SpokeBase { assertEq(_getUserRpStored(spoke1, alice), expectedRp); skip(365 days); - Utils.mintFeeShares(hub1, assetId, ADMIN); _assertSpokeDebt( spoke1, @@ -354,7 +347,7 @@ contract SpokeAccrueLiquidityFeeTest is SpokeBase { // withdraw any treasury fees to reset counter _withdrawLiquidityFees(hub1, assetId, UINT256_MAX); _assertEventNotEmitted(IHubBase.Add.selector); - _assertEventNotEmitted(IHub.MintFeeShares.selector); + _assertEventNotEmitted(IHub.AccrueFees.selector); expectedDrawnDebtAccrual = expectedDrawnDebt.percentMulUp(rate); expectedDrawnDebt += expectedDrawnDebtAccrual; @@ -362,7 +355,6 @@ contract SpokeAccrueLiquidityFeeTest is SpokeBase { expectedTreasuryFees = expectedDrawnDebtAccrual.percentMulUp(liquidityFee); skip(365 days); - Utils.mintFeeShares(hub1, assetId, ADMIN); _assertSpokeDebt( spoke1, @@ -384,13 +376,13 @@ contract SpokeAccrueLiquidityFeeTest is SpokeBase { vm.recordLogs(); // Bob supplies 1 share to trigger interest accrual with new liquidity fee Utils.supply(spoke1, reserveId, bob, minimumAssetsPerAddedShare(hub1, assetId), bob); - _assertEventNotEmitted(IHub.MintFeeShares.selector); + _assertEventNotEmitted(IHub.AccrueFees.selector); vm.recordLogs(); // withdraw any treasury fees to reset counter _withdrawLiquidityFees(hub1, assetId, UINT256_MAX); _assertEventNotEmitted(IHubBase.Add.selector); - _assertEventNotEmitted(IHub.MintFeeShares.selector); + _assertEventNotEmitted(IHub.AccrueFees.selector); expectedDrawnDebtAccrual = expectedDrawnDebt.percentMulUp(rate); expectedDrawnDebt += expectedDrawnDebtAccrual; @@ -450,7 +442,6 @@ contract SpokeAccrueLiquidityFeeTest is SpokeBase { assertEq(_getUserRpStored(spoke1, alice), expectedRp); skip(365 days); - Utils.mintFeeShares(hub1, assetId, ADMIN); _assertSpokeDebt( spoke1, @@ -470,8 +461,6 @@ contract SpokeAccrueLiquidityFeeTest is SpokeBase { spoke1.setUsingAsCollateral(reserveId, false, alice); assertEq(_getUserRpStored(spoke1, alice), 50_00); - Utils.mintFeeShares(hub1, assetId, ADMIN); - // no change in treasury fees _assertSpokeDebt( spoke1, @@ -514,7 +503,6 @@ contract SpokeAccrueLiquidityFeeTest is SpokeBase { Utils.borrow(spoke1, reserveId, alice, borrowAmount, alice); skip(365 days); - Utils.mintFeeShares(hub1, assetId, ADMIN); _assertSpokeDebt( spoke1, diff --git a/tests/unit/Spoke/Spoke.Withdraw.Scenario.t.sol b/tests/unit/Spoke/Spoke.Withdraw.Scenario.t.sol index 1cf08b042..343c7e8f0 100644 --- a/tests/unit/Spoke/Spoke.Withdraw.Scenario.t.sol +++ b/tests/unit/Spoke/Spoke.Withdraw.Scenario.t.sol @@ -13,7 +13,6 @@ contract SpokeWithdrawScenarioTest is SpokeBase { uint256 stage; uint256 sharePrecision; uint256 repayAmount; - uint256 expectedFeeAmount; uint256 addExRate; } @@ -81,8 +80,10 @@ contract SpokeWithdrawScenarioTest is SpokeBase { onBehalfOf: bob }); + uint256 treasuryFees = hub1.getSpokeAddedAssets(daiAssetId, address(treasurySpoke)); uint256 interestAccrued = hub1.getAddedAssets(daiAssetId) - _calculateBurntInterest(hub1, daiAssetId) - + treasuryFees - supplyAmount; uint256 totalSupplied = interestAccrued + supplyAmount; assertApproxEqAbs( @@ -98,8 +99,10 @@ contract SpokeWithdrawScenarioTest is SpokeBase { // Withdraw partial supplied assets Utils.withdraw(spoke1, _daiReserveId(spoke1), bob, partialWithdrawAmount, bob); + treasuryFees = hub1.getSpokeAddedAssets(daiAssetId, address(treasurySpoke)); interestAccrued = hub1.getAddedAssets(daiAssetId) - + treasuryFees - _calculateBurntInterest(hub1, daiAssetId) - (supplyAmount - partialWithdrawAmount); @@ -120,6 +123,9 @@ contract SpokeWithdrawScenarioTest is SpokeBase { // Withdraw all supplied assets Utils.withdraw(spoke1, _daiReserveId(spoke1), bob, UINT256_MAX, bob); + // treasury spoke withdraw fees + _withdrawLiquidityFees(hub1, daiAssetId, UINT256_MAX); + _checkSuppliedAmounts(daiAssetId, _daiReserveId(spoke1), spoke1, bob, 0, 'after withdraw'); // Check supply rate monotonically increasing after withdraw @@ -199,8 +205,6 @@ contract SpokeWithdrawScenarioTest is SpokeBase { // accrue interest skip(params.skipTime[0]); - uint256 expectedFeeAmount = _getExpectedFeeReceiverAddedAssets(hub1, params.reserveId); - // carol repays all with interest state.repayAmount = spoke1.getUserTotalDebt(params.reserveId, carol); // deal in case carol's repayAmount exceeds default supplied amount due to interest @@ -208,8 +212,6 @@ contract SpokeWithdrawScenarioTest is SpokeBase { vm.prank(carol); spoke1.repay(params.reserveId, state.repayAmount, carol); - assertEq(hub1.getAsset(params.reserveId).realizedFees, expectedFeeAmount, 'realized fees'); - TestData[3] memory reserveData; TestUserData[3] memory aliceData; TestUserData[3] memory bobData; @@ -268,6 +270,8 @@ contract SpokeWithdrawScenarioTest is SpokeBase { }); _checkSupplyRateIncreasing(state.addExRate, getAddExRate(state.assetId), 'after bob withdraw'); + // treasury spoke withdraw fees + _withdrawLiquidityFees(hub1, state.assetId, UINT256_MAX); state.stage = 2; reserveData[state.stage] = loadReserveInfo(spoke1, params.reserveId); @@ -305,7 +309,7 @@ contract SpokeWithdrawScenarioTest is SpokeBase { assertEq(tokenData[state.stage].spokeBalance, 0, 'tokenData spoke balance'); assertEq( tokenData[state.stage].hubBalance, - _calculateBurntInterest(hub1, state.assetId) + hub1.getAsset(state.assetId).realizedFees, + _calculateExpectedDustAfterFullWithdraw(hub1, state.assetId), 'tokenData hub balance' ); assertEq( diff --git a/tests/unit/Spoke/Spoke.Withdraw.t.sol b/tests/unit/Spoke/Spoke.Withdraw.t.sol index 92cc368f7..723c18a98 100644 --- a/tests/unit/Spoke/Spoke.Withdraw.t.sol +++ b/tests/unit/Spoke/Spoke.Withdraw.t.sol @@ -24,7 +24,6 @@ contract SpokeWithdrawTest is SpokeBase { uint256 alicePremiumDebt; uint256 borrowReserveSupplyAmount; uint256 addExRate; - uint256 expectedFeeAmount; } struct TestWithInterestFuzzParams { @@ -332,8 +331,6 @@ contract SpokeWithdrawTest is SpokeBase { // Wait a year to accrue interest skip(365 days); - uint256 expectedFeeAmount = _calcUnrealizedFees(hub1, daiAssetId); - // Ensure interest has accrued vm.assume(hub1.getAddedAssets(daiAssetId) > supplyAmount); @@ -349,8 +346,6 @@ contract SpokeWithdrawTest is SpokeBase { onBehalfOf: bob }); - assertEq(hub1.getAsset(daiAssetId).realizedFees, expectedFeeAmount, 'realized fees'); - uint256 addExRate = getAddExRate(daiAssetId); uint256 expectedAssets = spoke1.getUserSuppliedAssets(_daiReserveId(spoke1), bob); @@ -368,6 +363,9 @@ contract SpokeWithdrawTest is SpokeBase { assertEq(returnValues.amount, expectedAssets); assertEq(returnValues.shares, expectedShares); + // treasury spoke withdraw fees + _withdrawLiquidityFees(hub1, daiAssetId, UINT256_MAX); + _checkSuppliedAmounts(daiAssetId, _daiReserveId(spoke1), spoke1, bob, 0, 'after withdraw'); _checkSupplyRateIncreasing(addExRate, getAddExRate(daiAssetId), 'after withdraw'); _assertHubLiquidity(hub1, daiAssetId, 'spoke1.withdraw'); @@ -443,6 +441,8 @@ contract SpokeWithdrawTest is SpokeBase { assertEq(returnValues.amount, expectedAssets); assertEq(returnValues.shares, expectedShares); + _withdrawLiquidityFees(hub1, daiAssetId, UINT256_MAX); + _checkSuppliedAmounts(daiAssetId, _daiReserveId(spoke1), spoke1, bob, 0, 'after withdraw'); _checkSupplyRateIncreasing(addExRate, getAddExRate(daiAssetId), 'after withdraw'); _assertHubLiquidity(hub1, daiAssetId, 'spoke1.withdraw'); @@ -463,8 +463,6 @@ contract SpokeWithdrawTest is SpokeBase { state.borrowReserveSupplyAmount ) = _increaseReserveIndex(spoke1, state.reserveId); - state.expectedFeeAmount = _calcUnrealizedFees(hub1, daiAssetId); - (state.aliceDrawnDebt, state.alicePremiumDebt) = spoke1.getUserDebt(state.reserveId, alice); assertEq(state.alicePremiumDebt, 0, 'alice has no premium contribution to exchange rate'); @@ -510,7 +508,8 @@ contract SpokeWithdrawTest is SpokeBase { onBehalfOf: bob }); - assertEq(hub1.getAsset(daiAssetId).realizedFees, state.expectedFeeAmount, 'realized fees'); + // treasury spoke withdraw fees + _withdrawLiquidityFees(hub1, daiAssetId, UINT256_MAX); stage = 2; reserveData[stage] = loadReserveInfo(spoke1, state.reserveId); @@ -544,7 +543,7 @@ contract SpokeWithdrawTest is SpokeBase { assertEq(tokenData[stage].spokeBalance, 0, 'tokenData spoke balance'); assertEq( tokenData[stage].hubBalance, - _calculateBurntInterest(hub1, daiAssetId) + hub1.getAsset(daiAssetId).realizedFees, + _calculateExpectedDustAfterFullWithdraw(hub1, daiAssetId), 'tokenData hub balance' ); assertEq( @@ -619,8 +618,6 @@ contract SpokeWithdrawTest is SpokeBase { skipTime: params.skipTime }); - state.expectedFeeAmount = _calcUnrealizedFees(hub1, wbtcAssetId); - uint256 repayAmount = spoke1.getUserTotalDebt(state.reserveId, alice); // deal because repayAmount may exceed default supplied amount due to interest deal(address(underlying), alice, repayAmount); @@ -632,8 +629,6 @@ contract SpokeWithdrawTest is SpokeBase { // alice repays all with interest Utils.repay(spoke1, state.reserveId, alice, repayAmount, alice); - assertEq(hub1.getAsset(wbtcAssetId).realizedFees, state.expectedFeeAmount, 'realized fees'); - // number of test stages TestData[3] memory reserveData; TestUserData[3] memory aliceData; @@ -671,6 +666,9 @@ contract SpokeWithdrawTest is SpokeBase { onBehalfOf: bob }); + // treasury spoke withdraw fees + _withdrawLiquidityFees(hub1, assetId, UINT256_MAX); + stage = 2; reserveData[stage] = loadReserveInfo(spoke1, state.reserveId); aliceData[stage] = loadUserInfo(spoke1, state.reserveId, alice); @@ -712,7 +710,7 @@ contract SpokeWithdrawTest is SpokeBase { assertEq(tokenData[stage].spokeBalance, 0, 'tokenData spoke balance'); assertEq( tokenData[stage].hubBalance, - _calculateBurntInterest(hub1, assetId) + hub1.getAsset(assetId).realizedFees, + _calculateExpectedDustAfterFullWithdraw(hub1, assetId), 'tokenData hub balance' ); assertEq(underlying.balanceOf(alice), 0, 'alice balance'); @@ -747,8 +745,6 @@ contract SpokeWithdrawTest is SpokeBase { state.borrowReserveSupplyAmount ) = _increaseReserveIndex(spoke1, state.reserveId); - state.expectedFeeAmount = _calcUnrealizedFees(hub1, daiAssetId); - (, state.alicePremiumDebt) = spoke1.getUserDebt(state.reserveId, alice); assertGt(state.alicePremiumDebt, 0, 'alice has premium contribution to exchange rate'); @@ -757,8 +753,6 @@ contract SpokeWithdrawTest is SpokeBase { uint256 repayAmount = spoke1.getUserTotalDebt(state.reserveId, alice); Utils.repay(spoke1, state.reserveId, alice, repayAmount, alice); - assertEq(hub1.getAsset(daiAssetId).realizedFees, state.expectedFeeAmount, 'realized fees'); - uint256 stage = 0; reserveData[stage] = loadReserveInfo(spoke1, state.reserveId); aliceData[stage] = loadUserInfo(spoke1, state.reserveId, alice); @@ -790,6 +784,9 @@ contract SpokeWithdrawTest is SpokeBase { onBehalfOf: bob }); + // treasury spoke withdraw fees + _withdrawLiquidityFees(hub1, daiAssetId, UINT256_MAX); + stage = 2; reserveData[stage] = loadReserveInfo(spoke1, state.reserveId); aliceData[stage] = loadUserInfo(spoke1, state.reserveId, alice); @@ -825,7 +822,7 @@ contract SpokeWithdrawTest is SpokeBase { assertEq(tokenData[stage].spokeBalance, 0, 'tokenData spoke balance'); assertEq( tokenData[stage].hubBalance, - _calculateBurntInterest(hub1, daiAssetId) + hub1.getAsset(daiAssetId).realizedFees, + _calculateExpectedDustAfterFullWithdraw(hub1, daiAssetId), 'tokenData hub balance' ); assertEq( @@ -894,8 +891,6 @@ contract SpokeWithdrawTest is SpokeBase { skipTime: params.skipTime }); - state.expectedFeeAmount = _calcUnrealizedFees(hub1, assetId); - // repay all debt with interest uint256 repayAmount = spoke1.getUserTotalDebt(state.reserveId, alice); deal(address(underlying), alice, repayAmount); @@ -905,8 +900,6 @@ contract SpokeWithdrawTest is SpokeBase { Utils.repay(spoke1, state.reserveId, alice, repayAmount, alice); - assertEq(hub1.getAsset(assetId).realizedFees, state.expectedFeeAmount, 'realized fees'); - // number of test stages TestData[3] memory reserveData; TestUserData[3] memory aliceData; @@ -946,6 +939,9 @@ contract SpokeWithdrawTest is SpokeBase { onBehalfOf: bob }); + // treasury spoke withdraw fees + _withdrawLiquidityFees(hub1, assetId, UINT256_MAX); + stage = 2; reserveData[stage] = loadReserveInfo(spoke1, state.reserveId); aliceData[stage] = loadUserInfo(spoke1, state.reserveId, alice); @@ -987,7 +983,7 @@ contract SpokeWithdrawTest is SpokeBase { assertEq(tokenData[stage].spokeBalance, 0, 'tokenData spoke balance'); assertEq( tokenData[stage].hubBalance, - _calculateBurntInterest(hub1, assetId) + hub1.getAsset(assetId).realizedFees, + _calculateExpectedDustAfterFullWithdraw(hub1, assetId), 'tokenData hub balance' ); assertEq(underlying.balanceOf(alice), 0, 'alice balance'); diff --git a/tests/unit/Spoke/TreasurySpoke.t.sol b/tests/unit/Spoke/TreasurySpoke.t.sol index 62263c032..de3abae48 100644 --- a/tests/unit/Spoke/TreasurySpoke.t.sol +++ b/tests/unit/Spoke/TreasurySpoke.t.sol @@ -93,24 +93,11 @@ contract TreasurySpokeTest is SpokeBase { _openDebtPosition(spoke1, getReserveIdByAssetId(spoke1, hub1, daiAssetId), 100e18, true); skip(365 days); - assertEq(hub1.getAsset(daiAssetId).realizedFees, 0, 'fees'); // fees not yet accrued - uint256 expectedFeeAmount = _calcUnrealizedFees(hub1, daiAssetId); - Utils.mintFeeShares(hub1, daiAssetId, ADMIN); + assertGe(treasurySpoke.getSuppliedShares(daiAssetId), 0); + uint256 fees = treasurySpoke.getSuppliedAmount(daiAssetId); - assertEq(hub1.getAsset(daiAssetId).realizedFees, 0, 'realized fees after minting'); - assertGe( - treasurySpoke.getSuppliedShares(daiAssetId), - hub1.previewAddByAssets(daiAssetId, expectedFeeAmount) - ); - - Utils.withdraw( - _treasurySpoke(), - daiAssetId, - TREASURY_ADMIN, - UINT256_MAX, - address(treasurySpoke) - ); + Utils.withdraw(_treasurySpoke(), daiAssetId, TREASURY_ADMIN, fees, address(treasurySpoke)); } /// treasury supplies to earn interest and fees @@ -202,15 +189,8 @@ contract TreasurySpokeTest is SpokeBase { address tempUser = _openDebtPosition(spoke1, reserveId, amount, true); skip(skipTime); - assertEq(hub1.getAsset(assetId).realizedFees, 0, 'fees'); // fees not yet accrued - - uint256 expectedFeeAmount = _calcUnrealizedFees(hub1, assetId); - - Utils.mintFeeShares(hub1, assetId, ADMIN); uint256 fees = treasurySpoke.getSuppliedAmount(assetId); - assertEq(fees, expectedFeeAmount, 'supplied amount of fees'); - assertEq(hub1.getAsset(assetId).realizedFees, 0, 'realized fees after minting'); assertApproxEqAbs( hub1.getSpokeAddedAssets(assetId, address(treasurySpoke)), hub1.getAssetTotalOwed(assetId) - amount, diff --git a/tests/unit/libraries/LiquidationLogic/LiquidationLogic.LiquidateCollateral.t.sol b/tests/unit/libraries/LiquidationLogic/LiquidationLogic.LiquidateCollateral.t.sol index 9ad955e24..0e40e0657 100644 --- a/tests/unit/libraries/LiquidationLogic/LiquidationLogic.LiquidateCollateral.t.sol +++ b/tests/unit/libraries/LiquidationLogic/LiquidationLogic.LiquidateCollateral.t.sol @@ -85,6 +85,7 @@ contract LiquidationLogicLiquidateCollateralTest is LiquidationLogicBaseTest, Hu params.sharesToLiquidator = bound(sharesToLiquidator, 0, params.sharesToLiquidate); uint256 initialHubBalance = asset.balanceOf(address(hub)); + uint256 initialTreasuryShares = hub.getSpokeAddedShares(assetId, address(treasurySpoke)); uint256 expectedAmountToLiquidator; if (!params.receiveShares) { expectedAmountToLiquidator = hub.previewRemoveByShares(assetId, params.sharesToLiquidator); @@ -118,7 +119,7 @@ contract LiquidationLogicLiquidateCollateralTest is LiquidationLogicBaseTest, Hu assertEq(asset.balanceOf(address(hub)), initialHubBalance - expectedAmountToLiquidator); assertEq( - hub.getSpokeAddedShares(assetId, address(treasurySpoke)), + hub.getSpokeAddedShares(assetId, address(treasurySpoke)) - initialTreasuryShares, params.sharesToLiquidate - params.sharesToLiquidator ); }