diff --git a/markets/treasury-market/contracts/TreasuryMarket.sol b/markets/treasury-market/contracts/TreasuryMarket.sol index f0bfca7a16..c4a5ee8564 100644 --- a/markets/treasury-market/contracts/TreasuryMarket.sol +++ b/markets/treasury-market/contracts/TreasuryMarket.sol @@ -206,13 +206,44 @@ contract TreasuryMarket is ITreasuryMarket, Ownable, UUPSImplementation, IMarket emit LoanAdjusted(accountId, accountDebt.toUint(), 0); } else { // this depositor is eligible for possible rewards - //DepositRewardConfiguration[] memory drc = depositRewardConfigurations; for (uint256 i = 0; i < depositRewardConfigurations.length; i++) { - DepositRewardConfiguration memory config = depositRewardConfigurations[i]; - uint256 rewardAmount = accountCollateral - .mulDecimal(oracleManager.process(config.valueRatioOracle).price.toUint()) - .mulDecimal(config.percent); - + DepositRewardConfiguration storage config = depositRewardConfigurations[i]; + + uint256 rewardAmount; + { + uint256 remainingRewardableCollateral = accountCollateral; + + // we give the user as much rewards as we can give them until all rewards are exhausted + for ( + uint256 j = 0; + j < config.amounts.length && remainingRewardableCollateral > 0; + j++ + ) { + if (config.amounts[j].maxCollateral == 0) { + continue; + } + + uint256 rewardedCollateral = remainingRewardableCollateral < + config.amounts[j].maxCollateral + ? remainingRewardableCollateral + : config.amounts[j].maxCollateral; + + rewardAmount += rewardedCollateral + .mulDecimal( + oracleManager.process(config.valueRatioOracle).price.toUint() + ) + .mulDecimal(config.amounts[j].percent); + + remainingRewardableCollateral -= rewardedCollateral; + config.amounts[j].maxCollateral -= uint128(rewardedCollateral); + } + + if (remainingRewardableCollateral > 0) { + // the user was probably expecting to get some rewards so go ahead and revert to ensure they do not enter the pool + // if they are not getting those rewards + revert InsufficientAvailableReward(config.token, accountCollateral, 0); + } + } // stack was too deep to set this as a local variable. annoying. depositRewards[accountId][config.token] = LoanInfo( uint64(block.timestamp), @@ -508,7 +539,22 @@ contract TreasuryMarket is ITreasuryMarket, Ownable, UUPSImplementation, IMarket if (depositRewardConfigurations.length <= i) { depositRewardConfigurations.push(); } - depositRewardConfigurations[i] = newDrcs[i]; + + // NOTE: not possible to simply copy to storage here because of limitation of solidity compiler + depositRewardConfigurations[i].token = newDrcs[i].token; + depositRewardConfigurations[i].power = newDrcs[i].power; + depositRewardConfigurations[i].duration = newDrcs[i].duration; + depositRewardConfigurations[i].valueRatioOracle = newDrcs[i].valueRatioOracle; + depositRewardConfigurations[i].penaltyStart = newDrcs[i].penaltyStart; + depositRewardConfigurations[i].penaltyEnd = newDrcs[i].penaltyEnd; + + for (uint256 k = 0; k < depositRewardConfigurations[i].amounts.length; k++) { + depositRewardConfigurations[i].amounts.pop(); + } + for (uint256 k = 0; k < newDrcs.length; k++) { + depositRewardConfigurations[i].amounts.push(); + depositRewardConfigurations[i].amounts[k] = newDrcs[i].amounts[k]; + } // ensure that the v3 core system can pull funds from us IERC20(newDrcs[i].token).approve(address(v3System), type(uint256).max); diff --git a/markets/treasury-market/contracts/interfaces/ITreasuryMarket.sol b/markets/treasury-market/contracts/interfaces/ITreasuryMarket.sol index 2bff313545..abe6495954 100644 --- a/markets/treasury-market/contracts/interfaces/ITreasuryMarket.sol +++ b/markets/treasury-market/contracts/interfaces/ITreasuryMarket.sol @@ -7,14 +7,19 @@ import "./external/IV3CoreProxy.sol"; * @title Synthetix V3 Market allowing for a trusted entity to manage excess liquidity allocated to a liquidity pool. */ interface ITreasuryMarket { + struct DepositRewardAmounts { + uint128 percent; + uint128 maxCollateral; + } + struct DepositRewardConfiguration { address token; uint32 power; uint32 duration; - uint128 percent; bytes32 valueRatioOracle; uint128 penaltyStart; uint128 penaltyEnd; + DepositRewardAmounts[] amounts; } struct LoanInfo { diff --git a/markets/treasury-market/test/TreasuryMarket.test.sol b/markets/treasury-market/test/TreasuryMarket.test.sol index e557222efe..bca962be4c 100644 --- a/markets/treasury-market/test/TreasuryMarket.test.sol +++ b/markets/treasury-market/test/TreasuryMarket.test.sol @@ -119,33 +119,40 @@ contract TreasuryMarketTest is Test, IERC721Receiver { vm.prank(v3System.owner()); v3System.setFeatureFlagAllowAll("associateDebt", true); + ITreasuryMarket.DepositRewardAmounts[] + memory amnts = new ITreasuryMarket.DepositRewardAmounts[](2); + amnts[0].maxCollateral = 1 ether; + amnts[0].percent = 0.2 ether; + amnts[1].maxCollateral = 99 ether; + amnts[1].percent = 0.2 ether; + ITreasuryMarket.DepositRewardConfiguration[] memory dcr = new ITreasuryMarket.DepositRewardConfiguration[](2); dcr[0] = ITreasuryMarket.DepositRewardConfiguration({ token: address(collateralToken), power: 1, duration: 86400, - percent: 0.2 ether, valueRatioOracle: NodeModule(0x83A0444B93927c3AFCbe46E522280390F748E171).registerNode( NodeDefinition.NodeType.CHAINLINK, abi.encode(address(mockAggregator), uint256(0), uint8(18)), parents ), penaltyStart: 1.0 ether, - penaltyEnd: 0.5 ether + penaltyEnd: 0.5 ether, + amounts: amnts }); dcr[1] = ITreasuryMarket.DepositRewardConfiguration({ token: address(usdToken), power: 2, duration: 86400, - percent: 0.2 ether, valueRatioOracle: NodeModule(0x83A0444B93927c3AFCbe46E522280390F748E171).registerNode( NodeDefinition.NodeType.CHAINLINK, abi.encode(address(mockAggregator), uint256(0), uint8(18)), parents ), penaltyStart: 0.0 ether, - penaltyEnd: 0.0 ether + penaltyEnd: 0.0 ether, + amounts: amnts }); vm.startPrank(v3System.owner()); @@ -229,7 +236,28 @@ contract TreasuryMarketTest is Test, IERC721Receiver { market.saddle(accountId + 1); } - function test__RevertIf_SaddleAccountWithNoDelegatedCollateral() public { + function test_RevertIf_SaddleRewardsExhausted() external { + // delegate another account to cause the avg c-ratio to shoot way up5000000000000000000 + v3System.delegateCollateral( + accountId, + poolId, + address(collateralToken), + 101 ether, + 1 ether + ); + + vm.expectRevert( + abi.encodeWithSelector( + ITreasuryMarket.InsufficientAvailableReward.selector, + address(collateralToken), + 101 ether, + 0 + ) + ); + market.saddle(accountId); + } + + function test_RevertIf_SaddleAccountWithNoDelegatedCollateral() public { vm.expectRevert( abi.encodeWithSelector( ParameterError.InvalidParameter.selector, @@ -314,6 +342,17 @@ contract TreasuryMarketTest is Test, IERC721Receiver { ); } + function test_RevertIf_DepositRewardPenaltyNotSet() external { + vm.expectRevert( + abi.encodeWithSelector( + ParameterError.InvalidParameter.selector, + "depositRewardToken", + "config not found" + ) + ); + market.depositRewardPenalty(accountId, address(132612361236512)); + } + function test_SaddleSecondAccount() external { // saddle the first account sideMarket.setReportedDebt(1 ether); @@ -532,6 +571,15 @@ contract TreasuryMarketTest is Test, IERC721Receiver { assertEq(market.repaymentPenalty(accountId, 0), 0); } + function test_RepaymentPenaltyNoTimeElapse() external { + vm.prank(market.owner()); + market.setDebtDecayFunction(1, 0, 1 ether, 0.5 ether); + sideMarket.setReportedDebt(1 ether); + market.saddle(accountId); + + assertEq(market.repaymentPenalty(accountId, 0), 0); + } + function test_RepayLoanMidScheduleLinearWithPenalty() external { vm.warp(100000000); vm.prank(market.owner()); @@ -662,6 +710,14 @@ contract TreasuryMarketTest is Test, IERC721Receiver { market.mintTreasury(1 ether); } + function test_RevertIf_TreasuryMintExcess() external { + vm.prank(market.treasury()); + vm.expectRevert( + abi.encodeWithSelector(ITreasuryMarket.InsufficientExcessDebt.selector, 1000 ether, 0) + ); + market.mintTreasury(1000 ether); + } + function test_TreasuryMint() external { market.saddle(accountId); @@ -721,11 +777,23 @@ contract TreasuryMarketTest is Test, IERC721Receiver { assertEq(market.availableDepositRewards(address(collateralToken)), 0); } - function test__RevertIf_RemoveDepositRewardUnauthorized() external { + function test_RevertIf_RemoveDepositRewardUnauthorized() external { vm.expectRevert(abi.encodeWithSelector(AccessError.Unauthorized.selector, address(this))); market.removeFromDepositReward(address(collateralToken), 500 ether); } + function test_RevertIf_RemoveDepositRewardExcess() external { + vm.prank(market.owner()); + vm.expectRevert( + abi.encodeWithSelector( + ParameterError.InvalidParameter.selector, + "amount", + "greater than available rewards" + ) + ); + market.removeFromDepositReward(address(collateralToken), 1000.1 ether); + } + function test_RevertIf_UnsaddleUnauthorized() external { market.saddle(accountId); @@ -1016,7 +1084,7 @@ contract TreasuryMarketTest is Test, IERC721Receiver { market.unsaddle(1337); } - function test__RevertIf_UnsaddleTooBigForReward() public { + function test_RevertIf_UnsaddleTooBigForReward() public { market.saddle(accountId); sideMarket.setReportedDebt(1 ether); @@ -1137,6 +1205,13 @@ contract TreasuryMarketTest is Test, IERC721Receiver { market.setDepositRewardConfigurations(dcr); vm.stopPrank(); + ITreasuryMarket.DepositRewardAmounts[] + memory amnts = new ITreasuryMarket.DepositRewardAmounts[](2); + amnts[0].maxCollateral = 1 ether; + amnts[0].percent = 0.2 ether; + amnts[1].maxCollateral = 100000000 ether; + amnts[1].percent = 0.2 ether; + ITreasuryMarket.DepositRewardConfiguration[] memory configs = new ITreasuryMarket.DepositRewardConfiguration[](2); @@ -1147,28 +1222,28 @@ contract TreasuryMarketTest is Test, IERC721Receiver { token: address(usdToken), power: 1, duration: 86400, - percent: 0.2 ether, valueRatioOracle: NodeModule(0x83A0444B93927c3AFCbe46E522280390F748E171).registerNode( NodeDefinition.NodeType.CHAINLINK, abi.encode(address(mockAggregator), uint256(0), uint8(18)), parents ), penaltyStart: 1.0 ether, - penaltyEnd: 0.5 ether + penaltyEnd: 0.5 ether, + amounts: amnts }); configs[1] = ITreasuryMarket.DepositRewardConfiguration({ token: address(collateralToken), power: 1, duration: 86400, - percent: 0.2 ether, valueRatioOracle: NodeModule(0x83A0444B93927c3AFCbe46E522280390F748E171).registerNode( NodeDefinition.NodeType.CHAINLINK, abi.encode(address(mockAggregator), uint256(0), uint8(18)), parents ), penaltyStart: 1.0 ether, - penaltyEnd: 0.5 ether + penaltyEnd: 0.5 ether, + amounts: amnts }); vm.prank(market.owner()); @@ -1185,6 +1260,13 @@ contract TreasuryMarketTest is Test, IERC721Receiver { function test_SetDepositConfigurationOneRewardProperlyRemoved() public { bytes32[] memory parents = new bytes32[](0); + ITreasuryMarket.DepositRewardAmounts[] + memory amnts = new ITreasuryMarket.DepositRewardAmounts[](2); + amnts[0].maxCollateral = 1 ether; + amnts[0].percent = 0.2 ether; + amnts[1].maxCollateral = 100000000 ether; + amnts[1].percent = 0.2 ether; + // remove the usd deposit reward configuration ITreasuryMarket.DepositRewardConfiguration[] memory configs = new ITreasuryMarket.DepositRewardConfiguration[](1); @@ -1192,14 +1274,14 @@ contract TreasuryMarketTest is Test, IERC721Receiver { token: address(usdToken), power: 1, duration: 86400, - percent: 0.2 ether, valueRatioOracle: NodeModule(0x83A0444B93927c3AFCbe46E522280390F748E171).registerNode( NodeDefinition.NodeType.CHAINLINK, abi.encode(address(mockAggregator), uint256(0), uint8(18)), parents ), penaltyStart: 1.0 ether, - penaltyEnd: 0.5 ether + penaltyEnd: 0.5 ether, + amounts: amnts }); vm.startPrank(market.owner()); market.removeFromDepositReward(address(collateralToken), 1000 ether); @@ -1210,6 +1292,13 @@ contract TreasuryMarketTest is Test, IERC721Receiver { function test_SetDepositConfigurationOneRewardRugRemoved() public { bytes32[] memory parents = new bytes32[](0); + ITreasuryMarket.DepositRewardAmounts[] + memory amnts = new ITreasuryMarket.DepositRewardAmounts[](2); + amnts[0].maxCollateral = 1 ether; + amnts[0].percent = 0.2 ether; + amnts[1].maxCollateral = 100000000 ether; + amnts[1].percent = 0.2 ether; + // remove the usd deposit reward configuration ITreasuryMarket.DepositRewardConfiguration[] memory configs = new ITreasuryMarket.DepositRewardConfiguration[](1); @@ -1217,14 +1306,14 @@ contract TreasuryMarketTest is Test, IERC721Receiver { token: address(usdToken), power: 1, duration: 86400, - percent: 0.2 ether, valueRatioOracle: NodeModule(0x83A0444B93927c3AFCbe46E522280390F748E171).registerNode( NodeDefinition.NodeType.CHAINLINK, abi.encode(address(mockAggregator), uint256(0), uint8(18)), parents ), penaltyStart: 1.0 ether, - penaltyEnd: 0.5 ether + penaltyEnd: 0.5 ether, + amounts: amnts }); vm.prank(market.owner()); vm.expectRevert( @@ -1252,6 +1341,13 @@ contract TreasuryMarketTest is Test, IERC721Receiver { bytes32[] memory parents = new bytes32[](0); + ITreasuryMarket.DepositRewardAmounts[] + memory amnts = new ITreasuryMarket.DepositRewardAmounts[](2); + amnts[0].maxCollateral = 1 ether; + amnts[0].percent = 0.2 ether; + amnts[1].maxCollateral = 100000000 ether; + amnts[1].percent = 0.2 ether; + // remove the usd deposit reward configuration ITreasuryMarket.DepositRewardConfiguration[] memory configs = new ITreasuryMarket.DepositRewardConfiguration[](1); @@ -1259,14 +1355,14 @@ contract TreasuryMarketTest is Test, IERC721Receiver { token: address(collateralToken), power: 1, duration: 86400, - percent: 0.2 ether, valueRatioOracle: NodeModule(0x83A0444B93927c3AFCbe46E522280390F748E171).registerNode( NodeDefinition.NodeType.CHAINLINK, abi.encode(address(mockAggregator), uint256(0), uint8(18)), parents ), penaltyStart: 1.0 ether, - penaltyEnd: 0.5 ether + penaltyEnd: 0.5 ether, + amounts: amnts }); vm.prank(market.owner());