From 3bd1896dcfbd9b6eb2393235c9e20861e36aa5a6 Mon Sep 17 00:00:00 2001 From: Oleksii Matiiasevych Date: Sat, 15 Nov 2025 00:04:25 +0700 Subject: [PATCH 1/9] Apply audit fixes 1 --- contracts/LiquidityPool.sol | 1 + contracts/LiquidityPoolAave.sol | 1 - contracts/PublicLiquidityPool.sol | 6 ++++-- contracts/Rebalancer.sol | 12 ++++++------ contracts/Repayer.sol | 2 +- test/LiquidityPool.ts | 6 ++++++ test/LiquidityPoolAave.ts | 6 ++++++ test/LiquidityPoolAaveLongTerm.ts | 6 ++++++ test/LiquidityPoolStablecoin.ts | 6 ++++++ test/PublicLiquidityPool.ts | 7 +++++++ 10 files changed, 43 insertions(+), 10 deletions(-) diff --git a/contracts/LiquidityPool.sol b/contracts/LiquidityPool.sol index 492f9c2..ff0c3f8 100644 --- a/contracts/LiquidityPool.sol +++ b/contracts/LiquidityPool.sol @@ -315,6 +315,7 @@ contract LiquidityPool is ILiquidityPool, AccessControl, EIP712, ISigner { } function setMPCAddress(address mpcAddress_) external onlyRole(DEFAULT_ADMIN_ROLE) { + require(mpcAddress_ != address(0), ZeroAddress()); address oldMPCAddress = mpcAddress; mpcAddress = mpcAddress_; emit MPCAddressSet(oldMPCAddress, mpcAddress_); diff --git a/contracts/LiquidityPoolAave.sol b/contracts/LiquidityPoolAave.sol index f5ab032..56de862 100644 --- a/contracts/LiquidityPoolAave.sol +++ b/contracts/LiquidityPoolAave.sol @@ -43,7 +43,6 @@ contract LiquidityPoolAave is LiquidityPool { error NothingToRepay(); error CollateralNotSupported(); error CannotWithdrawAToken(); - error InvalidLength(); event SuppliedToAave(uint256 amount); event BorrowTokenLTVSet(address token, uint256 oldLTV, uint256 newLTV); diff --git a/contracts/PublicLiquidityPool.sol b/contracts/PublicLiquidityPool.sol index 2b0d46a..360e04c 100644 --- a/contracts/PublicLiquidityPool.sol +++ b/contracts/PublicLiquidityPool.sol @@ -149,10 +149,12 @@ contract PublicLiquidityPool is LiquidityPool, ERC4626 { uint256 totalBalance = token.balanceOf(address(this)); if (token == ASSETS) { uint256 profit = protocolFee; + uint256 virtualBalance = _virtualBalance; protocolFee = 0; - if (totalBalance > _virtualBalance) { + _virtualBalance = (virtualBalance - profit).toUint128(); + if (totalBalance > virtualBalance) { // In case there are donations sent to the pool. - profit += totalBalance - _virtualBalance; + profit += totalBalance - virtualBalance; } return profit; } diff --git a/contracts/Rebalancer.sol b/contracts/Rebalancer.sol index 46c7c9a..b3e3b24 100644 --- a/contracts/Rebalancer.sol +++ b/contracts/Rebalancer.sol @@ -71,9 +71,9 @@ contract Rebalancer is IRebalancer, AccessControlUpgradeable, CCTPAdapter { function initialize( address admin, address rebalancer, - address[] memory pools, - Domain[] memory domains, - Provider[] memory providers + address[] calldata pools, + Domain[] calldata domains, + Provider[] calldata providers ) external initializer() { _grantRole(DEFAULT_ADMIN_ROLE, admin); _grantRole(REBALANCER_ROLE, rebalancer); @@ -90,9 +90,9 @@ contract Rebalancer is IRebalancer, AccessControlUpgradeable, CCTPAdapter { } function _setRoute( - address[] memory pools, - Domain[] memory domains, - Provider[] memory providers, + address[] calldata pools, + Domain[] calldata domains, + Provider[] calldata providers, bool isAllowed ) internal { RebalancerStorage storage $ = _getStorage(); diff --git a/contracts/Repayer.sol b/contracts/Repayer.sol index 3a16b90..d5dc35a 100644 --- a/contracts/Repayer.sol +++ b/contracts/Repayer.sol @@ -67,7 +67,6 @@ contract Repayer is error ZeroAmount(); error InsufficientBalance(); error RouteDenied(); - error InvalidRoute(); error InvalidToken(); error UnsupportedProvider(); error InvalidPoolAssets(); @@ -97,6 +96,7 @@ contract Repayer is DOMAIN = localDomain; ASSETS = assets; WRAPPED_NATIVE_TOKEN = IWrappedNativeToken(wrappedNativeToken); + _disableInitializers(); } receive() external payable { diff --git a/test/LiquidityPool.ts b/test/LiquidityPool.ts index 168c9f1..85c6263 100644 --- a/test/LiquidityPool.ts +++ b/test/LiquidityPool.ts @@ -1773,6 +1773,12 @@ describe("LiquidityPool", function () { .to.be.revertedWithCustomError(liquidityPool, "AccessControlUnauthorizedAccount"); }); + it("Should NOT allow admin to set MPC address to 0", async function () { + const {liquidityPool, admin} = await loadFixture(deployAll); + await expect(liquidityPool.connect(admin).setMPCAddress(ZERO_ADDRESS)) + .to.be.revertedWithCustomError(liquidityPool, "ZeroAddress()"); + }); + it("Should allow admin to set signer address", async function () { const {liquidityPool, admin, user} = await loadFixture(deployAll); const oldSignerAddress = await liquidityPool.signerAddress(); diff --git a/test/LiquidityPoolAave.ts b/test/LiquidityPoolAave.ts index aa9ea24..9f399d0 100644 --- a/test/LiquidityPoolAave.ts +++ b/test/LiquidityPoolAave.ts @@ -3072,6 +3072,12 @@ describe("LiquidityPoolAave", function () { .to.be.revertedWithCustomError(liquidityPool, "AccessControlUnauthorizedAccount"); }); + it("Should NOT allow admin to set MPC address to 0", async function () { + const {liquidityPool, admin} = await loadFixture(deployAll); + await expect(liquidityPool.connect(admin).setMPCAddress(ZERO_ADDRESS)) + .to.be.revertedWithCustomError(liquidityPool, "ZeroAddress()"); + }); + it("Should allow admin to set default token LTV", async function () { const {liquidityPool, admin} = await loadFixture(deployAll); const oldDefaultLTV = await liquidityPool.defaultLTV(); diff --git a/test/LiquidityPoolAaveLongTerm.ts b/test/LiquidityPoolAaveLongTerm.ts index 05b1606..f93cfc2 100644 --- a/test/LiquidityPoolAaveLongTerm.ts +++ b/test/LiquidityPoolAaveLongTerm.ts @@ -4069,6 +4069,12 @@ describe("LiquidityPoolAaveLongTerm", function () { .to.be.revertedWithCustomError(liquidityPool, "AccessControlUnauthorizedAccount"); }); + it("Should NOT allow admin to set MPC address to 0", async function () { + const {liquidityPool, admin} = await loadFixture(deployAll); + await expect(liquidityPool.connect(admin).setMPCAddress(ZERO_ADDRESS)) + .to.be.revertedWithCustomError(liquidityPool, "ZeroAddress()"); + }); + it("Should allow admin to set default token LTV", async function () { const {liquidityPool, admin} = await loadFixture(deployAll); const oldDefaultLTV = await liquidityPool.defaultLTV(); diff --git a/test/LiquidityPoolStablecoin.ts b/test/LiquidityPoolStablecoin.ts index 729cbba..f40a328 100644 --- a/test/LiquidityPoolStablecoin.ts +++ b/test/LiquidityPoolStablecoin.ts @@ -1924,6 +1924,12 @@ describe("LiquidityPoolStablecoin", function () { .to.be.revertedWithCustomError(liquidityPool, "AccessControlUnauthorizedAccount"); }); + it("Should NOT allow admin to set MPC address to 0", async function () { + const {liquidityPool, admin} = await loadFixture(deployAll); + await expect(liquidityPool.connect(admin).setMPCAddress(ZERO_ADDRESS)) + .to.be.revertedWithCustomError(liquidityPool, "ZeroAddress()"); + }); + it("Should allow WITHDRAW_PROFIT_ROLE to pause and unpause borrowing", async function () { const {liquidityPool, withdrawProfit} = await loadFixture(deployAll); expect(await liquidityPool.borrowPaused()) diff --git a/test/PublicLiquidityPool.ts b/test/PublicLiquidityPool.ts index e88a10b..472dd74 100644 --- a/test/PublicLiquidityPool.ts +++ b/test/PublicLiquidityPool.ts @@ -1441,6 +1441,7 @@ describe("PublicLiquidityPool", function () { await liquidityPool.connect(withdrawProfit).withdrawProfit([usdc], withdrawProfit); expect(await usdc.balanceOf(liquidityPool)).to.eq(0); expect(await usdc.balanceOf(withdrawProfit)).to.eq(protocolFee); + expect(await liquidityPool.totalAssets()).to.eq(0); }); it("Should NOT withdraw liquidity if the contract is paused", async function () { @@ -1544,6 +1545,12 @@ describe("PublicLiquidityPool", function () { .to.be.revertedWithCustomError(liquidityPool, "AccessControlUnauthorizedAccount"); }); + it("Should NOT allow admin to set MPC address to 0", async function () { + const {liquidityPool, admin} = await loadFixture(deployAll); + await expect(liquidityPool.connect(admin).setMPCAddress(ZERO_ADDRESS)) + .to.be.revertedWithCustomError(liquidityPool, "ZeroAddress()"); + }); + it("Should allow admin to set signer address", async function () { const {liquidityPool, admin, user} = await loadFixture(deployAll); const oldSignerAddress = await liquidityPool.signerAddress(); From 8e051799c4ddacc1755a1ec5965f2976dc63bdce Mon Sep 17 00:00:00 2001 From: Oleksii Matiiasevych Date: Tue, 25 Nov 2025 22:33:41 +0700 Subject: [PATCH 2/9] Add various fixes --- contracts/ERC4626Adapter.sol | 9 +- contracts/Repayer.sol | 9 +- contracts/utils/AcrossAdapter.sol | 2 +- contracts/utils/AdapterHelper.sol | 9 + contracts/utils/CCTPAdapter.sol | 2 +- .../utils/OptimismStandardBridgeAdapter.sol | 6 +- contracts/utils/StargateAdapter.sol | 1 + test/ERC4626Adapter.ts | 68 ++++++++ test/Repayer.ts | 155 ++++++++++++++++++ 9 files changed, 250 insertions(+), 11 deletions(-) diff --git a/contracts/ERC4626Adapter.sol b/contracts/ERC4626Adapter.sol index ed1ec65..3c3466d 100644 --- a/contracts/ERC4626Adapter.sol +++ b/contracts/ERC4626Adapter.sol @@ -118,10 +118,11 @@ contract ERC4626Adapter is ILiquidityPoolBase, AccessControl { uint256 localBalance = token.balanceOf(address(this)); if (token == ASSETS) { uint256 deposited = totalDeposited; - uint256 vaultBalance = TARGET_VAULT.maxWithdraw(address(this)); - if (vaultBalance < deposited) return localBalance; - uint256 profit = vaultBalance - deposited; - TARGET_VAULT.withdraw(profit, address(this), address(this)); + uint256 depositedShares = TARGET_VAULT.previewWithdraw(deposited); + uint256 totalShares = TARGET_VAULT.balanceOf(address(this)); + if (totalShares <= depositedShares) return localBalance; + uint256 profit = TARGET_VAULT.redeem(totalShares - depositedShares, address(this), address(this)); + assert(TARGET_VAULT.maxWithdraw(address(this)) >= deposited); return profit + localBalance; } return localBalance; diff --git a/contracts/Repayer.sol b/contracts/Repayer.sol index d5dc35a..091e503 100644 --- a/contracts/Repayer.sol +++ b/contracts/Repayer.sol @@ -129,6 +129,7 @@ contract Repayer is /// @notice If the selected provider requires native currency payment to cover fees, /// then caller has to include it in the transaction. It is then the responsibility /// of the Adapter to forward the payment and return any change back to the caller. + /// @dev Adapters are responsible for revoking unused allowance if necessary. function initiateRepay( IERC20 token, uint256 amount, @@ -150,14 +151,14 @@ contract Repayer is RepayerStorage storage $ = _getStorage(); + if (!$.poolSupportsAllTokens[destinationPool]) { + require(token == ASSETS, InvalidToken()); + } + if (provider == Provider.LOCAL) { // This should always pass because isRouteAllowed check will fail earlier. // It is put here for explicitness. require(destinationDomain == DOMAIN, UnsupportedDomain()); - - if (!$.poolSupportsAllTokens[destinationPool]) { - require(token == ASSETS, InvalidToken()); - } // For local we proceed to the process right away. _processRepayLOCAL(token, amount, destinationPool); } else diff --git a/contracts/utils/AcrossAdapter.sol b/contracts/utils/AcrossAdapter.sol index a681fb7..c036b96 100644 --- a/contracts/utils/AcrossAdapter.sol +++ b/contracts/utils/AcrossAdapter.sol @@ -23,7 +23,7 @@ abstract contract AcrossAdapter is AdapterHelper { address destinationPool, Domain destinationDomain, bytes calldata extraData - ) internal { + ) internal notPayable { require(address(ACROSS_SPOKE_POOL) != address(0), ZeroAddress()); token.forceApprove(address(ACROSS_SPOKE_POOL), amount); ( diff --git a/contracts/utils/AdapterHelper.sol b/contracts/utils/AdapterHelper.sol index b29b14a..db7e03f 100644 --- a/contracts/utils/AdapterHelper.sol +++ b/contracts/utils/AdapterHelper.sol @@ -5,6 +5,12 @@ import {IRoute} from ".././interfaces/IRoute.sol"; abstract contract AdapterHelper is IRoute { error SlippageTooHigh(); + error NotPayable(); + + modifier notPayable() { + require(msg.value == 0, NotPayable()); + _; + } function domainChainId(Domain destinationDomain) public pure virtual returns (uint32) { if (destinationDomain == Domain.ETHEREUM) { @@ -25,6 +31,9 @@ abstract contract AdapterHelper is IRoute { if (destinationDomain == Domain.POLYGON_MAINNET) { return 137; } else + if (destinationDomain == Domain.UNICHAIN) { + return 130; + } else if (destinationDomain == Domain.BSC) { return 56; } else diff --git a/contracts/utils/CCTPAdapter.sol b/contracts/utils/CCTPAdapter.sol index fdf2336..b1bba97 100644 --- a/contracts/utils/CCTPAdapter.sol +++ b/contracts/utils/CCTPAdapter.sol @@ -28,7 +28,7 @@ abstract contract CCTPAdapter is AdapterHelper { uint256 amount, address destinationPool, Domain destinationDomain - ) internal { + ) internal notPayable { token.forceApprove(address(CCTP_TOKEN_MESSENGER), amount); CCTP_TOKEN_MESSENGER.depositForBurnWithCaller( amount, diff --git a/contracts/utils/OptimismStandardBridgeAdapter.sol b/contracts/utils/OptimismStandardBridgeAdapter.sol index a3988fc..28b2c49 100644 --- a/contracts/utils/OptimismStandardBridgeAdapter.sol +++ b/contracts/utils/OptimismStandardBridgeAdapter.sol @@ -28,13 +28,16 @@ abstract contract OptimismStandardBridgeAdapter is AdapterHelper { Domain destinationDomain, bytes calldata extraData, Domain localDomain - ) internal { + ) internal notPayable{ // We are only interested in fast L1->L2 bridging, because the reverse is slow. require( localDomain == Domain.ETHEREUM && destinationDomain == Domain.OP_MAINNET, UnsupportedDomain() ); require(address(OPTIMISM_STANDARD_BRIDGE) != address(0), ZeroAddress()); + // WARNING: Contract doesn't maintain an input/output token mapping which could result in a mismatch. + // If the output token is wrong then the input tokens will be lost. + // Notice: In case of minGasLimit being too low, the message could be retried on the destination chain. (address outputToken, uint32 minGasLimit) = abi.decode(extraData, (address, uint32)); if (token == WRAPPED_NATIVE_TOKEN) { WRAPPED_NATIVE_TOKEN.withdraw(amount); @@ -42,6 +45,7 @@ abstract contract OptimismStandardBridgeAdapter is AdapterHelper { return; } + require(outputToken != address(0), ZeroAddress()); token.forceApprove(address(OPTIMISM_STANDARD_BRIDGE), amount); OPTIMISM_STANDARD_BRIDGE.bridgeERC20To( address(token), diff --git a/contracts/utils/StargateAdapter.sol b/contracts/utils/StargateAdapter.sol index 43ef87b..a6c8f0d 100644 --- a/contracts/utils/StargateAdapter.sol +++ b/contracts/utils/StargateAdapter.sol @@ -94,6 +94,7 @@ abstract contract StargateAdapter is AdapterHelper { oftCmd: new bytes(1) }); + // The caller is responsible for estimating and providing the correct messaging fee. MessagingFee memory messagingFee = MessagingFee(msg.value, 0); ( diff --git a/test/ERC4626Adapter.ts b/test/ERC4626Adapter.ts index d15c635..2923679 100644 --- a/test/ERC4626Adapter.ts +++ b/test/ERC4626Adapter.ts @@ -416,6 +416,74 @@ describe("ERC4626Adapter", function () { .to.be.revertedWithCustomError(adapter, "EnforcedPause"); }); + it("Should NOT withdraw liquidity as profit from the target vault, 1 share", async function () { + const { + adapter, liquidityPool, usdc, lp, user, generateProfit, withdrawProfit + } = await loadFixture(deployAll); + const amount = 1n; + await usdc.connect(lp).approve(adapter, amount); + await adapter.connect(lp).depositWithPull(amount); + expect(await adapter.totalDeposited()).to.eq(amount); + expect(await liquidityPool.balanceOf(adapter)).to.eq(amount); + + const profit = 1n; + await generateProfit(profit); + expect(await adapter.totalDeposited()).to.eq(amount); + expect(await liquidityPool.balanceOf(adapter)).to.eq(amount); + expect(await liquidityPool.totalAssets()).to.eq(amount + profit); + + await expect(adapter.connect(withdrawProfit).withdrawProfit([usdc], user)) + .to.be.revertedWithCustomError(adapter, "NoProfit"); + }); + + it("Should NOT withdraw liquidity as profit from the target vault, 2 shares", async function () { + const { + adapter, liquidityPool, usdc, lp, user, generateProfit, withdrawProfit + } = await loadFixture(deployAll); + const amount = 2n; + await usdc.connect(lp).approve(adapter, amount); + await adapter.connect(lp).depositWithPull(amount); + expect(await adapter.totalDeposited()).to.eq(amount); + expect(await liquidityPool.balanceOf(adapter)).to.eq(amount); + + const profit = 1n; + await generateProfit(profit); + expect(await adapter.totalDeposited()).to.eq(amount); + expect(await liquidityPool.balanceOf(adapter)).to.eq(amount); + expect(await liquidityPool.totalAssets()).to.eq(amount + profit); + + await expect(adapter.connect(withdrawProfit).withdrawProfit([usdc], user)) + .to.be.revertedWithCustomError(adapter, "NoProfit"); + }); + + it("Should NOT withdraw liquidity as profit from the target vault, 2 shares, 5 assets", async function () { + const { + adapter, liquidityPool, usdc, USDC_DEC, lp, user, generateProfit, withdrawProfit + } = await loadFixture(deployAll); + const amount = 2n; + const amountOthers = 100n * USDC_DEC; + await usdc.connect(lp).approve(adapter, amount); + await adapter.connect(lp).depositWithPull(amount); + await usdc.connect(lp).approve(liquidityPool, amountOthers); + await liquidityPool.connect(lp)[ERC4626Deposit](amountOthers, lp); + expect(await adapter.totalDeposited()).to.eq(amount); + expect(await liquidityPool.balanceOf(adapter)).to.eq(amount); + expect(await liquidityPool.totalAssets()).to.eq(amountOthers + amount); + + const profit = 3n + 150n * USDC_DEC; + await generateProfit(profit); + console.log(await liquidityPool.previewWithdraw(amount)); + expect(await adapter.totalDeposited()).to.eq(amount); + expect(await liquidityPool.balanceOf(adapter)).to.eq(amount); + expect(await liquidityPool.totalAssets()).to.eq(amountOthers + amount + profit); + + await expect(adapter.connect(withdrawProfit).withdrawProfit([usdc], user)) + .to.emit(adapter, "ProfitWithdrawn").withArgs(usdc, user, 2n); + expect(await adapter.totalDeposited()).to.eq(amount); + expect(await liquidityPool.maxWithdraw(adapter)).to.eq(2n); + expect(await usdc.balanceOf(user)).to.eq(2n); + }); + it("Should NOT allow native token donations", async function () { const {admin, adapter} = await loadFixture(deployAll); const amount = 2n * ETH; diff --git a/test/Repayer.ts b/test/Repayer.ts index c4d4695..34dca34 100644 --- a/test/Repayer.ts +++ b/test/Repayer.ts @@ -597,6 +597,36 @@ describe("Repayer", function () { )).to.be.revertedWithCustomError(repayer, "SlippageTooHigh()"); }); + it("Should revert Across repay if native currency is sent along", async function () { + const {repayer, EURC_DEC, admin, repayUser, + liquidityPool, eurc, user, eurcOwner, + } = await loadFixture(deployAll); + + await eurc.connect(eurcOwner).transfer(repayer, 10n * EURC_DEC); + + await repayer.connect(admin).setRoute( + [liquidityPool], + [Domain.ETHEREUM], + [Provider.ACROSS], + [true], + ALLOWED + ); + const amount = 4n * EURC_DEC; + const extraData = AbiCoder.defaultAbiCoder().encode( + ["address", "uint256", "address", "uint32", "uint32", "uint32"], + [ZERO_ADDRESS, amount * 998n / 1000n - 1n, user.address, 1n, 2n, 3n] + ); + await expect(repayer.connect(repayUser).initiateRepay( + eurc, + amount, + liquidityPool, + Domain.ETHEREUM, + Provider.ACROSS, + extraData, + {value: 1n} + )).to.be.revertedWithCustomError(repayer, "NotPayable()"); + }); + it("Should allow repayer to initiate Everclear repay on fork", async function () { const {repayer, USDC_DEC, admin, repayUser, liquidityPool, everclearFeeAdapter, forkNetworkConfig, @@ -867,6 +897,115 @@ describe("Repayer", function () { .to.be.revertedWithCustomError(optimismBridge, "OptimismBridgeWrongRemoteToken"); }); + it("Should revert Optimism repay if native currency is sent along", async function () { + const { + USDC_DEC, usdc, repayUser, liquidityPool, optimismBridge, cctpTokenMessenger, cctpMessageTransmitter, + acrossV3SpokePool, everclearFeeAdapter, weth, stargateTreasurerTrue, admin, deployer, + } = await loadFixture(deployAll); + + const repayerImpl = ( + await deployX("Repayer", deployer, "Repayer2", {}, + Domain.ETHEREUM, + usdc, + cctpTokenMessenger, + cctpMessageTransmitter, + acrossV3SpokePool, + everclearFeeAdapter, + weth, + stargateTreasurerTrue, + optimismBridge, + ) + ) as Repayer; + const repayerInit = (await repayerImpl.initialize.populateTransaction( + admin, + repayUser, + [liquidityPool, liquidityPool], + [Domain.ETHEREUM, Domain.OP_MAINNET], + [Provider.LOCAL, Provider.OPTIMISM_STANDARD_BRIDGE], + [true, true], + )).data; + const repayerProxy = (await deployX( + "TransparentUpgradeableProxy", deployer, "TransparentUpgradeableProxyRepayer2", {}, + repayerImpl, admin, repayerInit + )) as TransparentUpgradeableProxy; + const repayer = (await getContractAt("Repayer", repayerProxy, deployer)) as Repayer; + + await usdc.transfer(repayer, 10n * USDC_DEC); + + const amount = 4n * USDC_DEC; + const outputToken = usdc.target; + const minGasLimit = 100000n; + const extraData = AbiCoder.defaultAbiCoder().encode( + ["address", "uint32"], + [outputToken, minGasLimit] + ); + const tx = repayer.connect(repayUser).initiateRepay( + usdc, + amount, + liquidityPool, + Domain.OP_MAINNET, + Provider.OPTIMISM_STANDARD_BRIDGE, + extraData, + {value: 1n} + ); + await expect(tx) + .to.be.revertedWithCustomError(repayer, "NotPayable()"); + }); + + it("Should revert Optimism repay if output token is zero address", async function () { + const { + USDC_DEC, usdc, repayUser, liquidityPool, optimismBridge, cctpTokenMessenger, cctpMessageTransmitter, + acrossV3SpokePool, everclearFeeAdapter, weth, stargateTreasurerTrue, admin, deployer, + } = await loadFixture(deployAll); + + const repayerImpl = ( + await deployX("Repayer", deployer, "Repayer2", {}, + Domain.ETHEREUM, + usdc, + cctpTokenMessenger, + cctpMessageTransmitter, + acrossV3SpokePool, + everclearFeeAdapter, + weth, + stargateTreasurerTrue, + optimismBridge, + ) + ) as Repayer; + const repayerInit = (await repayerImpl.initialize.populateTransaction( + admin, + repayUser, + [liquidityPool, liquidityPool], + [Domain.ETHEREUM, Domain.OP_MAINNET], + [Provider.LOCAL, Provider.OPTIMISM_STANDARD_BRIDGE], + [true, true], + )).data; + const repayerProxy = (await deployX( + "TransparentUpgradeableProxy", deployer, "TransparentUpgradeableProxyRepayer2", {}, + repayerImpl, admin, repayerInit + )) as TransparentUpgradeableProxy; + const repayer = (await getContractAt("Repayer", repayerProxy, deployer)) as Repayer; + + await usdc.transfer(repayer, 10n * USDC_DEC); + + const amount = 4n * USDC_DEC; + const outputToken = ZERO_ADDRESS; + const minGasLimit = 100000n; + const extraData = AbiCoder.defaultAbiCoder().encode( + ["address", "uint32"], + [outputToken, minGasLimit] + ); + const tx = repayer.connect(repayUser).initiateRepay( + usdc, + amount, + liquidityPool, + Domain.OP_MAINNET, + Provider.OPTIMISM_STANDARD_BRIDGE, + extraData + ); + await expect(tx) + .to.be.revertedWithCustomError(repayer, "ZeroAddress()"); + }); + it("Should NOT allow repayer to initiate Optimism repay on invalid route", async function () { const {repayer, USDC_DEC, usdc, admin, repayUser, liquidityPool} = await loadFixture(deployAll); @@ -1099,6 +1238,22 @@ describe("Repayer", function () { .to.be.revertedWithCustomError(repayer, "ProcessFailed()"); }); + it("Should revert CCTP initiate if native currency is sent along", async function () { + const {repayer, usdc, USDC_DEC, liquidityPool, repayUser} = await loadFixture(deployAll); + + await usdc.transfer(repayer, 10n * USDC_DEC); + + await expect(repayer.connect(repayUser).initiateRepay( + usdc, + 4n * USDC_DEC, + liquidityPool, + Domain.ETHEREUM, + Provider.CCTP, + "0x", + {value: 1n} + )).to.be.revertedWithCustomError(repayer, "NotPayable()"); + }); + it("Should perform Stargate repay with a mock pool", async function () { const {repayer, USDC_DEC, usdc, admin, repayUser, liquidityPool, deployer} = await loadFixture(deployAll); From c96084c5d945a1d763346c482127189524d2580d Mon Sep 17 00:00:00 2001 From: Oleksii Matiiasevych Date: Tue, 25 Nov 2025 23:02:31 +0700 Subject: [PATCH 3/9] Remove console.log from tests --- test/ERC4626Adapter.ts | 3 +-- test/LiquidityPoolAave.ts | 1 + 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/test/ERC4626Adapter.ts b/test/ERC4626Adapter.ts index 2923679..f4841b6 100644 --- a/test/ERC4626Adapter.ts +++ b/test/ERC4626Adapter.ts @@ -342,7 +342,7 @@ describe("ERC4626Adapter", function () { expect(await adapter.totalDeposited()).to.eq(amount); expect(await liquidityPool.balanceOf(adapter)).to.eq(amount); - const profit = 100n * USDC_DEC; + const profit = 1000n * USDC_DEC; await generateProfit(profit); expect(await adapter.totalDeposited()).to.eq(amount); expect(await liquidityPool.balanceOf(adapter)).to.eq(amount); @@ -472,7 +472,6 @@ describe("ERC4626Adapter", function () { const profit = 3n + 150n * USDC_DEC; await generateProfit(profit); - console.log(await liquidityPool.previewWithdraw(amount)); expect(await adapter.totalDeposited()).to.eq(amount); expect(await liquidityPool.balanceOf(adapter)).to.eq(amount); expect(await liquidityPool.totalAssets()).to.eq(amountOthers + amount + profit); diff --git a/test/LiquidityPoolAave.ts b/test/LiquidityPoolAave.ts index 9f399d0..38c7af7 100644 --- a/test/LiquidityPoolAave.ts +++ b/test/LiquidityPoolAave.ts @@ -3214,6 +3214,7 @@ describe("LiquidityPoolAave", function () { .to.emit(liquidityPool, "SuppliedToAave").withArgs(amount); expect(await aToken.balanceOf(liquidityPool)).to.be.greaterThanOrEqual(amount - 2n); + await time.increase(100); await expect(liquidityPool.connect(liquidityAdmin).withdraw(user, amount)) .to.emit(liquidityPool, "WithdrawnFromAave").withArgs(user.address, amount); From cb780a6e49a03a6905fca89d796ef3d814df95dd Mon Sep 17 00:00:00 2001 From: Oleksii Matiiasevych Date: Tue, 25 Nov 2025 23:19:59 +0700 Subject: [PATCH 4/9] Merge main and fix tests --- test/ERC4626Adapter.ts | 2 +- test/PublicLiquidityPool.ts | 24 +----------------------- 2 files changed, 2 insertions(+), 24 deletions(-) diff --git a/test/ERC4626Adapter.ts b/test/ERC4626Adapter.ts index f4841b6..2fc78ba 100644 --- a/test/ERC4626Adapter.ts +++ b/test/ERC4626Adapter.ts @@ -104,7 +104,7 @@ describe("ERC4626Adapter", function () { amountToBorrow, mockTarget, callDataWithAmountToReceive, - 31337, + undefined, nonce ); diff --git a/test/PublicLiquidityPool.ts b/test/PublicLiquidityPool.ts index 472dd74..9cd4cce 100644 --- a/test/PublicLiquidityPool.ts +++ b/test/PublicLiquidityPool.ts @@ -333,7 +333,6 @@ describe("PublicLiquidityPool", function () { amountToBorrow, mockTarget, callDataWithAmountToReceive, - 31337 ); await expect(liquidityPool.connect(user).borrow( @@ -390,7 +389,6 @@ describe("PublicLiquidityPool", function () { amountToBorrow, mockTarget, callDataWithAmountToReceive, - 31337 ); const borrowCalldata = await liquidityPool.borrowAndSwap.populateTransaction( @@ -454,7 +452,6 @@ describe("PublicLiquidityPool", function () { amountToBorrow, mockTarget, callDataWithAmountToReceive, - 31337 ); const borrowCalldata = await liquidityPool.borrowAndSwap.populateTransaction( @@ -520,7 +517,6 @@ describe("PublicLiquidityPool", function () { amountToBorrow, mockTarget, callDataWithAmountToReceive, - 31337 ); const borrowCalldata = await liquidityPool.borrowAndSwap.populateTransaction( @@ -574,7 +570,6 @@ describe("PublicLiquidityPool", function () { amountToBorrow, mockTarget, callDataWithAmountToReceive, - 31337 ); const borrowCalldata = await liquidityPool.borrowAndSwap.populateTransaction( @@ -632,7 +627,6 @@ describe("PublicLiquidityPool", function () { [amountToBorrow, amountToBorrow2], mockTarget, callData.data, - 31337 ); await expect(liquidityPool.connect(user).borrowMany( @@ -676,7 +670,6 @@ describe("PublicLiquidityPool", function () { [amountToBorrow], mockTarget, callData.data, - 31337 ); const borrowCalldata = await liquidityPool.borrowAndSwapMany.populateTransaction( @@ -813,7 +806,6 @@ describe("PublicLiquidityPool", function () { amountToBorrow, mockTarget, callDataWithAmountToReceive, - 31337 ); await expect(liquidityPool.connect(user).borrow( @@ -961,7 +953,6 @@ describe("PublicLiquidityPool", function () { amountToBorrow, user2, callData, - 31337 ); await expect(liquidityPool.connect(user).borrow( @@ -993,7 +984,6 @@ describe("PublicLiquidityPool", function () { amountToBorrow, user2, "0x", - 31337 ); await expect(liquidityPool.connect(user).borrow( @@ -1025,7 +1015,6 @@ describe("PublicLiquidityPool", function () { amountToBorrow, user2, callData, - 31337 ); await liquidityPool.connect(user).borrow( @@ -1066,7 +1055,7 @@ describe("PublicLiquidityPool", function () { amountToBorrow, user2, "0x", - 31337, + undefined, 0n, deadline, ); @@ -1105,7 +1094,6 @@ describe("PublicLiquidityPool", function () { amountToBorrow, usdc, callDataWithAmountToReceive, - 31337 ); await expect(liquidityPool.connect(user).borrow( @@ -1135,7 +1123,6 @@ describe("PublicLiquidityPool", function () { amountToBorrow, user2, "0x", - 31337 ); await expect(liquidityPool.connect(user).borrow( @@ -1184,7 +1171,6 @@ describe("PublicLiquidityPool", function () { amountToBorrow, user2, "0x", - 31337 ); await expect(liquidityPool.connect(user2).borrow( @@ -1229,7 +1215,6 @@ describe("PublicLiquidityPool", function () { amountToBorrow, mockTarget, callData.data, - 31337 ); const borrowCalldata = await liquidityPool.borrowAndSwap.populateTransaction( @@ -1277,7 +1262,6 @@ describe("PublicLiquidityPool", function () { amountToBorrow, mockTarget, callDataWithAmountToReceive, - 31337 ); const borrowCalldata = await liquidityPool.borrowAndSwap.populateTransaction( @@ -1314,7 +1298,6 @@ describe("PublicLiquidityPool", function () { amountToBorrow, mockTarget, callData, - 31337 ); await expect(liquidityPool.connect(user).borrowAndSwap( @@ -1354,7 +1337,6 @@ describe("PublicLiquidityPool", function () { amountToBorrow, mockTarget, callDataWithAmountToReceive, - 31337 ); await liquidityPool.connect(user).borrow( @@ -1416,7 +1398,6 @@ describe("PublicLiquidityPool", function () { amountToBorrow, mockTarget, callDataWithAmountToReceive, - 31337 ); await liquidityPool.connect(user).borrow( @@ -1652,7 +1633,6 @@ describe("PublicLiquidityPool", function () { amountToBorrow, mockTarget, callDataWithAmountToReceive, - 31337 ); await expect(liquidityPool.connect(user).borrow( @@ -1704,7 +1684,6 @@ describe("PublicLiquidityPool", function () { amountToBorrow, mockTarget, callDataWithAmountToReceive, - 31337 ); await expect(liquidityPool.connect(user).borrow( @@ -1756,7 +1735,6 @@ describe("PublicLiquidityPool", function () { amountToBorrow, mockTarget, callDataWithAmountToReceive, - 31337 ); await expect(liquidityPool.connect(user).borrow( From a591da555f67033570fb5061f70619942abb40d3 Mon Sep 17 00:00:00 2001 From: Oleksii Matiiasevych Date: Wed, 26 Nov 2025 16:24:57 +0700 Subject: [PATCH 5/9] Make all tests run --- test/ERC4626Adapter.ts | 2 +- test/PublicLiquidityPool.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/test/ERC4626Adapter.ts b/test/ERC4626Adapter.ts index 9104837..2fc78ba 100644 --- a/test/ERC4626Adapter.ts +++ b/test/ERC4626Adapter.ts @@ -26,7 +26,7 @@ function addAmountToReceive(callData: string, amountToReceive: bigint) { const ERC4626Deposit = "deposit(uint256,address)"; -describe.only("ERC4626Adapter", function () { +describe("ERC4626Adapter", function () { const deployAll = async () => { const [ deployer, admin, user, user2, mpc_signer, liquidityAdmin, withdrawProfit, pauser, lp, diff --git a/test/PublicLiquidityPool.ts b/test/PublicLiquidityPool.ts index f78045b..9cd4cce 100644 --- a/test/PublicLiquidityPool.ts +++ b/test/PublicLiquidityPool.ts @@ -32,7 +32,7 @@ function addAmountToReceive(callData: string, amountToReceive: bigint) { ]); } -describe.only("PublicLiquidityPool", function () { +describe("PublicLiquidityPool", function () { const deployAll = async () => { const [ deployer, admin, user, user2, mpc_signer, feeSetter, withdrawProfit, pauser, lp, From 7fd2f52eccaf5c74291970f985d84003fd96bd03 Mon Sep 17 00:00:00 2001 From: Oleksii Matiiasevych Date: Wed, 26 Nov 2025 18:30:07 +0700 Subject: [PATCH 6/9] Last part of fixes --- contracts/LiquidityPool.sol | 3 +++ contracts/LiquidityPoolAave.sol | 5 +++-- contracts/Repayer.sol | 18 +++++++++--------- 3 files changed, 15 insertions(+), 11 deletions(-) diff --git a/contracts/LiquidityPool.sol b/contracts/LiquidityPool.sol index ff0c3f8..e0ff057 100644 --- a/contracts/LiquidityPool.sol +++ b/contracts/LiquidityPool.sol @@ -136,6 +136,9 @@ contract LiquidityPool is ILiquidityPool, AccessControl, EIP712, ISigner { // Allow native token transfers. } + /// @notice The liqudity admin is supposed to call this function after transferring exact amount of assets. + /// Supplying amount less than the actual increase will result in the extra funds being treated as profit. + /// Supplying amount greater than the actual increase will result in the future profits treated as deposit. function deposit(uint256 amount) external virtual override onlyRole(LIQUIDITY_ADMIN_ROLE) { // called after receiving deposit in USDC uint256 newBalance = ASSETS.balanceOf(address(this)); diff --git a/contracts/LiquidityPoolAave.sol b/contracts/LiquidityPoolAave.sol index 56de862..f1160d5 100644 --- a/contracts/LiquidityPoolAave.sol +++ b/contracts/LiquidityPoolAave.sol @@ -69,8 +69,9 @@ contract LiquidityPoolAave is LiquidityPool { AaveDataTypes.ReserveData memory collateralData = AAVE_POOL.getReserveData(address(liquidityToken)); ATOKEN = IERC20(collateralData.aTokenAddress); IAavePoolDataProvider poolDataProvider = IAavePoolDataProvider(provider.getPoolDataProvider()); - (,,,,,bool usageAsCollateralEnabled,,,,) = poolDataProvider.getReserveConfigurationData(liquidityToken); - require(usageAsCollateralEnabled, CollateralNotSupported()); + (,,,,,bool usageAsCollateralEnabled,,,bool isActive, bool isFrozen) = + poolDataProvider.getReserveConfigurationData(liquidityToken); + require(usageAsCollateralEnabled && isActive && !isFrozen, CollateralNotSupported()); _setMinHealthFactor(minHealthFactor_); _setDefaultLTV(defaultLTV_); } diff --git a/contracts/Repayer.sol b/contracts/Repayer.sol index 091e503..270e054 100644 --- a/contracts/Repayer.sol +++ b/contracts/Repayer.sol @@ -106,10 +106,10 @@ contract Repayer is function initialize( address admin, address repayer, - address[] memory pools, - Domain[] memory domains, - Provider[] memory providers, - bool[] memory poolSupportsAllTokens + address[] calldata pools, + Domain[] calldata domains, + Provider[] calldata providers, + bool[] calldata poolSupportsAllTokens ) external initializer() { _grantRole(DEFAULT_ADMIN_ROLE, admin); _grantRole(REPAYER_ROLE, repayer); @@ -120,7 +120,7 @@ contract Repayer is address[] calldata pools, Domain[] calldata domains, Provider[] calldata providers, - bool[] memory poolSupportsAllTokens, + bool[] calldata poolSupportsAllTokens, bool isAllowed ) external onlyRole(DEFAULT_ADMIN_ROLE) { _setRoute(pools, domains, providers, poolSupportsAllTokens, isAllowed); @@ -211,10 +211,10 @@ contract Repayer is } function _setRoute( - address[] memory pools, - Domain[] memory domains, - Provider[] memory providers, - bool[] memory poolSupportsAllTokens, + address[] calldata pools, + Domain[] calldata domains, + Provider[] calldata providers, + bool[] calldata poolSupportsAllTokens, bool isAllowed ) internal { RepayerStorage storage $ = _getStorage(); From d2ac2c56d7a5eb87127a3062254b24fae2b011aa Mon Sep 17 00:00:00 2001 From: Oleksii Matiiasevych Date: Wed, 26 Nov 2025 20:32:06 +0700 Subject: [PATCH 7/9] Make public pool maxWithdraw and maxRedeem compliant with the EIP4626 --- contracts/PublicLiquidityPool.sol | 17 ++++++++ test/PublicLiquidityPool.ts | 65 ++++++++++++++++++++++++++++++- 2 files changed, 80 insertions(+), 2 deletions(-) diff --git a/contracts/PublicLiquidityPool.sol b/contracts/PublicLiquidityPool.sol index 360e04c..96e4681 100644 --- a/contracts/PublicLiquidityPool.sol +++ b/contracts/PublicLiquidityPool.sol @@ -98,6 +98,23 @@ contract PublicLiquidityPool is LiquidityPool, ERC4626 { return _virtualBalance - protocolFee; } + function maxWithdraw(address owner) public view override returns (uint256) { + if (paused) { + return 0; + } + return Math.min(super.maxWithdraw(owner), ASSETS.balanceOf(address(this))); + } + + function maxRedeem(address owner) public view override returns (uint256) { + if (paused) { + return 0; + } + return Math.min( + super.maxRedeem(owner), + _convertToShares(ASSETS.balanceOf(address(this)), Math.Rounding.Floor) + ); + } + function _setProtocolFeeRate(uint16 protocolFeeRate_) internal { require(protocolFeeRate_ <= RATE_DENOMINATOR, InvalidProtocolFeeRate()); protocolFeeRate = protocolFeeRate_; diff --git a/test/PublicLiquidityPool.ts b/test/PublicLiquidityPool.ts index 9cd4cce..11d5e3d 100644 --- a/test/PublicLiquidityPool.ts +++ b/test/PublicLiquidityPool.ts @@ -188,6 +188,8 @@ describe("PublicLiquidityPool", function () { expect(await liquidityPool.totalSupply()).to.eq(amount); expect(await liquidityPool.balance(usdc)).to.eq(amount); expect(await liquidityPool.balanceOf(lp)).to.eq(amount); + expect(await liquidityPool.maxWithdraw(lp)).to.eq(amount); + expect(await liquidityPool.maxRedeem(lp)).to.eq(amount); expect(await usdc.balanceOf(liquidityPool)).to.eq(amount); }); @@ -306,6 +308,62 @@ describe("PublicLiquidityPool", function () { expect(await liquidityPool.balanceOf(lp)).to.eq(0); }); + it("Should calculate maxRedeem and maxWithdraw correctly", async function () { + const { + liquidityPool, mockTarget, usdc, USDC_DEC, user, mpc_signer, lp + } = await loadFixture(deployAll); + const amountLiquidity = 100n; + await usdc.connect(lp).approve(liquidityPool, amountLiquidity); + await liquidityPool.connect(lp)[ERC4626Deposit](amountLiquidity, lp); + expect(await liquidityPool.maxWithdraw(lp)).to.eq(amountLiquidity); + expect(await liquidityPool.maxRedeem(lp)).to.eq(amountLiquidity); + + const amountToBorrow = 100n; + const fee = 20n; + const protocolFee = fee / 5n; + const amountToReceive = amountToBorrow - fee; + const fillAmount = amountToReceive; + + const additionalData = "0x123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0"; + + const callData = await mockTarget.fulfill.populateTransaction(usdc, fillAmount, additionalData); + const callDataWithAmountToReceive = addAmountToReceive(callData.data, amountToReceive);// 100 shares, 116 total assets, balance 20 + + const signature = await signBorrow( + mpc_signer, + liquidityPool, + user, + usdc, + amountToBorrow, + mockTarget, + callDataWithAmountToReceive, + ); + + await expect(liquidityPool.connect(user).borrow( + usdc, + amountToBorrow, + mockTarget, + callDataWithAmountToReceive, + 0n, + 2000000000n, + signature)) + .to.emit(mockTarget, "DataReceived").withArgs(additionalData); + expect(await usdc.balanceOf(liquidityPool)).to.eq(amountLiquidity - amountToReceive); + expect(await usdc.balanceOf(mockTarget)).to.eq(amountToReceive); + expect(await usdc.allowance(liquidityPool, mockTarget)).to.eq(0); + expect(await liquidityPool.totalDeposited()).to.eq(amountLiquidity + fee); + expect(await liquidityPool.totalAssets()).to.eq(amountLiquidity + fee - protocolFee); + expect(await liquidityPool.protocolFee()).to.eq(protocolFee); + expect(await liquidityPool.totalSupply()).to.eq(amountLiquidity); + expect(await liquidityPool.balanceOf(lp)).to.eq(amountLiquidity); + expect(await liquidityPool.balance(usdc)).to.eq(amountLiquidity - amountToReceive); + expect(await liquidityPool.maxWithdraw(lp)).to.eq(amountLiquidity - amountToReceive); + expect(await liquidityPool.maxRedeem(lp)).to.eq(17n); + await usdc.connect(lp).transfer(liquidityPool, amountToBorrow); + expect(await liquidityPool.maxWithdraw(lp)).to.eq(amountLiquidity + fee - protocolFee); + expect(await liquidityPool.maxRedeem(lp)).to.eq(amountLiquidity); + }); + it("Should borrow a token with contract call", async function () { const { liquidityPool, mockTarget, usdc, USDC_DEC, user, mpc_signer, lp @@ -1432,9 +1490,12 @@ describe("PublicLiquidityPool", function () { await liquidityPool.connect(lp)[ERC4626Deposit](amountLiquidity, lp); await expect(liquidityPool.connect(pauser).pause()) .to.emit(liquidityPool, "Paused"); - - await expect(liquidityPool.connect(lp)[ERC4626Withdraw](amountLiquidity, user, lp)) + expect(await liquidityPool.maxWithdraw(user)).to.eq(0); + expect(await liquidityPool.maxRedeem(user)).to.eq(0); + await expect(liquidityPool.connect(lp)[ERC4626Withdraw](0, user, lp)) .to.be.revertedWithCustomError(liquidityPool, "EnforcedPause"); + await expect(liquidityPool.connect(lp)[ERC4626Withdraw](amountLiquidity, user, lp)) + .to.be.revertedWithCustomError(liquidityPool, "ERC4626ExceededMaxWithdraw"); }); it("Should NOT withdraw liquidity to zero address", async function () { From 15b0372d8bc9a15581af2480134018eb4cd46025 Mon Sep 17 00:00:00 2001 From: Oleksii Matiiasevych Date: Wed, 26 Nov 2025 20:34:26 +0700 Subject: [PATCH 8/9] Fix lint --- test/PublicLiquidityPool.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/PublicLiquidityPool.ts b/test/PublicLiquidityPool.ts index 11d5e3d..408924a 100644 --- a/test/PublicLiquidityPool.ts +++ b/test/PublicLiquidityPool.ts @@ -310,7 +310,7 @@ describe("PublicLiquidityPool", function () { it("Should calculate maxRedeem and maxWithdraw correctly", async function () { const { - liquidityPool, mockTarget, usdc, USDC_DEC, user, mpc_signer, lp + liquidityPool, mockTarget, usdc, user, mpc_signer, lp } = await loadFixture(deployAll); const amountLiquidity = 100n; await usdc.connect(lp).approve(liquidityPool, amountLiquidity); @@ -327,7 +327,7 @@ describe("PublicLiquidityPool", function () { const additionalData = "0x123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0"; const callData = await mockTarget.fulfill.populateTransaction(usdc, fillAmount, additionalData); - const callDataWithAmountToReceive = addAmountToReceive(callData.data, amountToReceive);// 100 shares, 116 total assets, balance 20 + const callDataWithAmountToReceive = addAmountToReceive(callData.data, amountToReceive); const signature = await signBorrow( mpc_signer, From 8a93608beb885b2d497f2fa15e660f2a2c3df68a Mon Sep 17 00:00:00 2001 From: Oleksii Matiiasevych Date: Thu, 27 Nov 2025 15:38:13 +0700 Subject: [PATCH 9/9] Improve ERC4626Adapter withdraw profit safety assertion --- contracts/ERC4626Adapter.sol | 2 +- test/ERC4626Adapter.ts | 36 ++++++++++++++++++++++++++++++++++-- 2 files changed, 35 insertions(+), 3 deletions(-) diff --git a/contracts/ERC4626Adapter.sol b/contracts/ERC4626Adapter.sol index 3c3466d..d20c9a1 100644 --- a/contracts/ERC4626Adapter.sol +++ b/contracts/ERC4626Adapter.sol @@ -122,7 +122,7 @@ contract ERC4626Adapter is ILiquidityPoolBase, AccessControl { uint256 totalShares = TARGET_VAULT.balanceOf(address(this)); if (totalShares <= depositedShares) return localBalance; uint256 profit = TARGET_VAULT.redeem(totalShares - depositedShares, address(this), address(this)); - assert(TARGET_VAULT.maxWithdraw(address(this)) >= deposited); + assert(TARGET_VAULT.previewRedeem(depositedShares) >= deposited); return profit + localBalance; } return localBalance; diff --git a/test/ERC4626Adapter.ts b/test/ERC4626Adapter.ts index 2fc78ba..62c0864 100644 --- a/test/ERC4626Adapter.ts +++ b/test/ERC4626Adapter.ts @@ -89,7 +89,7 @@ describe("ERC4626Adapter", function () { const PAUSER_ROLE = encodeBytes32String("PAUSER_ROLE"); await adapter.connect(admin).grantRole(PAUSER_ROLE, pauser); - const generateProfit = async (amount: bigint, nonce: bigint = 0n) => { + const generateProfit = async (amount: bigint, nonce: bigint = 0n, transferProfit: boolean = true) => { const amountToBorrow = amount; const amountToReceive = 0n; @@ -117,7 +117,9 @@ describe("ERC4626Adapter", function () { 2000000000n, signature); - await usdc.connect(usdcOwner).transfer(liquidityPool, amount); + if (transferProfit) { + await usdc.connect(usdcOwner).transfer(liquidityPool, amount); + } }; return {deployer, admin, user, user2, mpc_signer, usdc, usdcOwner, eurc, eurcOwner, @@ -370,6 +372,36 @@ describe("ERC4626Adapter", function () { expect(await usdc.balanceOf(user)).to.eq(amount + profit + profit + donation); }); + it("Should withdraw profit from an underfunded target vault", async function () { + const { + adapter, liquidityPool, usdc, USDC_DEC, lp, user, usdcOwner, generateProfit, withdrawProfit, liquidityAdmin + } = await loadFixture(deployAll); + const amount = 1000n * USDC_DEC; + await usdc.connect(lp).approve(adapter, amount); + await adapter.connect(lp).depositWithPull(amount); + expect(await adapter.totalDeposited()).to.eq(amount); + expect(await liquidityPool.balanceOf(adapter)).to.eq(amount); + + const profit = 1000n * USDC_DEC; + await generateProfit(profit, 0n, false); + expect(await adapter.totalDeposited()).to.eq(amount); + expect(await liquidityPool.balanceOf(adapter)).to.eq(amount); + expect(await liquidityPool.totalAssets()).to.eq(amount + profit); + + await expect(adapter.connect(withdrawProfit).withdrawProfit([usdc], user)) + .to.emit(adapter, "ProfitWithdrawn").withArgs(usdc, user, profit); + expect(await adapter.totalDeposited()).to.eq(amount); + expect(await liquidityPool.balanceOf(adapter)).to.eq(amount / 2n); + expect(await liquidityPool.previewRedeem(amount / 2n)).to.eq(amount); + expect(await usdc.balanceOf(user)).to.eq(profit); + + await usdc.connect(usdcOwner).transfer(liquidityPool, amount); + await adapter.connect(liquidityAdmin).withdraw(user, amount); + expect(await adapter.totalDeposited()).to.eq(0n); + expect(await liquidityPool.balanceOf(adapter)).to.eq(0n); + expect(await usdc.balanceOf(user)).to.eq(amount + profit); + }); + it("Should NOT withdraw profit in target vault shares", async function () { const { adapter, liquidityPool, usdc, USDC_DEC, lp, withdrawProfit, user