From 4269d1ee222d2de41f3a5572ab5e0b2fa28aeda7 Mon Sep 17 00:00:00 2001 From: Pranav Bhardwaj Date: Fri, 30 Aug 2024 10:52:52 -0400 Subject: [PATCH] add extrema cases --- .../adapters/TargetWeightWrapExtension.sol | 71 ++-- .../targetWeightWrapExtension.spec.ts | 318 ++++++++++++++++++ 2 files changed, 355 insertions(+), 34 deletions(-) diff --git a/contracts/adapters/TargetWeightWrapExtension.sol b/contracts/adapters/TargetWeightWrapExtension.sol index 9b821f70..536cfeb6 100644 --- a/contracts/adapters/TargetWeightWrapExtension.sol +++ b/contracts/adapters/TargetWeightWrapExtension.sol @@ -139,7 +139,6 @@ contract TargetWeightWrapExtension is BaseExtension, ReentrancyGuard { { require(isRebalancingActive, "Rebalancing is not active"); require(rebalanceInfo.targetAssets.contains(_targetAsset), "Invalid target asset"); - require(isReserveOverweight(), "Reserve asset is not overweight"); bytes memory data = abi.encodeWithSelector( wrapModule.wrap.selector, @@ -153,8 +152,8 @@ contract TargetWeightWrapExtension is BaseExtension, ReentrancyGuard { invokeManager(address(wrapModule), data); (uint256 targetAssetWeight, uint256 reserveWeight) = getTargetAssetAndReserveWeight(_targetAsset); - require(targetAssetWeight < executionParams[_targetAsset].maxTargetWeight, "Target asset overweight post-wrap"); - require(reserveWeight > rebalanceInfo.minReserveWeight, "Reserve asset underweight post-wrap"); + require(targetAssetWeight <= executionParams[_targetAsset].maxTargetWeight, "Target asset overweight post-wrap"); + require(reserveWeight >= rebalanceInfo.minReserveWeight, "Reserve asset underweight post-wrap"); } /** @@ -188,8 +187,8 @@ contract TargetWeightWrapExtension is BaseExtension, ReentrancyGuard { invokeManager(address(wrapModule), data); (uint256 targetAssetWeight, uint256 reserveWeight) = getTargetAssetAndReserveWeight(_targetAsset); - require(targetAssetWeight > executionParams[_targetAsset].minTargetWeight, "Target asset underweight post-unwrap"); - require(reserveWeight < rebalanceInfo.maxReserveWeight, "Reserve asset overweight post-unwrap"); + require(targetAssetWeight >= executionParams[_targetAsset].minTargetWeight, "Target asset underweight post-unwrap"); + require(reserveWeight <= rebalanceInfo.maxReserveWeight, "Reserve asset overweight post-unwrap"); } /* ========== Operator Functions ========== */ @@ -271,7 +270,9 @@ contract TargetWeightWrapExtension is BaseExtension, ReentrancyGuard { * @return reserveValuation The valuation of the reserve asset. */ function getReserveValuation() public view returns(uint256 reserveValuation) { - reserveValuation = setValuer.calculateComponentValuation(setToken, rebalanceInfo.reserveAsset, rebalanceInfo.reserveAsset); + reserveValuation = setToken.isComponent(rebalanceInfo.reserveAsset) + ? setValuer.calculateComponentValuation(setToken, rebalanceInfo.reserveAsset, rebalanceInfo.reserveAsset) + : 0; } /** @@ -280,7 +281,9 @@ contract TargetWeightWrapExtension is BaseExtension, ReentrancyGuard { * @return targetAssetValuation The valuation of the specified target asset. */ function getTargetAssetValuation(address _targetAsset) public view returns(uint256 targetAssetValuation) { - targetAssetValuation = setValuer.calculateComponentValuation(setToken, _targetAsset, rebalanceInfo.reserveAsset); + targetAssetValuation = setToken.isComponent(_targetAsset) + ? setValuer.calculateComponentValuation(setToken, _targetAsset, rebalanceInfo.reserveAsset) + : 0; } /** @@ -303,6 +306,33 @@ contract TargetWeightWrapExtension is BaseExtension, ReentrancyGuard { } /** + * @notice Gets the weight of a specific target asset relative to the total valuation of the SetToken. + * @dev The weight is returned as a percentage where 100% equals 1e18. + * @param _targetAsset The address of the target asset. + * @return targetAssetWeight The weight of the specified target asset relative to the SetToken's total valuation. + */ + function getTargetAssetWeight(address _targetAsset) public view returns(uint256 targetAssetWeight) { + uint256 targetAssetValuation = getTargetAssetValuation(_targetAsset); + uint256 totalValuation = getTotalValuation(); + targetAssetWeight = targetAssetValuation.preciseDiv(totalValuation); + } + + /** + * @notice Gets the weights of both the target asset and the reserve asset relative to the total valuation of the SetToken. + * @dev The weights are returned as percentages where 100% equals 1e18. + * @param _targetAsset The address of the target asset. + * @return targetAssetWeight The weight of the target asset relative to the SetToken's total valuation. + * @return reserveWeight The weight of the reserve asset relative to the SetToken's total valuation. + */ + function getTargetAssetAndReserveWeight(address _targetAsset) public view returns(uint256 targetAssetWeight, uint256 reserveWeight) { + uint256 targetAssetValuation = getTargetAssetValuation(_targetAsset); + uint256 reserveValuation = getReserveValuation(); + uint256 totalValuation = getTotalValuation(); + targetAssetWeight = targetAssetValuation.preciseDiv(totalValuation); + reserveWeight = reserveValuation.preciseDiv(totalValuation); + } + + /** * @notice Checks if the reserve asset is overweight. */ function isReserveOverweight() public view returns(bool) { @@ -332,33 +362,6 @@ contract TargetWeightWrapExtension is BaseExtension, ReentrancyGuard { return getTargetAssetWeight(_targetAsset) < executionParams[_targetAsset].minTargetWeight; } - /** - * @notice Gets the weight of a specific target asset relative to the total valuation of the SetToken. - * @dev The weight is returned as a percentage where 100% equals 1e18. - * @param _targetAsset The address of the target asset. - * @return targetAssetWeight The weight of the specified target asset relative to the SetToken's total valuation. - */ - function getTargetAssetWeight(address _targetAsset) public view returns(uint256 targetAssetWeight) { - uint256 targetAssetValuation = getTargetAssetValuation(_targetAsset); - uint256 totalValuation = getTotalValuation(); - targetAssetWeight = targetAssetValuation.preciseDiv(totalValuation); - } - - /** - * @notice Gets the weights of both the target asset and the reserve asset relative to the total valuation of the SetToken. - * @dev The weights are returned as percentages where 100% equals 1e18. - * @param _targetAsset The address of the target asset. - * @return targetAssetWeight The weight of the target asset relative to the SetToken's total valuation. - * @return reserveWeight The weight of the reserve asset relative to the SetToken's total valuation. - */ - function getTargetAssetAndReserveWeight(address _targetAsset) public view returns(uint256 targetAssetWeight, uint256 reserveWeight) { - uint256 targetAssetValuation = getTargetAssetValuation(_targetAsset); - uint256 reserveValuation = getReserveValuation(); - uint256 totalValuation = getTotalValuation(); - targetAssetWeight = targetAssetValuation.preciseDiv(totalValuation); - reserveWeight = reserveValuation.preciseDiv(totalValuation); - } - /** * @notice Gets the list of target assets that can be wrapped into or unwrapped from during rebalancing. * @return An array of addresses representing the target assets. diff --git a/test/adapters/targetWeightWrapExtension.spec.ts b/test/adapters/targetWeightWrapExtension.spec.ts index b594a56d..1c972ad5 100644 --- a/test/adapters/targetWeightWrapExtension.spec.ts +++ b/test/adapters/targetWeightWrapExtension.spec.ts @@ -724,6 +724,262 @@ describe("TargetWeightWrapExtension", async () => { }); }); + describe("#isReserveOverweight", async () => { + async function subject(): Promise { + return await targetWeightWrapExtension.isReserveOverweight(); + } + + it("should return false when the reserve is not overweight", async () => { + const actualIsReserveOverweight = await subject(); + expect(actualIsReserveOverweight).to.be.false; + }); + + context("when the reserve weight is equal to the maxReserveWeight", async () => { + beforeEach(async () => { + await targetWeightWrapExtension.connect(operator.wallet).setTargetWeights( + setV2Setup.weth.address, + await targetWeightWrapExtension.getReserveWeight(), + await targetWeightWrapExtension.getReserveWeight(), + [wrapAdapter.address], + [ + { + minTargetWeight: ether(0.40), + maxTargetWeight: ether(0.60), + wrapAdapterName: wrapAdapterName, + wrapData: ZERO_BYTES, + unwrapData: ZERO_BYTES, + } as TargetWeightWrapParams, + ] + ); + }); + + it("should return false", async () => { + const actualIsReserveOverweight = await subject(); + expect(actualIsReserveOverweight).to.be.false; + }); + }); + + context("when the reserve is overweight", async () => { + beforeEach(async () => { + await targetWeightWrapExtension.connect(operator.wallet).setTargetWeights( + setV2Setup.weth.address, + ZERO, + ZERO, + [wrapAdapter.address], + [ + { + minTargetWeight: ether(0.40), + maxTargetWeight: ether(0.60), + wrapAdapterName: wrapAdapterName, + wrapData: ZERO_BYTES, + unwrapData: ZERO_BYTES, + } as TargetWeightWrapParams, + ] + ); + }); + + it("should return true", async () => { + const actualIsReserveOverweight = await subject(); + expect(actualIsReserveOverweight).to.be.true; + }); + }); + }); + + describe("#isReserveUnderweight", async () => { + async function subject(): Promise { + return await targetWeightWrapExtension.isReserveUnderweight(); + } + + it("should return false when the reserve is not underweight", async () => { + const actualIsReserveUnderweight = await subject(); + expect(actualIsReserveUnderweight).to.be.false; + }); + + context("when the reserve weight is equal to the minReserveWeight", async () => { + beforeEach(async () => { + await targetWeightWrapExtension.connect(operator.wallet).setTargetWeights( + setV2Setup.weth.address, + await targetWeightWrapExtension.getReserveWeight(), + await targetWeightWrapExtension.getReserveWeight(), + [wrapAdapter.address], + [ + { + minTargetWeight: ether(0.40), + maxTargetWeight: ether(0.60), + wrapAdapterName: wrapAdapterName, + wrapData: ZERO_BYTES, + unwrapData: ZERO_BYTES, + } as TargetWeightWrapParams, + ] + ); + }); + + it("should return false", async () => { + const actualIsReserveUnderweight = await subject(); + expect(actualIsReserveUnderweight).to.be.false; + }); + }); + + context("when the reserve is underweight", async () => { + beforeEach(async () => { + await targetWeightWrapExtension.connect(operator.wallet).setTargetWeights( + setV2Setup.weth.address, + ether(1), + ether(1), + [wrapAdapter.address], + [ + { + minTargetWeight: ether(0.40), + maxTargetWeight: ether(0.60), + wrapAdapterName: wrapAdapterName, + wrapData: ZERO_BYTES, + unwrapData: ZERO_BYTES, + } as TargetWeightWrapParams, + ] + ); + }); + + it("should return true", async () => { + const actualIsReserveUnderweight = await subject(); + expect(actualIsReserveUnderweight).to.be.true; + }); + }); + }); + + describe("#isTargetOverweight", async () => { + let subjectTargetAsset: Address; + + beforeEach(async () => { + subjectTargetAsset = wrapAdapter.address; + }); + + async function subject(): Promise { + return await targetWeightWrapExtension.isTargetOverweight(subjectTargetAsset); + } + + it("should return false when the target is not overweight", async () => { + const actualIsTargetOverweight = await subject(); + expect(actualIsTargetOverweight).to.be.false; + }); + + context("when the target weight is equal to the maxTargetWeight", async () => { + beforeEach(async () => { + await targetWeightWrapExtension.connect(operator.wallet).setTargetWeights( + setV2Setup.weth.address, + ether(0.45), + ether(0.55), + [wrapAdapter.address], + [ + { + minTargetWeight: await targetWeightWrapExtension.getTargetAssetWeight(subjectTargetAsset), + maxTargetWeight: await targetWeightWrapExtension.getTargetAssetWeight(subjectTargetAsset), + wrapAdapterName: wrapAdapterName, + wrapData: ZERO_BYTES, + unwrapData: ZERO_BYTES, + } as TargetWeightWrapParams, + ] + ); + }); + + it("should return false", async () => { + const actualIsTargetOverweight = await subject(); + expect(actualIsTargetOverweight).to.be.false; + }); + }); + + context("when the target is overweight", async () => { + beforeEach(async () => { + await targetWeightWrapExtension.connect(operator.wallet).setTargetWeights( + setV2Setup.weth.address, + ether(0.45), + ether(0.55), + [wrapAdapter.address], + [ + { + minTargetWeight: ZERO, + maxTargetWeight: ZERO, + wrapAdapterName: wrapAdapterName, + wrapData: ZERO_BYTES, + unwrapData: ZERO_BYTES, + } as TargetWeightWrapParams, + ] + ); + }); + + it("should return true", async () => { + const actualIsTargetOverweight = await subject(); + expect(actualIsTargetOverweight).to.be.true; + }); + }); + }); + + describe("#isTargetUnderweight", async () => { + let subjectTargetAsset: Address; + + beforeEach(async () => { + subjectTargetAsset = wrapAdapter.address; + }); + + async function subject(): Promise { + return await targetWeightWrapExtension.isTargetUnderweight(subjectTargetAsset); + } + + it("should return false when the target is not underweight", async () => { + const actualIsTargetUnderweight = await subject(); + expect(actualIsTargetUnderweight).to.be.false; + }); + + context("when the target weight is equal to the minTargetWeight", async () => { + beforeEach(async () => { + await targetWeightWrapExtension.connect(operator.wallet).setTargetWeights( + setV2Setup.weth.address, + ether(0.45), + ether(0.55), + [wrapAdapter.address], + [ + { + minTargetWeight: await targetWeightWrapExtension.getTargetAssetWeight(subjectTargetAsset), + maxTargetWeight: await targetWeightWrapExtension.getTargetAssetWeight(subjectTargetAsset), + wrapAdapterName: wrapAdapterName, + wrapData: ZERO_BYTES, + unwrapData: ZERO_BYTES, + } as TargetWeightWrapParams, + ] + ); + }); + + it("should return false", async () => { + const actualIsTargetUnderweight = await subject(); + expect(actualIsTargetUnderweight).to.be.false; + }); + }); + + context("when the target is underweight", async () => { + beforeEach(async () => { + await targetWeightWrapExtension.connect(operator.wallet).setTargetWeights( + setV2Setup.weth.address, + ether(0.45), + ether(0.55), + [wrapAdapter.address], + [ + { + minTargetWeight: ether(1), + maxTargetWeight: ether(1), + wrapAdapterName: wrapAdapterName, + wrapData: ZERO_BYTES, + unwrapData: ZERO_BYTES, + } as TargetWeightWrapParams, + ] + ); + }); + + it("should return true", async () => { + const actualIsTargetUnderweight = await subject(); + expect(actualIsTargetUnderweight).to.be.true; + }); + }); + }); + describe("#wrap", async () => { let subjectTargetAsset: Address; let subjectReserveUnits: BigNumber; @@ -764,6 +1020,37 @@ describe("TargetWeightWrapExtension", async () => { expect(reservePositionUnitChange).to.be.lte(subjectReserveUnits.add(2)); }); + context("when fully allocating the reserve", async () => { + beforeEach(async () => { + await targetWeightWrapExtension.connect(operator.wallet).setTargetWeights( + setV2Setup.weth.address, + ether(0), + ether(1), + [wrapAdapter.address], + [ + { + minTargetWeight: ether(0), + maxTargetWeight: ether(1), + wrapAdapterName: wrapAdapterName, + wrapData: ZERO_BYTES, + unwrapData: ZERO_BYTES, + } as TargetWeightWrapParams, + ] + ); + + subjectTargetAsset = wrapAdapter.address; + subjectReserveUnits = await setToken.getDefaultPositionRealUnit(setV2Setup.weth.address); + }); + + it("should be able to remove the reserve asset from the SetToken", async () => { + expect(await setToken.isComponent(setV2Setup.weth.address)).to.be.true; + + await subject(); + + expect(await setToken.isComponent(setV2Setup.weth.address)).to.be.false; + }); + }); + context("when isRebalancingActive is false", async () => { beforeEach(async () => { await targetWeightWrapExtension.connect(operator.wallet).pauseRebalance(); @@ -866,6 +1153,37 @@ describe("TargetWeightWrapExtension", async () => { expect(reservePositionUnitChange).to.be.lte(subjectTargetUnits.add(2)); }); + context("when removing a component", async () => { + beforeEach(async () => { + await targetWeightWrapExtension.connect(operator.wallet).setTargetWeights( + setV2Setup.weth.address, + ether(0.45), + ether(1), + [wrapAdapter.address], + [ + { + minTargetWeight: ZERO, + maxTargetWeight: ZERO, + wrapAdapterName: wrapAdapterName, + wrapData: ZERO_BYTES, + unwrapData: ZERO_BYTES, + } as TargetWeightWrapParams, + ] + ); + + subjectTargetAsset = wrapAdapter.address; + subjectTargetUnits = ether(1); + }); + + it("should be able to remove the component from the SetToken", async () => { + expect(await setToken.isComponent(wrapAdapter.address)).to.be.true; + + await subject(); + + expect(await setToken.isComponent(wrapAdapter.address)).to.be.false; + }); + }); + context("when isRebalancingActive is false", async () => { beforeEach(async () => { await targetWeightWrapExtension.connect(operator.wallet).pauseRebalance();