diff --git a/.gitmodules b/.gitmodules index bf9e9133..8fc2c8d5 100644 --- a/.gitmodules +++ b/.gitmodules @@ -22,3 +22,6 @@ path = dep/tapioca-mocks url = https://github.com/Tapioca-DAO/tapioca-mocks branch = main +[submodule "dep/tapiocaz"] + path = dep/tapiocaz + url = https://github.com/Tapioca-DAO/tapiocaz diff --git a/contracts/governance/twTAP.sol b/contracts/governance/twTAP.sol index 7568d32c..3ddc8c96 100644 --- a/contracts/governance/twTAP.sol +++ b/contracts/governance/twTAP.sol @@ -146,6 +146,8 @@ contract TwTAP is ITwTapMagnitudeMultiplier public twTapMagnitudeMultiplier; uint256 constant MULTIPLIER_PRECISION = 1e18; + uint256 public constant REWARD_MULTIPLIER_BRACKET = 100_000; // 10% brackets + uint256 public constant REWARD_CAP_BRACKET = 50_000; // 5% cap to floor/ceil the reward /// @notice The minimum amount of weeks to start decaying the cumulative uint256 public minWeeksToDecay = 2; @@ -412,7 +414,11 @@ contract TwTAP is // Revert if the lock is x time bigger than the cumulative if (magnitude > (lastEpochCumulative * growthCapBps) / 1e4) revert NotValid(); - uint256 multiplier = computeTarget(dMIN, dMAX, magnitude * _amount, pool.cumulative * pool.totalDeposited); + uint256 multiplier = capCumulativeReward( + computeTarget(dMIN, dMAX, magnitude * _amount, pool.cumulative * pool.totalDeposited), + REWARD_MULTIPLIER_BRACKET, + REWARD_CAP_BRACKET + ); // Calculate twAML voting weight bool divergenceForce; @@ -737,7 +743,7 @@ contract TwTAP is } /** - * @notice Set the minimum amount of weeks to start decaying the cumulative + * @notice Set the minimum amount of weeks to start decaying the cumulative */ function setMinWeeksToDecay(uint256 _minWeeksToDecay) external onlyOwner { minWeeksToDecay = _minWeeksToDecay; diff --git a/contracts/options/TapiocaOptionBroker.sol b/contracts/options/TapiocaOptionBroker.sol index e4eb347f..3ef67bb7 100644 --- a/contracts/options/TapiocaOptionBroker.sol +++ b/contracts/options/TapiocaOptionBroker.sol @@ -105,6 +105,8 @@ contract TapiocaOptionBroker is Pausable, Ownable, PearlmitHandler, IERC721Recei ITobMagnitudeMultiplier public tobMagnitudeMultiplier; uint256 constant MULTIPLIER_PRECISION = 1e18; + uint256 public constant REWARD_MULTIPLIER_BRACKET = 50_000; // 5% brackets + uint256 public constant REWARD_CAP_BRACKET = 20_000; // 2% cap to floor/ceil the reward /// @notice The minimum amount of weeks to start decaying the cumulative uint256 public minWeeksToDecay = 2; @@ -347,7 +349,11 @@ contract TapiocaOptionBroker is Pausable, Ownable, PearlmitHandler, IERC721Recei uint256 target; { (uint256 totalPoolShares,) = tOLP.getTotalPoolDeposited(uint256(lock.sglAssetID)); - target = computeTarget(dMIN, dMAX, magnitude * uint256(lock.ybShares), pool.cumulative * totalPoolShares); + target = capCumulativeReward( + computeTarget(dMIN, dMAX, magnitude * uint256(lock.ybShares), pool.cumulative * totalPoolShares), + REWARD_MULTIPLIER_BRACKET, + REWARD_CAP_BRACKET + ); } // Revert if the lock 4x the last epoch cumulative diff --git a/contracts/options/twAML.sol b/contracts/options/twAML.sol index eba579c8..99eb11e9 100644 --- a/contracts/options/twAML.sol +++ b/contracts/options/twAML.sol @@ -154,4 +154,20 @@ abstract contract TWAML is FullMath { z = 1; } } + + /** + * @notice Will ceil/floor the reward amount to the nearest number by increment of `_multiplier`. + * @dev e.g: tOB: 1, multiplier: 5, cap: 2 + * twTap: 1, multiplier: 10, cap: 5 + */ + function capCumulativeReward(uint256 _amount, uint256 _multiplier, uint256 _cap) internal pure returns (uint256) { + uint256 remainder = _amount % _multiplier; + if (remainder == 0) { + return _amount; + } + if (remainder > _cap) { + return _amount + _multiplier - remainder; + } + return _amount - remainder; + } } diff --git a/dep/tapiocaz b/dep/tapiocaz new file mode 160000 index 00000000..4b54948a --- /dev/null +++ b/dep/tapiocaz @@ -0,0 +1 @@ +Subproject commit 4b54948a26e583d3837ff05b6f2b178883a18859 diff --git a/test/unit/UnitBaseTest.sol b/test/unit/UnitBaseTest.sol index 38525934..8bd568ab 100644 --- a/test/unit/UnitBaseTest.sol +++ b/test/unit/UnitBaseTest.sol @@ -126,7 +126,9 @@ contract UnitBaseTest is TestHelper { address public ENDPOINT_B; function setUp() public virtual override { + vm.label(adminAddr, "Admin"); vm.label(aliceAddr, "Alice"); + vm.label(bobAddr, "Bob"); // Peripheral contracts pearlmit = createPearlmit(adminAddr); @@ -280,14 +282,14 @@ contract UnitBaseTest is TestHelper { address _mainToken, IPearlmit _pearlmit, address _owner - ) internal returns (Penrose penrose, BigBang bbMasterContract, Singularity sglMasterContract) { + ) internal returns (Penrose _penrose, BigBang bbMasterContract, Singularity sglMasterContract) { address tapStrat = address(createYieldBoxEmptyStrategy(_yb, _tap)); address mainTokenStrat = address(createYieldBoxEmptyStrategy(_yb, _mainToken)); uint256 tapAssetId = IYieldBox(_yb).registerAsset(TokenType.ERC20, _tap, tapStrat, 0); uint256 mainTokenAssetId = IYieldBox(_yb).registerAsset(TokenType.ERC20, _mainToken, mainTokenStrat, 0); - penrose = new Penrose( + _penrose = new Penrose( IYieldBox(_yb), ICluster(_cluster), BoringIERC20(_tap), @@ -301,9 +303,9 @@ contract UnitBaseTest is TestHelper { bbMasterContract = new BigBang(); sglMasterContract = new Singularity(); vm.startPrank(adminAddr); - penrose.registerBigBangMasterContract(address(bbMasterContract), IPenrose.ContractType.mediumRisk); - penrose.registerSingularityMasterContract(address(sglMasterContract), IPenrose.ContractType.mediumRisk); - penrose.setUsdoToken(address(usdoMock), usdoMockTokenId); + _penrose.registerBigBangMasterContract(address(bbMasterContract), IPenrose.ContractType.mediumRisk); + _penrose.registerSingularityMasterContract(address(sglMasterContract), IPenrose.ContractType.mediumRisk); + _penrose.setUsdoToken(address(usdoMock), usdoMockTokenId); vm.stopPrank(); } diff --git a/test/unit/options/twL/twl_capCumulativeReward.t.sol b/test/unit/options/twL/twl_capCumulativeReward.t.sol new file mode 100644 index 00000000..79577527 --- /dev/null +++ b/test/unit/options/twL/twl_capCumulativeReward.t.sol @@ -0,0 +1,46 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity 0.8.22; + +import {TWAML} from "contracts/options/twAML.sol"; + +import "forge-std/Test.sol"; + +contract twl_CapCumulativeReward is TWAML, Test { + uint256 constant TOB_MULTIPLIER = 5 * 1e4; + uint256 constant TOB_CAP = 2 * 1e4; + uint256 constant MIN_TOB_AMOUNT = 1e4; + uint256 constant MAX_TOB_AMOUNT = 50 * 1e4; + + uint256 constant TWTAP_MULTIPLIER = 10 * 1e4; + uint256 constant TWTAP_CAP = 5 * 1e4; + uint256 constant MIN_TWTAP_AMOUNT = 1e4; + uint256 constant MAX_TWTAP_AMOUNT = 100 * 1e4; + + /** + * @dev It should cap the cumulative reward to the nearest multiplier of 5. + */ + function test_shouldCapCumulativeRewardOnTob(uint256 _amount) external { + _amount = bound(_amount, MIN_TOB_AMOUNT, MAX_TOB_AMOUNT); + + // it should cap the cumulative reward + assertEq( + capCumulativeReward(_amount, TOB_MULTIPLIER, TOB_CAP) % TOB_MULTIPLIER, + 0, + "twl_CapCumulativeReward::test_shouldCapCumulativeRewardOnTob: Invalid amount" + ); + } + + /** + * @dev It should cap the cumulative reward to the nearest multiplier of 10. + */ + function test_shouldCapCumulativeRewardOnTwTap(uint256 _amount) external { + _amount = bound(_amount, MIN_TWTAP_AMOUNT, MAX_TWTAP_AMOUNT); + + // it should cap the cumulative reward + assertEq( + capCumulativeReward(_amount, TWTAP_MULTIPLIER, TWTAP_CAP) % TWTAP_MULTIPLIER, + 0, + "twl_CapCumulativeReward::test_shouldCapCumulativeRewardOnTwTap: Invalid amount" + ); + } +} diff --git a/test/unit/options/twTap/twTapBaseTest.sol b/test/unit/options/twTap/twTapBaseTest.sol index a8d01bda..75ef6e7a 100644 --- a/test/unit/options/twTap/twTapBaseTest.sol +++ b/test/unit/options/twTap/twTapBaseTest.sol @@ -35,6 +35,8 @@ contract twTapBaseTest is UnitBaseTest { // Constants uint256 public EPOCH_DURATION = 1 weeks; + uint256 public MIN_LOCK_DURATION = 4; + uint256 public MAX_LOCK_DURATION = 6; // Addresses address public PAYMENT_TOKEN_RECEIVER = address(bytes20(keccak256("PAYMENT_TOKEN_RECEIVER"))); @@ -124,4 +126,23 @@ contract twTapBaseTest is UnitBaseTest { twTap.participate(aliceAddr, _amount, _duration * twTap.EPOCH_DURATION()); vm.stopPrank(); } + + function _participate(address _to, uint256 _amount, uint256 _duration) internal { + vm.startPrank(_to); + tapOFT.freeMint(_to, _amount); + tapOFT.approve(address(pearlmit), _amount); + pearlmit.approve(20, address(tapOFT), 0, address(twTap), uint200(_amount), uint48(block.timestamp + 1)); + twTap.participate(_to, _amount, _duration * twTap.EPOCH_DURATION()); + vm.stopPrank(); + } + + function _boundValues(uint256 _lockAmount, uint256 _lockDuration) internal returns (uint256, uint256) { + _lockAmount = bound(_lockAmount, 1, type(uint88).max); + _lockDuration = bound(_lockDuration, MIN_LOCK_DURATION, MAX_LOCK_DURATION); + return (_lockAmount, _lockDuration); + } + + function _toWeek(uint256 _timestamp) internal returns (uint256) { + return (_timestamp - twTap.creation()) / twTap.EPOCH_DURATION(); + } } diff --git a/test/unit/options/twTap/twTap_advanceWeek.t.sol b/test/unit/options/twTap/twTap_advanceWeek.t.sol new file mode 100644 index 00000000..13e7d398 --- /dev/null +++ b/test/unit/options/twTap/twTap_advanceWeek.t.sol @@ -0,0 +1,194 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity 0.8.22; + +import {twTapBaseTest, TwTAP} from "test/unit/options/twTap/twTapBaseTest.sol"; +import {WeekTotals, Participation} from "contracts/governance/twTAP.sol"; + +contract twTap_advanceWeek is twTapBaseTest { + uint256 constant PARTICIPATION_ID = 1; + + modifier whenParticipating(address _to, uint256 _lockAmount, uint256 _lockDuration) { + (_lockAmount, _lockDuration) = _boundValues(_lockAmount, _lockDuration); + _participate(_to, _lockAmount, _lockDuration); + _; + } + + function test_RevertWhen_CallerDoesNotHaveRole() external { + // it should revert + vm.expectRevert(TwTAP.NotAuthorized.selector); + twTap.advanceWeek(1); + } + + modifier whenCallerHasRole() { + _; + _resetPrank(adminAddr); + cluster.setRoleForContract(adminAddr, keccak256("NEW_EPOCH"), true); + } + + function test_WhenTimeDidNotAdvanceEnoughToAdvanceWeek() external whenCallerHasRole { + // it should do nothing + vm.startPrank(adminAddr); + twTap.advanceWeek(1); + + assertEq( + twTap.lastProcessedWeek(), + 0, + "twTap_advanceWeek::test_WhenTimeDidNotAdvanceEnoughToAdvanceWeek: Invalid epoch" + ); + } + + modifier whenTimeAdvancedEnough() { + skip(twTap.EPOCH_DURATION()); + _; + } + + function test_WhenTimeAdvancedEnough(uint256 _lockAmount, uint256 _lockDuration) + external + whenParticipating(aliceAddr, _lockAmount, _lockDuration) + whenCallerHasRole + whenTimeAdvancedEnough + { + _resetPrank(adminAddr); + + // it should emit AdvanceEpoch + vm.expectEmit(true, true, false, false); + emit TwTAP.AdvanceEpoch(1, 0); + twTap.advanceWeek(1); + + // it should pass week net active votes + int256 netActiveVotesBef = twTap.weekTotals(0); + int256 netActiveVotesAft = twTap.weekTotals(1); + assertGt( + netActiveVotesAft, + netActiveVotesBef, + "twTap_advanceWeek::test_WhenTimeAdvancedEnough: Invalid week net active votes" + ); + + // it should update lastProcessedWeek + assertEq(twTap.lastProcessedWeek(), 1, "twTap_advanceWeek::test_WhenTimeAdvancedEnough: Invalid epoch"); + } + + function test_WhenDecayRateIsNotSet(uint256 _lockAmount, uint256 _lockDuration) + external + whenParticipating(aliceAddr, _lockAmount, _lockDuration) + whenCallerHasRole + whenTimeAdvancedEnough + { + _resetPrank(adminAddr); + + // it should do nothing + (,,, uint256 cumulativeBef) = twTap.twAML(); + twTap.advanceWeek(1); + (,,, uint256 cumulativeAft) = twTap.twAML(); + assertEq(cumulativeAft, cumulativeBef, "twTap_advanceWeek::test_WhenDecayRateIsNotSet: Invalid cumulative"); + } + + modifier whenDecayRateIsBiggerThan0() { + _resetPrank(adminAddr); + twTap.setDecayRateBps(1000); + _; + } + + function test_RevertWhen_EpochSmallerThan2(uint256 _lockAmount, uint256 _lockDuration) + external + whenParticipating(aliceAddr, _lockAmount, _lockDuration) + whenCallerHasRole + whenTimeAdvancedEnough + whenDecayRateIsBiggerThan0 + { + // it should revert + _resetPrank(adminAddr); + vm.expectRevert(TwTAP.EpochTooLow.selector); + twTap.advanceWeek(1); + } + + /// @notice We already skipped one epoch in `whenTimeAdvancedEnough()` + modifier whenEpochBiggerOrEqual2(uint256 _lockDuration) { + uint256 unlockEpoch = _getUnlockEpoch(); + skip(twTap.EPOCH_DURATION() * (unlockEpoch - 1)); + _; + } + + modifier whenLiquidityDecreased() { + twTap.exitPosition(1); // First we need to exit the position to decrease the liquidity + _; + } + + function test_WhenLiquidityDecreasedMoreThanTheDecayActivation(uint256 _lockAmount, uint256 _lockDuration) + external + whenParticipating(aliceAddr, _lockAmount, _lockDuration) + whenCallerHasRole + whenTimeAdvancedEnough + whenEpochBiggerOrEqual2(_lockDuration) + whenDecayRateIsBiggerThan0 + whenLiquidityDecreased + { + _resetPrank(adminAddr); + + (,,, uint256 cumulativeBef) = twTap.twAML(); + // We need to skip the decay activation + skip(twTap.EPOCH_DURATION()); + twTap.advanceWeek(_getUnlockEpoch()); + (,,, uint256 cumulativeAft) = twTap.twAML(); + + // it should decay + assertGt( + cumulativeBef, + cumulativeAft, + "twTap_advanceWeek::test_WhenLiquidityDecreasedMoreThanTheDecayActivation: Invalid cumulative" + ); + } + + function test_WhenLiquidityDidNotDecreasedMoreThanHeDecayActivation(uint256 _lockAmount, uint256 _lockDuration) + external + whenParticipating(aliceAddr, 1, _lockDuration) + whenCallerHasRole + whenTimeAdvancedEnough + whenDecayRateIsBiggerThan0 + whenEpochBiggerOrEqual2(_lockDuration) + whenLiquidityDecreased + { + // it should not decay + + (,,, uint256 cumulativeBef) = twTap.twAML(); + skip(twTap.EPOCH_DURATION()); + twTap.advanceWeek(_getUnlockEpoch()); + (,,, uint256 cumulativeAft) = twTap.twAML(); + + // it should decay + Participation memory participation = twTap.getParticipation(PARTICIPATION_ID); + assertEq( + cumulativeBef, + cumulativeBef - participation.averageMagnitude, + "twTap_advanceWeek::test_WhenLiquidityDidNotDecreasedMoreThanHeDecayActivation: Invalid cumulative" + ); + } + + function test_WhenLiquidityDidNotDecrease(uint256 _lockAmount, uint256 _lockDuration) + external + whenParticipating(aliceAddr, _lockAmount, _lockDuration) + whenCallerHasRole + whenTimeAdvancedEnough + whenDecayRateIsBiggerThan0 + whenEpochBiggerOrEqual2(_lockDuration) + { + // it should not decay + _resetPrank(adminAddr); + + (,,, uint256 cumulativeBef) = twTap.twAML(); + // We need to skip the decay activation + skip(twTap.EPOCH_DURATION()); + twTap.advanceWeek(1); + (,,, uint256 cumulativeAft) = twTap.twAML(); + + // it should decay + assertEq( + cumulativeBef, cumulativeAft, "twTap_advanceWeek::test_WhenLiquidityDidNotDecrease: Invalid cumulative" + ); + } + + function _getUnlockEpoch() internal returns (uint256) { + Participation memory participation = twTap.getParticipation(PARTICIPATION_ID); + return _toWeek(participation.expiry); + } +} diff --git a/test/unit/options/twTap/twTap_advanceWeek.tree b/test/unit/options/twTap/twTap_advanceWeek.tree new file mode 100644 index 00000000..e4401099 --- /dev/null +++ b/test/unit/options/twTap/twTap_advanceWeek.tree @@ -0,0 +1,23 @@ +twTap_advanceWeek +├── when caller does not have role +│ └── it should revert +└── when caller has role + ├── when time did not advance enough to advance week + │ └── it should do nothing + └── when time advanced enough + ├── it should emit AdvanceEpoch + ├── it should pass week net active votes + ├── it should update lastProcessedWeek + ├── when decay rate is not set + │ └── it should do nothing + └── when decay rate is bigger than 0 + ├── when epoch smaller than 2 + │ └── it should revert + └── when epoch bigger or equal 2 + ├── when liquidity decreased + │ ├── when liquidity decreased more than the decay activation + │ │ └── it should decay + │ └── when liquidity did not decreased more than he decay activation + │ └── it should not decay + └── when liquidity did not decrease + └── it should not decay \ No newline at end of file diff --git a/test/unit/options/twTap/twTap_exitPosition.t.sol b/test/unit/options/twTap/twTap_exitPosition.t.sol index 8ce9f89e..e4961fa1 100644 --- a/test/unit/options/twTap/twTap_exitPosition.t.sol +++ b/test/unit/options/twTap/twTap_exitPosition.t.sol @@ -173,10 +173,4 @@ contract twTap_exitPosition is twTapBaseTest, TWAML { tapOFT.balanceOf(aliceAddr), _lockAmount, "twTap_exitPosition::test_WhenItShouldContinue: Invalid balance" ); } - - function _boundValues(uint256 _lockAmount, uint256 _lockDuration) internal pure returns (uint256, uint256) { - _lockAmount = bound(_lockAmount, 1, type(uint88).max); - _lockDuration = bound(_lockDuration, 1, 4); - return (_lockAmount, _lockDuration); - } } diff --git a/test/unit/options/twTap/twTap_participate.t.sol b/test/unit/options/twTap/twTap_participate.t.sol index 53dc3047..387fa652 100644 --- a/test/unit/options/twTap/twTap_participate.t.sol +++ b/test/unit/options/twTap/twTap_participate.t.sol @@ -67,8 +67,7 @@ contract twTap_participate is twTapBaseTest, TWAML { whenLockDurationIsMoreThanAWeek whenLockDurationIsLessThanMaxDuration { - (_lockAmount, _lockDuration) = _boundValues(_lockAmount, _lockDuration); - _lockDuration = _lockDuration == twTap.EPOCH_DURATION() ? _lockDuration + 1 : _lockDuration - 1; + _lockDuration = twTap.EPOCH_DURATION() + 1; // it should revert vm.expectRevert(TwTAP.DurationNotMultiple.selector); @@ -87,9 +86,10 @@ contract twTap_participate is twTapBaseTest, TWAML { whenLockDurationIsAMultipleOfEpochDuration { (_lockAmount, _lockDuration) = _boundValues(_lockAmount, _lockDuration); - skip(twTap.EPOCH_DURATION()); + skip(twTap.EPOCH_DURATION() * _lockDuration); // it should revert + uint256 _lockDuration = _lockDuration * twTap.EPOCH_DURATION(); vm.expectRevert(TwTAP.AdvanceWeekFirst.selector); twTap.participate(aliceAddr, _lockAmount, _lockDuration); } @@ -111,8 +111,8 @@ contract twTap_participate is twTapBaseTest, TWAML { // it should revert // It should be TwTap.TransferFailed.selector, // for simplicity we use vm.expectRevert() if we don't permit it + uint256 _lockDuration = _lockDuration * twTap.EPOCH_DURATION(); vm.expectRevert(); - vm.expectRevert(TwTAP.AdvanceWeekFirst.selector); twTap.participate(aliceAddr, _lockAmount, _lockDuration); } @@ -141,6 +141,7 @@ contract twTap_participate is twTapBaseTest, TWAML { tapOFT.freeMint(aliceAddr, _lockAmount); // it should participate without changing AML + uint256 _lockDuration = _lockDuration * twTap.EPOCH_DURATION(); test_WhenItShouldParticipate(_lockAmount, _lockDuration, false); (uint256 totalParticipants, uint256 averageMagnitude, uint256 totalDeposited, uint256 cumulative) = @@ -178,6 +179,7 @@ contract twTap_participate is twTapBaseTest, TWAML { tapOFT.freeMint(aliceAddr, _lockAmount); // it should participate and change AML + uint256 _lockDuration = _lockDuration * twTap.EPOCH_DURATION(); test_WhenItShouldParticipate(_lockAmount, _lockDuration, true); } @@ -287,10 +289,4 @@ contract twTap_participate is twTapBaseTest, TWAML { function _timestampToWeek(uint256 _timestamp) internal view returns (uint256) { return (_timestamp - twTap.creation()) / twTap.EPOCH_DURATION(); } - - function _boundValues(uint256 _lockAmount, uint256 _lockDuration) internal returns (uint256, uint256) { - _lockAmount = bound(_lockAmount, 1, type(uint88).max); - _lockDuration = twTap.EPOCH_DURATION() * bound(_lockDuration, 4, 6); - return (_lockAmount, _lockDuration); - } } diff --git a/tsconfig.json b/tsconfig.json index e6f45991..145b8ea9 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -13,11 +13,11 @@ "skipLibCheck": true, "baseUrl": ".", "paths": { - "@tapioca-sdk/*": ["lib/tapioca-sdk/src/*"], + "@tapioca-sdk/*": ["dep/tapioca-sdk/src/*"], "@typechain/*": ["gen/typechain/*"], - "@tapioca-periph/config":["lib/tap-utils/tasks/deploy/DEPLOY_CONFIG.ts"], - "@tapioca-bar/config":["lib/tapioca-bar/tasks/deploy/DEPLOY_CONFIG.ts"], - "@tapiocaz/config":["lib/tapiocaz/tasks/deploy/DEPLOY_CONFIG.ts"], + "@tapioca-periph/config":["dep/tap-utils/tasks/deploy/DEPLOY_CONFIG.ts"], + "@tapioca-bar/config":["dep/tapioca-bar/tasks/deploy/DEPLOY_CONFIG.ts"], + "@tapiocaz/config":["dep/tapiocaz/tasks/deploy/DEPLOY_CONFIG.ts"], } }, "include": [