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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 31 additions & 17 deletions src/contracts/AbstractARM.sol
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,7 @@ abstract contract AbstractARM is OwnableOperable, ERC20Upgradeable {
event MarketAdded(address indexed market);
event MarketRemoved(address indexed market);
event ARMBufferUpdated(uint256 armBuffer);
event Allocated(address indexed market, int256 assets);
event Allocated(address indexed market, int256 targetLiquidityDelta, int256 actualLiquidityDelta);

constructor(
address _token0,
Expand Down Expand Up @@ -905,59 +905,73 @@ abstract contract AbstractARM is OwnableOperable, ERC20Upgradeable {

/// @notice Deposit or withdraw liquidity assets to/from the active lending market
/// to match the ARM's liquidity buffer which is a percentage of the available assets.
/// The buffer excludes liquidity assets reserved for the ARM's withdrawal queue. That is, more
/// liquidity assets will be withdrawn from the lending market if the ARM's liquidity asset balance
/// does not cover the buffer, which can be zero, and the ARM's outstanding withdrawals.
/// Will revert if there is no active lending market set.
/// @return liquidityDelta The actual liquidity less target liquidity before
/// the deposit/withdrawal to/from the active lending market.
function allocate() external returns (int256 liquidityDelta) {
/// @return targetLiquidityDelta the desired amount that is deposited/withdrawn to/from the lending market.
/// A positive value is the liquidity assets that should be deposited to the lending market.
/// A negative value is the desired liquidity assets that should be withdrawn from the lending market.
/// @return actualLiquidityDelta the actual amount that is deposited/withdrawn to/from the lending market.
/// A positive value is the liquidity assets that were deposited to the lending market.
/// A negative value is the liquidity assets that were withdrawn from the lending market. This can be less than
/// the `targetLiquidityDelta`, or even zero, if there is high utilization in the lending market.
function allocate() external returns (int256 targetLiquidityDelta, int256 actualLiquidityDelta) {
require(activeMarket != address(0), "ARM: no active market");

liquidityDelta = _allocate();
return _allocate();
}

function _allocate() internal returns (int256 liquidityDelta) {
function _allocate() internal returns (int256 targetLiquidityDelta, int256 actualLiquidityDelta) {
(uint256 availableAssets, uint256 outstandingWithdrawals) = _availableAssets();
if (availableAssets == 0) return 0;
if (availableAssets == 0) return (0, 0);
uint256 targetArmLiquidity = availableAssets * armBuffer / 1e18;

int256 armLiquidity = SafeCast.toInt256(IERC20(liquidityAsset).balanceOf(address(this)))
// The current liquidity available in swap is the liquidity asset balance less
// any outstanding withdrawals from the ARM's withdrawal queue
int256 currentArmLiquidity = SafeCast.toInt256(IERC20(liquidityAsset).balanceOf(address(this)))
- SafeCast.toInt256(outstandingWithdrawals);
uint256 targetArmLiquidity = availableAssets * armBuffer / 1e18;

liquidityDelta = armLiquidity - SafeCast.toInt256(targetArmLiquidity);
targetLiquidityDelta = currentArmLiquidity - SafeCast.toInt256(targetArmLiquidity);

// Load the active lending market address from storage to save gas
address activeMarketMem = activeMarket;

// The allocateThreshold prevents the ARM from constantly depositing and withdrawing if there are rounding issues
if (liquidityDelta > allocateThreshold) {
if (targetLiquidityDelta > allocateThreshold) {
// We have too much liquidity in the ARM, we need to deposit some to the active lending market

uint256 depositAmount = SafeCast.toUint256(liquidityDelta);
uint256 depositAmount = SafeCast.toUint256(targetLiquidityDelta);

IERC20(liquidityAsset).approve(activeMarketMem, depositAmount);
IERC4626(activeMarketMem).deposit(depositAmount, address(this));
} else if (liquidityDelta < 0) {

actualLiquidityDelta = SafeCast.toInt256(depositAmount);
} else if (targetLiquidityDelta < 0) {
// We have too little liquidity in the ARM, we need to withdraw some from the active lending market

uint256 availableMarketAssets = IERC4626(activeMarketMem).maxWithdraw(address(this));
uint256 desiredWithdrawAmount = SafeCast.toUint256(-liquidityDelta);
uint256 desiredWithdrawAmount = SafeCast.toUint256(-targetLiquidityDelta);

if (availableMarketAssets < desiredWithdrawAmount) {
// Not enough assets in the market so redeem as much as possible.
// maxRedeem is used instead of balanceOf as we want to redeem as much as possible without failing.
// redeem of the ARM's balance can fail if the lending market is highly utilized or temporarily paused.
// Redeem and not withdrawal is used to avoid leaving a small amount of assets in the market.
uint256 shares = IERC4626(activeMarketMem).maxRedeem(address(this));
if (shares <= minSharesToRedeem) return liquidityDelta;
if (shares <= minSharesToRedeem) return (targetLiquidityDelta, 0);
// This should not fail according to the ERC-4626 spec as maxRedeem was used earlier
// but it depends on the 4626 implementation of the lending market.
// It may fail if the market is highly utilized and not compliant with 4626.
IERC4626(activeMarketMem).redeem(shares, address(this), address(this));
uint256 redeemedAssets = IERC4626(activeMarketMem).redeem(shares, address(this), address(this));
actualLiquidityDelta = -SafeCast.toInt256(redeemedAssets);
} else {
IERC4626(activeMarketMem).withdraw(desiredWithdrawAmount, address(this), address(this));
actualLiquidityDelta = -SafeCast.toInt256(desiredWithdrawAmount);
}
}

emit Allocated(activeMarketMem, liquidityDelta);
emit Allocated(activeMarketMem, targetLiquidityDelta, actualLiquidityDelta);
}

////////////////////////////////////////////////////
Expand Down
24 changes: 13 additions & 11 deletions test/fork/OriginARM/AllocateWithAdapter.sol
Original file line number Diff line number Diff line change
Expand Up @@ -51,14 +51,15 @@ contract Fork_Concrete_OriginARM_AllocateWithAdapter_Test_ is Fork_Shared_Test {
assertEq(market.balanceOf(address(siloMarket)), 0, "shares before");
assertApproxEqAbs(originARM.totalAssets(), DEFAULT_AMOUNT + MIN_TOTAL_SUPPLY, 1, "totalAssets before");
uint256 expectedShares = market.convertToShares(DEFAULT_AMOUNT + MIN_TOTAL_SUPPLY);
int256 expectedLiquidityDelta = (DEFAULT_AMOUNT + MIN_TOTAL_SUPPLY).toInt256();

// Expected event
vm.expectEmit(address(market));
emit IERC4626.Deposit(
address(siloMarket), address(siloMarket), DEFAULT_AMOUNT + MIN_TOTAL_SUPPLY, expectedShares
);
vm.expectEmit(address(originARM));
emit AbstractARM.Allocated(address(siloMarket), (DEFAULT_AMOUNT + MIN_TOTAL_SUPPLY).toInt256());
emit AbstractARM.Allocated(address(siloMarket), expectedLiquidityDelta, expectedLiquidityDelta);

// Main call
originARM.allocate();
Expand All @@ -83,16 +84,16 @@ contract Fork_Concrete_OriginARM_AllocateWithAdapter_Test_ is Fork_Shared_Test {
assertApproxEqAbs(marketBalanceBefore, sharesBefore, 1, "shares before");
assertApproxEqAbs(originARM.totalAssets(), DEFAULT_AMOUNT + MIN_TOTAL_SUPPLY, 1, "totalAssets before");

int256 expectedAmount = getLiquidityDelta();
uint256 expectedShares = market.previewWithdraw(abs(expectedAmount));
int256 expectedLiquidityDelta = getLiquidityDelta();
uint256 expectedShares = market.previewWithdraw(abs(expectedLiquidityDelta));

// Expected event
vm.expectEmit(address(market));
emit IERC4626.Withdraw(
address(siloMarket), address(originARM), address(siloMarket), abs(expectedAmount), expectedShares
address(siloMarket), address(originARM), address(siloMarket), abs(expectedLiquidityDelta), expectedShares
);
vm.expectEmit(address(originARM));
emit AbstractARM.Allocated(address(siloMarket), expectedAmount);
emit AbstractARM.Allocated(address(siloMarket), expectedLiquidityDelta, expectedLiquidityDelta);

// Main call
originARM.allocate();
Expand All @@ -117,17 +118,17 @@ contract Fork_Concrete_OriginARM_AllocateWithAdapter_Test_ is Fork_Shared_Test {
assertApproxEqAbs(marketBalanceBefore, sharesBefore, 1, "shares before");
assertApproxEqAbs(originARM.totalAssets(), DEFAULT_AMOUNT + MIN_TOTAL_SUPPLY, 1, "totalAssets before");

int256 expectedAmount = getLiquidityDelta();
uint256 expectedShares = market.previewWithdraw(abs(expectedAmount));
assertApproxEqAbs(abs(expectedAmount), DEFAULT_AMOUNT + MIN_TOTAL_SUPPLY, 1, "expectedAmount");
int256 expectedLiquidityDelta = getLiquidityDelta();
uint256 expectedShares = market.previewWithdraw(abs(expectedLiquidityDelta));
assertApproxEqAbs(abs(expectedLiquidityDelta), DEFAULT_AMOUNT + MIN_TOTAL_SUPPLY, 1, "expectedLiquidityDelta");

// Expected event
vm.expectEmit(address(market));
emit IERC4626.Withdraw(
address(siloMarket), address(originARM), address(siloMarket), abs(expectedAmount), expectedShares
address(siloMarket), address(originARM), address(siloMarket), abs(expectedLiquidityDelta), expectedShares
);
vm.expectEmit(address(originARM));
emit AbstractARM.Allocated(address(siloMarket), expectedAmount);
emit AbstractARM.Allocated(address(siloMarket), expectedLiquidityDelta, expectedLiquidityDelta);

// Main call
originARM.allocate();
Expand Down Expand Up @@ -156,14 +157,15 @@ contract Fork_Concrete_OriginARM_AllocateWithAdapter_Test_ is Fork_Shared_Test {

uint256 expectedShares = siloMarket.maxRedeem(address(originARM));
uint256 expectedAmount = market.convertToAssets(expectedShares);
int256 expectedLiquidityDelta = getLiquidityDelta();

// Expected event
vm.expectEmit(address(market));
emit IERC4626.Withdraw(
address(siloMarket), address(originARM), address(siloMarket), expectedAmount, expectedShares
);
vm.expectEmit(address(originARM));
emit AbstractARM.Allocated(address(siloMarket), getLiquidityDelta());
emit AbstractARM.Allocated(address(siloMarket), expectedLiquidityDelta, expectedLiquidityDelta + 1 ether + 1);
// Main call
originARM.allocate();

Expand Down
24 changes: 13 additions & 11 deletions test/fork/OriginARM/AllocateWithoutAdapter.sol
Original file line number Diff line number Diff line change
Expand Up @@ -44,12 +44,13 @@ contract Fork_Concrete_OriginARM_AllocateWithoutAdapter_Test_ is Fork_Shared_Tes
assertEq(market.balanceOf(address(originARM)), 0, "shares before");
assertApproxEqAbs(originARM.totalAssets(), DEFAULT_AMOUNT + MIN_TOTAL_SUPPLY, 1, "totalAssets before");
uint256 expectedShares = market.convertToShares(DEFAULT_AMOUNT + MIN_TOTAL_SUPPLY);
int256 expectedLiquidityDelta = (DEFAULT_AMOUNT + MIN_TOTAL_SUPPLY).toInt256();

// Expected event
vm.expectEmit(address(market));
emit IERC4626.Deposit(address(originARM), address(originARM), DEFAULT_AMOUNT + MIN_TOTAL_SUPPLY, expectedShares);
vm.expectEmit(address(originARM));
emit AbstractARM.Allocated(address(market), (DEFAULT_AMOUNT + MIN_TOTAL_SUPPLY).toInt256());
emit AbstractARM.Allocated(address(market), expectedLiquidityDelta, expectedLiquidityDelta);

// Main call
originARM.allocate();
Expand All @@ -74,16 +75,16 @@ contract Fork_Concrete_OriginARM_AllocateWithoutAdapter_Test_ is Fork_Shared_Tes
assertApproxEqAbs(marketBalanceBefore, sharesBefore, 1, "shares before");
assertApproxEqAbs(originARM.totalAssets(), DEFAULT_AMOUNT + MIN_TOTAL_SUPPLY, 1, "totalAssets before");

int256 expectedAmount = getLiquidityDelta();
uint256 expectedShares = market.previewWithdraw(abs(expectedAmount));
int256 expectedLiquidityDelta = getLiquidityDelta();
uint256 expectedShares = market.previewWithdraw(abs(expectedLiquidityDelta));

// Expected event
vm.expectEmit(address(market));
emit IERC4626.Withdraw(
address(originARM), address(originARM), address(originARM), abs(expectedAmount), expectedShares
address(originARM), address(originARM), address(originARM), abs(expectedLiquidityDelta), expectedShares
);
vm.expectEmit(address(originARM));
emit AbstractARM.Allocated(address(market), expectedAmount);
emit AbstractARM.Allocated(address(market), expectedLiquidityDelta, expectedLiquidityDelta);

// Main call
originARM.allocate();
Expand All @@ -108,17 +109,17 @@ contract Fork_Concrete_OriginARM_AllocateWithoutAdapter_Test_ is Fork_Shared_Tes
assertApproxEqAbs(marketBalanceBefore, sharesBefore, 1, "shares before");
assertApproxEqAbs(originARM.totalAssets(), DEFAULT_AMOUNT + MIN_TOTAL_SUPPLY, 1, "totalAssets before");

int256 expectedAmount = getLiquidityDelta();
uint256 expectedShares = market.previewWithdraw(abs(expectedAmount));
assertApproxEqAbs(abs(expectedAmount), DEFAULT_AMOUNT + MIN_TOTAL_SUPPLY, 1, "expectedAmount");
int256 expectedLiquidityDelta = getLiquidityDelta();
uint256 expectedShares = market.previewWithdraw(abs(expectedLiquidityDelta));
assertApproxEqAbs(abs(expectedLiquidityDelta), DEFAULT_AMOUNT + MIN_TOTAL_SUPPLY, 1, "expectedLiquidityDelta");

// Expected event
vm.expectEmit(address(market));
emit IERC4626.Withdraw(
address(originARM), address(originARM), address(originARM), abs(expectedAmount), expectedShares
address(originARM), address(originARM), address(originARM), abs(expectedLiquidityDelta), expectedShares
);
vm.expectEmit(address(originARM));
emit AbstractARM.Allocated(address(market), expectedAmount);
emit AbstractARM.Allocated(address(market), expectedLiquidityDelta, expectedLiquidityDelta);

// Main call
originARM.allocate();
Expand Down Expand Up @@ -147,14 +148,15 @@ contract Fork_Concrete_OriginARM_AllocateWithoutAdapter_Test_ is Fork_Shared_Tes

uint256 expectedShares = market.maxRedeem(address(originARM));
uint256 expectedAmount = market.convertToAssets(expectedShares);
int256 expectedLiquidityDelta = getLiquidityDelta();

// Expected event
vm.expectEmit(address(market));
emit IERC4626.Withdraw(
address(originARM), address(originARM), address(originARM), expectedAmount - 1, expectedShares
);
vm.expectEmit(address(originARM));
emit AbstractARM.Allocated(address(market), getLiquidityDelta());
emit AbstractARM.Allocated(address(market), expectedLiquidityDelta, expectedLiquidityDelta + 1 ether);
// Main call
originARM.allocate();

Expand Down
Loading