diff --git a/contracts/Reliquary.sol b/contracts/Reliquary.sol index 2a27b5f..fa0ea99 100644 --- a/contracts/Reliquary.sol +++ b/contracts/Reliquary.sol @@ -460,6 +460,11 @@ contract Reliquary is fromPosition.rewardDebt = newFromAmount * multiplier / ACC_REWARD_PRECISION; newPosition.rewardDebt = amount * multiplier / ACC_REWARD_PRECISION; + address _rewarder = rewarder[poolId]; + if (_rewarder != address(0)) { + IRewarder(_rewarder).onSplit(fromId, newId, amount, fromAmount, level); + } + emit ReliquaryEvents.CreateRelic(poolId, to, newId); emit ReliquaryEvents.Split(fromId, newId, amount); } @@ -528,6 +533,20 @@ contract Reliquary is toPosition.rewardDebt = vars.newToAmount * vars.accRewardPerShare * levels[vars.poolId].multipliers[vars.newToLevel] / ACC_REWARD_PRECISION; + address _rewarder = rewarder[vars.poolId]; + if (_rewarder != address(0)) { + IRewarder(_rewarder).onShift( + fromId, + toId, + amount, + vars.fromAmount, + vars.toAmount, + vars.fromLevel, + vars.oldToLevel, + vars.newToLevel + ); + } + emit ReliquaryEvents.Shift(fromId, toId, amount); } @@ -572,6 +591,19 @@ contract Reliquary is _burn(fromId); delete positionForId[fromId]; + address _rewarder = rewarder[poolId]; + if (_rewarder != address(0)) { + IRewarder(_rewarder).onMerge( + fromId, + toId, + fromAmount, + toAmount, + fromLevel, + oldToLevel, + newToLevel + ); + } + emit ReliquaryEvents.Merge(fromId, toId, fromAmount); } @@ -762,19 +794,38 @@ contract Reliquary is } address _rewarder = rewarder[poolId]; if (_rewarder != address(0)) { - IRewarder(_rewarder).onReward(relicId, received, harvestTo); + IRewarder(_rewarder).onReward( + relicId, + received, + harvestTo, + vars.oldAmount, + vars.oldLevel, + vars.newLevel + ); } } if (kind == Kind.DEPOSIT) { address _rewarder = rewarder[poolId]; if (_rewarder != address(0)) { - IRewarder(_rewarder).onDeposit(relicId, amount); + IRewarder(_rewarder).onDeposit( + relicId, + amount, + vars.oldAmount, + vars.oldLevel, + vars.newLevel + ); } } else if (kind == Kind.WITHDRAW) { address _rewarder = rewarder[poolId]; if (_rewarder != address(0)) { - IRewarder(_rewarder).onWithdraw(relicId, amount); + IRewarder(_rewarder).onWithdraw( + relicId, + amount, + vars.oldAmount, + vars.oldLevel, + vars.newLevel + ); } } } diff --git a/contracts/interfaces/IRewarder.sol b/contracts/interfaces/IRewarder.sol index e5d3fbe..e333d3c 100644 --- a/contracts/interfaces/IRewarder.sol +++ b/contracts/interfaces/IRewarder.sol @@ -2,12 +2,65 @@ pragma solidity ^0.8.15; +import "../Reliquary.sol"; + interface IRewarder { - function onReward(uint relicId, uint rewardAmount, address to) external; + function onReward( + uint relicId, + uint rewardAmount, + address to, + uint amount, + uint oldLevel, + uint newLevel + ) external; + + function onDeposit( + uint relicId, + uint depositAmount, + uint oldAmount, + uint oldLevel, + uint newLevel + ) external; + + function onWithdraw( + uint relicId, + uint withdrawalAmount, + uint oldAmount, + uint oldLevel, + uint newLevel + ) external; + + function onSplit( + uint fromId, + uint newId, + uint amount, + uint fromAmount, + uint level + ) external; - function onDeposit(uint relicId, uint depositAmount) external; + function onShift( + uint fromId, + uint toId, + uint amount, + uint oldFromAmount, + uint oldToAmount, + uint fromLevel, + uint oldToLevel, + uint newToLevel + ) external; - function onWithdraw(uint relicId, uint withdrawalAmount) external; + function onMerge( + uint fromId, + uint toId, + uint fromAmount, + uint toAmount, + uint fromLevel, + uint oldToLevel, + uint newToLevel + ) external; - function pendingTokens(uint relicId, uint rewardAmount) external view returns (address[] memory, uint[] memory); + function pendingTokens( + uint relicId, + uint rewardAmount + ) external view returns (address[] memory, uint[] memory); } diff --git a/contracts/interfaces/IRollingRewarder.sol b/contracts/interfaces/IRollingRewarder.sol new file mode 100644 index 0000000..fe97318 --- /dev/null +++ b/contracts/interfaces/IRollingRewarder.sol @@ -0,0 +1,12 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.15; + +import "./IRewarder.sol"; + +interface IRollingRewarder is IRewarder { + + function fund() external; + + function setRewardsPool(address _rewardsPool) external; +} diff --git a/contracts/rewarders/ChildMultiplierRewarder.sol b/contracts/rewarders/ChildMultiplierRewarder.sol new file mode 100644 index 0000000..f4e60c6 --- /dev/null +++ b/contracts/rewarders/ChildMultiplierRewarder.sol @@ -0,0 +1,28 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.17; + +import "./ChildRewarder.sol"; +import "./MultiplierRewarder.sol"; +import "openzeppelin-contracts/contracts/access/Ownable.sol"; + +contract ChildMultiplierRewarder is MultiplierRewarder, ChildRewarder, Ownable { + + constructor( + uint _rewardMultiplier, + address _rewardToken, + address _reliquary + ) MultiplierRewarder(_rewardMultiplier, _rewardToken, _reliquary) ChildRewarder() {} + + function onReward( + uint relicId, + uint rewardAmount, + address to, + uint, // oldAmount, + uint, // oldLevel, + uint // newLevel + ) external override(IRewarder, MultiplierRewarder) onlyParent { + _onReward(relicId, rewardAmount, to); + } + +} diff --git a/contracts/rewarders/ChildRewarder.sol b/contracts/rewarders/ChildRewarder.sol index fa28028..4c10a4b 100644 --- a/contracts/rewarders/ChildRewarder.sol +++ b/contracts/rewarders/ChildRewarder.sol @@ -2,32 +2,25 @@ pragma solidity ^0.8.17; -import "./MultiplierRewarderOwnable.sol"; +import "../interfaces/IRewarder.sol"; /// @title Child rewarder contract to be deployed and called by a ParentRewarder, rather than directly by the Reliquary. -contract ChildRewarder is MultiplierRewarderOwnable { +abstract contract ChildRewarder is IRewarder { /// @notice Address of ParentRewarder which deployed this contract address public immutable parent; modifier onlyParent() { - require(msg.sender == address(parent), "Only parent can call this function."); + require( + msg.sender == address(parent), + "Only parent can call this function." + ); _; } /** * @dev Contructor called on deployment of this contract. - * @param _rewardMultiplier Amount to multiply reward by, relative to BASIS_POINTS. - * @param _rewardToken Address of token rewards are distributed in. - * @param _reliquary Address of Reliquary this rewarder will read state from. */ - constructor(uint _rewardMultiplier, address _rewardToken, address _reliquary) - MultiplierRewarderOwnable(_rewardMultiplier, _rewardToken, _reliquary) - { + constructor() { parent = msg.sender; } - - /// @inheritdoc SingleAssetRewarder - function onReward(uint relicId, uint rewardAmount, address to) external override onlyParent { - super._onReward(relicId, rewardAmount, to); - } } diff --git a/contracts/rewarders/DepositBonusRewarder.sol b/contracts/rewarders/DepositBonusRewarder.sol index e5a7e79..64043d2 100644 --- a/contracts/rewarders/DepositBonusRewarder.sol +++ b/contracts/rewarders/DepositBonusRewarder.sol @@ -37,7 +37,13 @@ contract DepositBonusRewarder is SingleAssetRewarder { } /// @inheritdoc SingleAssetRewarder - function onDeposit(uint relicId, uint depositAmount) external override onlyReliquary { + function onDeposit( + uint relicId, + uint depositAmount, + uint, // oldAmount, + uint, // oldLevel, + uint // newLevel + ) external override onlyReliquary { if (depositAmount >= minimum) { uint _lastDepositTime = lastDepositTime[relicId]; uint timestamp = block.timestamp; @@ -49,7 +55,10 @@ contract DepositBonusRewarder is SingleAssetRewarder { /// @inheritdoc SingleAssetRewarder function onWithdraw( uint relicId, - uint //withdrawalAmount + uint, // withdrawalAmount, + uint, // oldAmount, + uint, // oldLevel, + uint // newLevel ) external override onlyReliquary { uint _lastDepositTime = lastDepositTime[relicId]; delete lastDepositTime[relicId]; @@ -94,4 +103,34 @@ contract DepositBonusRewarder is SingleAssetRewarder { claimed = false; } } + + // Required overrides + function onSplit( + uint fromId, + uint newId, + uint amount, + uint fromAmount, + uint level + ) external override {} + + function onShift( + uint fromId, + uint toId, + uint amount, + uint oldFromAmount, + uint oldToAmount, + uint fromLevel, + uint oldToLevel, + uint newToLevel + ) external override {} + + function onMerge( + uint fromId, + uint toId, + uint fromAmount, + uint toAmount, + uint fromLevel, + uint oldToLevel, + uint newToLevel + ) external override {} } diff --git a/contracts/rewarders/MultiplierRewarder.sol b/contracts/rewarders/MultiplierRewarder.sol index e8ae7eb..278fa0c 100644 --- a/contracts/rewarders/MultiplierRewarder.sol +++ b/contracts/rewarders/MultiplierRewarder.sol @@ -22,9 +22,11 @@ contract MultiplierRewarder is SingleAssetRewarder { * @param _rewardToken Address of token rewards are distributed in. * @param _reliquary Address of Reliquary this rewarder will read state from. */ - constructor(uint _rewardMultiplier, address _rewardToken, address _reliquary) - SingleAssetRewarder(_rewardToken, _reliquary) - { + constructor( + uint _rewardMultiplier, + address _rewardToken, + address _reliquary + ) SingleAssetRewarder(_rewardToken, _reliquary) { rewardMultiplier = _rewardMultiplier; } @@ -33,14 +35,24 @@ contract MultiplierRewarder is SingleAssetRewarder { * @param rewardAmount Amount of reward token owed for this position from the Reliquary. * @param to Address to send rewards to. */ - function onReward(uint relicId, uint rewardAmount, address to) external virtual override onlyReliquary { + function onReward( + uint relicId, + uint rewardAmount, + address to, + uint, // oldAmount, + uint, // oldLevel, + uint // newLevel + ) external virtual override onlyReliquary { _onReward(relicId, rewardAmount, to); } /// @dev Separate internal function that may be called by inheriting contracts. function _onReward(uint relicId, uint rewardAmount, address to) internal { if (rewardMultiplier != 0 && rewardAmount != 0) { - IERC20(rewardToken).safeTransfer(to, pendingToken(relicId, rewardAmount)); + IERC20(rewardToken).safeTransfer( + to, + pendingToken(relicId, rewardAmount) + ); } emit LogOnReward(relicId, rewardAmount, to); } @@ -51,6 +63,51 @@ contract MultiplierRewarder is SingleAssetRewarder { uint, //relicId uint rewardAmount ) public view override returns (uint pending) { - pending = rewardAmount * rewardMultiplier / BASIS_POINTS; + pending = (rewardAmount * rewardMultiplier) / BASIS_POINTS; } + + function onDeposit( + uint relicId, + uint depositAmount, + uint oldAmount, + uint oldLevel, + uint newLevel + ) external virtual override {} + + function onWithdraw( + uint relicId, + uint withdrawalAmount, + uint oldAmount, + uint oldLevel, + uint newLevel + ) external virtual override {} + + function onSplit( + uint fromId, + uint newId, + uint amount, + uint fromAmount, + uint level + ) external virtual override {} + + function onShift( + uint fromId, + uint toId, + uint amount, + uint oldFromAmount, + uint oldToAmount, + uint fromLevel, + uint oldToLevel, + uint newToLevel + ) external virtual override {} + + function onMerge( + uint fromId, + uint toId, + uint fromAmount, + uint toAmount, + uint fromLevel, + uint oldToLevel, + uint newToLevel + ) external virtual override {} } diff --git a/contracts/rewarders/ParentRewarder-Rolling.sol b/contracts/rewarders/ParentRewarder-Rolling.sol new file mode 100644 index 0000000..3d7c68b --- /dev/null +++ b/contracts/rewarders/ParentRewarder-Rolling.sol @@ -0,0 +1,278 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.17; + +import "./RollingRewarder.sol"; +import "./MultiplierRewarder.sol"; +import "openzeppelin-contracts/contracts/access/Ownable.sol"; +import "openzeppelin-contracts/contracts/utils/structs/EnumerableSet.sol"; +import "openzeppelin-contracts/contracts/access/AccessControlEnumerable.sol"; + +/// @title Extension to the SingleAssetRewarder contract that allows managing multiple reward tokens via access control +/// and enumerable children contracts. +contract ParentRewarderRolling is MultiplierRewarder, AccessControlEnumerable { + using EnumerableSet for EnumerableSet.AddressSet; + + EnumerableSet.AddressSet private childrenRewarders; + + uint public immutable poolId; + + /// @dev Access control roles. + bytes32 public constant REWARD_SETTER = keccak256("REWARD_SETTER"); + bytes32 public constant CHILD_SETTER = keccak256("CHILD_SETTER"); + + event LogRewardMultiplier(uint rewardMultiplier); + event ChildCreated(address indexed child, address indexed token); + event ChildRemoved(address indexed child); + + /** + * @dev Contructor called on deployment of this contract. + * @param _rewardMultiplier Amount to multiply reward by, relative to BASIS_POINTS. + * @param _rewardToken Address of token rewards are distributed in. + * @param _reliquary Address of Reliquary this rewarder will read state from. + */ + constructor( + uint _rewardMultiplier, + address _rewardToken, + address _reliquary, + uint _poolId + ) MultiplierRewarder(_rewardMultiplier, _rewardToken, _reliquary) { + poolId = _poolId; + _grantRole(DEFAULT_ADMIN_ROLE, msg.sender); + } + + /** + * @notice Set the rewardMultiplier to a new value and emit a logging event. + * Separate role from who can add/remove children. + * @param _rewardMultiplier Amount to multiply reward by, relative to BASIS_POINTS. + */ + function setRewardMultiplier( + uint _rewardMultiplier + ) external onlyRole(REWARD_SETTER) { + rewardMultiplier = _rewardMultiplier; + emit LogRewardMultiplier(_rewardMultiplier); + } + + /** + * @notice Deploys a ChildRewarder contract and adds it to the childrenRewarders set. + * @param _rewardToken Address of token rewards are distributed in. + * @param owner Address to transfer ownership of the ChildRewarder contract to. + * @return child Address of the new ChildRewarder. + */ + function createChild( + address _rewardToken, + address owner + ) external onlyRole(CHILD_SETTER) returns (address child) { + child = address(new RollingRewarder(_rewardToken, reliquary, poolId)); + Ownable(child).transferOwnership(owner); + childrenRewarders.add(child); + emit ChildCreated(child, address(_rewardToken)); + } + + /// @notice Removes a ChildRewarder from the childrenRewarders set. + /// @param childRewarder Address of the ChildRewarder contract to remove. + function removeChild( + address childRewarder + ) external onlyRole(CHILD_SETTER) { + require( + childrenRewarders.remove(childRewarder), + "That is not my child rewarder!" + ); + emit ChildRemoved(childRewarder); + } + + /// Call onReward function of each child. + /// @inheritdoc SingleAssetRewarder + function onReward( + uint relicId, + uint rewardAmount, + address to, + uint oldAmount, + uint oldLevel, + uint newLevel + ) external override onlyReliquary { + super._onReward(relicId, rewardAmount, to); + + uint length = childrenRewarders.length(); + for (uint i; i < length; ) { + IRewarder(childrenRewarders.at(i)).onReward( + relicId, + rewardAmount, + to, + oldAmount, + oldLevel, + newLevel + ); + unchecked { + ++i; + } + } + } + + function onDeposit( + uint relicId, + uint depositAmount, + uint oldAmount, + uint oldLevel, + uint newLevel + ) external override onlyReliquary { + uint length = childrenRewarders.length(); + for (uint i; i < length; ) { + IRewarder(childrenRewarders.at(i)).onDeposit( + relicId, + depositAmount, + oldAmount, + oldLevel, + newLevel + ); + unchecked { + ++i; + } + } + } + + function onWithdraw( + uint relicId, + uint withdrawAmount, + uint oldAmount, + uint oldLevel, + uint newLevel + ) external override onlyReliquary { + uint length = childrenRewarders.length(); + for (uint i; i < length; ) { + IRewarder(childrenRewarders.at(i)).onWithdraw( + relicId, + withdrawAmount, + oldAmount, + oldLevel, + newLevel + ); + unchecked { + ++i; + } + } + } + + function onSplit( + uint fromId, + uint newId, + uint amount, + uint fromAmount, + uint level + ) external override onlyReliquary { + uint length = childrenRewarders.length(); + for (uint i; i < length; ) { + IRewarder(childrenRewarders.at(i)).onSplit( + fromId, + newId, + amount, + fromAmount, + level + ); + unchecked { + ++i; + } + } + } + + function onShift( + uint fromId, + uint toId, + uint amount, + uint oldFromAmount, + uint oldToAmount, + uint fromLevel, + uint oldToLevel, + uint newToLevel + ) external override onlyReliquary { + uint length = childrenRewarders.length(); + for (uint i; i < length; ) { + IRewarder(childrenRewarders.at(i)).onShift( + fromId, + toId, + amount, + oldFromAmount, + oldToAmount, + fromLevel, + oldToLevel, + newToLevel + ); + unchecked { + ++i; + } + } + } + + function onMerge( + uint fromId, + uint toId, + uint fromAmount, + uint toAmount, + uint fromLevel, + uint oldToLevel, + uint newToLevel + ) external override onlyReliquary { + uint length = childrenRewarders.length(); + for (uint i; i < length; ) { + IRewarder(childrenRewarders.at(i)).onMerge( + fromId, + toId, + fromAmount, + toAmount, + fromLevel, + oldToLevel, + newToLevel + ); + unchecked { + ++i; + } + } + } + + /** + * @dev WARNING: This operation will copy the entire childrenRewarders storage to memory, which can be quite + * expensive. This is designed to mostly be used by view accessors that are queried without any gas fees. + * Developers should keep in mind that this function has an unbounded cost, and using it as part of a state- + * changing function may render the function uncallable if the set grows to a point where copying to memory + * consumes too much gas to fit in a block. + */ + function getChildrenRewarders() external view returns (address[] memory) { + return childrenRewarders.values(); + } + + /// @inheritdoc SingleAssetRewarder + function pendingTokens( + uint relicId, + uint rewardAmount + ) + external + view + override + returns (address[] memory rewardTokens, uint[] memory rewardAmounts) + { + uint length = childrenRewarders.length() + 1; + rewardTokens = new address[](length); + rewardTokens[0] = rewardToken; + + rewardAmounts = new uint[](length); + rewardAmounts[0] = pendingToken(relicId, rewardAmount); + + for (uint i = 1; i < length; ) { + RollingRewarder rewarder = RollingRewarder( + childrenRewarders.at(i - 1) + ); + rewardTokens[i] = rewarder.rewardToken(); + rewardAmounts[i] = rewarder.pendingToken(relicId, rewardAmount); + unchecked { + ++i; + } + } + } + + function setChildsRewardPool( + address child, + address pool + ) external onlyRole(CHILD_SETTER) { + RollingRewarder(child).setRewardsPool(pool); + } +} diff --git a/contracts/rewarders/ParentRewarder.sol b/contracts/rewarders/ParentRewarder.sol index 2a2bab0..30cdfa7 100644 --- a/contracts/rewarders/ParentRewarder.sol +++ b/contracts/rewarders/ParentRewarder.sol @@ -2,7 +2,8 @@ pragma solidity ^0.8.17; -import "./ChildRewarder.sol"; +import "./ChildMultiplierRewarder.sol"; +import "./MultiplierRewarder.sol"; import "openzeppelin-contracts/contracts/utils/structs/EnumerableSet.sol"; import "openzeppelin-contracts/contracts/access/AccessControlEnumerable.sol"; @@ -44,25 +45,25 @@ contract ParentRewarder is MultiplierRewarder, AccessControlEnumerable { } /** - * @notice Deploys a ChildRewarder contract and adds it to the childrenRewarders set. + * @notice Deploys a ChildMultiplierRewarder contract and adds it to the childrenRewarders set. * @param _rewardToken Address of token rewards are distributed in. * @param _rewardMultiplier Amount to multiply reward by, relative to BASIS_POINTS. - * @param owner Address to transfer ownership of the ChildRewarder contract to. - * @return child Address of the new ChildRewarder. + * @param owner Address to transfer ownership of the ChildMultiplierRewarder contract to. + * @return child Address of the new ChildMultiplierRewarder. */ function createChild(address _rewardToken, uint _rewardMultiplier, address owner) external onlyRole(CHILD_SETTER) returns (address child) { - child = address(new ChildRewarder(_rewardMultiplier, _rewardToken, reliquary)); + child = address(new ChildMultiplierRewarder(_rewardMultiplier, _rewardToken, reliquary)); Ownable(child).transferOwnership(owner); childrenRewarders.add(child); emit ChildCreated(child, address(_rewardToken)); } - /// @notice Removes a ChildRewarder from the childrenRewarders set. - /// @param childRewarder Address of the ChildRewarder contract to remove. + /// @notice Removes a ChildMultiplierRewarder from the childrenRewarders set. + /// @param childRewarder Address of the ChildMultiplierRewarder contract to remove. function removeChild(address childRewarder) external onlyRole(CHILD_SETTER) { require(childrenRewarders.remove(childRewarder), "That is not my child rewarder!"); emit ChildRemoved(childRewarder); @@ -70,12 +71,26 @@ contract ParentRewarder is MultiplierRewarder, AccessControlEnumerable { /// Call onReward function of each child. /// @inheritdoc SingleAssetRewarder - function onReward(uint relicId, uint rewardAmount, address to) external override onlyReliquary { + function onReward( + uint relicId, + uint rewardAmount, + address to, + uint oldAmount, + uint oldLevel, + uint newLevel + ) external override onlyReliquary { super._onReward(relicId, rewardAmount, to); uint length = childrenRewarders.length(); for (uint i; i < length;) { - IRewarder(childrenRewarders.at(i)).onReward(relicId, rewardAmount, to); + IRewarder(childrenRewarders.at(i)).onReward( + relicId, + rewardAmount, + to, + oldAmount, + oldLevel, + newLevel + ); unchecked { ++i; } @@ -108,7 +123,7 @@ contract ParentRewarder is MultiplierRewarder, AccessControlEnumerable { rewardAmounts[0] = pendingToken(relicId, rewardAmount); for (uint i = 1; i < length;) { - ChildRewarder rewarder = ChildRewarder(childrenRewarders.at(i - 1)); + ChildMultiplierRewarder rewarder = ChildMultiplierRewarder(childrenRewarders.at(i - 1)); rewardTokens[i] = rewarder.rewardToken(); rewardAmounts[i] = rewarder.pendingToken(relicId, rewardAmount); unchecked { diff --git a/contracts/rewarders/RewardsPool.sol b/contracts/rewarders/RewardsPool.sol new file mode 100644 index 0000000..c01aba1 --- /dev/null +++ b/contracts/rewarders/RewardsPool.sol @@ -0,0 +1,25 @@ +pragma solidity ^0.8.15; + +import "openzeppelin-contracts/contracts/token/ERC20/IERC20.sol"; +import "../interfaces/IRollingRewarder.sol"; + + +contract RewardsPool { + address public immutable RewardToken; + address public Rewarder; + uint256 public lastRetrievedTime; + uint256 public totalRewards; + + constructor(address _rewardToken, address rewarder) { + RewardToken = _rewardToken; + Rewarder = rewarder; + IERC20(RewardToken).approve(Rewarder, type(uint256).max); + } + + function fundRewarder() external { + uint256 balance = IERC20(RewardToken).balanceOf(address(this)); + totalRewards += balance; + IRollingRewarder(Rewarder).fund(); + } + +} \ No newline at end of file diff --git a/contracts/rewarders/RollingRewarder.sol b/contracts/rewarders/RollingRewarder.sol new file mode 100644 index 0000000..53c5963 --- /dev/null +++ b/contracts/rewarders/RollingRewarder.sol @@ -0,0 +1,346 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.15; + +import "../interfaces/IRollingRewarder.sol"; +import "./ChildRewarder.sol"; +import "./SingleAssetRewarder.sol"; +import {IReliquary, LevelInfo} from "../interfaces/IReliquary.sol"; +import {IEmissionCurve} from "../interfaces/IEmissionCurve.sol"; +import "openzeppelin-contracts/contracts/token/ERC20/utils/SafeERC20.sol"; +import "openzeppelin-contracts/contracts/token/ERC20/extensions/IERC20Metadata.sol"; +import "openzeppelin-contracts/contracts/access/Ownable.sol"; + +/// @title Simple rewarder that distributes its own token based on a ratio to rewards emitted by the Reliquary +contract RollingRewarder is IRollingRewarder, SingleAssetRewarder, ChildRewarder, Ownable { + using SafeERC20 for IERC20; + + uint256 public immutable ACC_REWARD_PRECISION = 1e18; + uint256 public immutable REWARD_PER_SECOND_PRECISION = 1e2; + + uint256 public immutable poolId; + address public rewardsPool; + + uint256 public lastDistributionTime; + uint256 public distributionPeriod; + uint256 public lastIssuanceTimestamp; + uint256 public totalIssued; + + uint256 public _rewardPerSecond; + uint256 public accRewardPerShare; + uint[] private multipliers; + + mapping(uint256 => uint256) public rewardDebt; + mapping(uint256 => uint256) public rewardCredit; + + event LogOnReward(uint relicId, uint rewardAmount, address to); + event UpdateDistributionPeriod(uint256 newDistributionPeriod); + + /** + * @dev Contructor called on deployment of this contract. + * @param _rewardToken Address of token rewards are distributed in. + * @param _reliquary Address of Reliquary this rewarder will read state from. + */ + constructor( + address _rewardToken, + address _reliquary, + uint256 _poolId + ) SingleAssetRewarder(_rewardToken, _reliquary) { + poolId = _poolId; + + uint256[] memory _multipliers = IReliquary(_reliquary) + .getLevelInfo(_poolId) + .multipliers; + for (uint i; i < _multipliers.length; ) { + multipliers.push(_multipliers[i]); + unchecked { + ++i; + } + } + + _updateDistributionPeriod(7 days); + } + + /** + * @notice Called by Reliquary harvest or withdrawAndHarvest function. + * @param to Address to send rewards to. + */ + function onReward( + uint relicId, + uint rewardAmount, + address to, + uint amount, + uint oldLevel, + uint newLevel + ) external virtual override(IRewarder, SingleAssetRewarder) onlyParent { + uint256 oldAmountMultiplied = amount * multipliers[oldLevel]; + uint256 newAmountMultiplied = amount * multipliers[newLevel]; + + _issueTokens( + _poolBalance() - newAmountMultiplied + oldAmountMultiplied + ); + + uint256 pending = ((oldAmountMultiplied * accRewardPerShare) / + ACC_REWARD_PRECISION) - rewardDebt[relicId]; + pending += rewardCredit[relicId]; + rewardDebt[relicId] = ((newAmountMultiplied * accRewardPerShare) / + ACC_REWARD_PRECISION); + if (pending > 0) { + IERC20(rewardToken).safeTransfer(to, pending); + emit LogOnReward(relicId, pending, to); + } + } + + function onDeposit( + uint relicId, + uint depositAmount, + uint oldAmount, + uint oldLevel, + uint newLevel + ) external virtual override(IRewarder, SingleAssetRewarder) onlyParent { + uint256 oldAmountMultiplied = oldAmount * multipliers[oldLevel]; + uint256 newAmountMultiplied = (oldAmount + depositAmount) * + multipliers[newLevel]; + + _issueTokens( + _poolBalance() - newAmountMultiplied + oldAmountMultiplied + ); + + rewardCredit[relicId] += + ((oldAmountMultiplied * accRewardPerShare) / ACC_REWARD_PRECISION) - + rewardDebt[relicId]; + rewardDebt[relicId] = ((newAmountMultiplied * accRewardPerShare) / + ACC_REWARD_PRECISION); + } + + function onWithdraw( + uint relicId, + uint withdrawalAmount, + uint oldAmount, + uint oldLevel, + uint newLevel + ) external virtual override(IRewarder, SingleAssetRewarder) onlyParent { + uint256 oldAmountMultiplied = oldAmount * multipliers[oldLevel]; + uint256 newAmountMultiplied = (oldAmount - withdrawalAmount) * + multipliers[newLevel]; + + _issueTokens( + _poolBalance() - newAmountMultiplied + oldAmountMultiplied + ); + + rewardCredit[relicId] += + (oldAmountMultiplied * accRewardPerShare) / + ACC_REWARD_PRECISION - + rewardDebt[relicId]; + rewardDebt[relicId] = ((newAmountMultiplied * accRewardPerShare) / + ACC_REWARD_PRECISION); + } + + function onSplit( + uint fromId, + uint newId, + uint amount, + uint fromAmount, + uint level + ) external virtual onlyParent { + _issueTokens(_poolBalance()); + uint256 _multiplier = multipliers[level]; + rewardCredit[fromId] += + ((fromAmount * _multiplier * accRewardPerShare) / + ACC_REWARD_PRECISION) - + rewardDebt[fromId]; + rewardDebt[fromId] = (((fromAmount - amount) * + _multiplier * + accRewardPerShare) / ACC_REWARD_PRECISION); + rewardDebt[newId] = ((amount * _multiplier * accRewardPerShare) / + ACC_REWARD_PRECISION); + } + + function onShift( + uint fromId, + uint toId, + uint amount, + uint oldFromAmount, + uint oldToAmount, + uint fromLevel, + uint oldToLevel, + uint newToLevel + ) external virtual onlyParent { + uint256 _multiplierFrom = multipliers[fromLevel]; + uint256 oldFromAmountMultiplied = oldFromAmount * _multiplierFrom; + uint256 newFromAmountMultiplied = (oldFromAmount - amount) * + _multiplierFrom; + uint256 oldToAmountMultiplied = oldToAmount * multipliers[oldToLevel]; + uint256 newToAmountMultiplied = (oldToAmount + amount) * + multipliers[newToLevel]; + + _issueTokens( + _poolBalance() - + newFromAmountMultiplied + + oldFromAmountMultiplied - + newToAmountMultiplied + + oldToAmountMultiplied + ); + + rewardCredit[fromId] += + ((oldFromAmount * _multiplierFrom * accRewardPerShare) / + ACC_REWARD_PRECISION) - + rewardDebt[fromId]; + rewardDebt[fromId] = (((oldFromAmount - amount) * + _multiplierFrom * + accRewardPerShare) / ACC_REWARD_PRECISION); + rewardCredit[toId] += + ((oldToAmount * multipliers[oldToLevel] * accRewardPerShare) / + ACC_REWARD_PRECISION) - + rewardDebt[toId]; + rewardDebt[toId] = (((oldToAmount + amount) * + multipliers[newToLevel] * + accRewardPerShare) / ACC_REWARD_PRECISION); + } + + function onMerge( + uint fromId, + uint toId, + uint fromAmount, + uint toAmount, + uint fromLevel, + uint oldToLevel, + uint newToLevel + ) external virtual onlyParent { + uint fromAmountMultiplied = fromAmount * multipliers[fromLevel]; + uint oldToAmountMultiplied = toAmount * multipliers[oldToLevel]; + uint newToAmountMultiplied = (toAmount + fromAmount) * + multipliers[newToLevel]; + + _issueTokens( + _poolBalance() - + newToAmountMultiplied + + oldToAmountMultiplied + + fromAmountMultiplied + ); + + uint pendingTo = (accRewardPerShare * + (fromAmountMultiplied + oldToAmountMultiplied)) / + ACC_REWARD_PRECISION + + rewardCredit[fromId] - + rewardDebt[fromId] - + rewardDebt[toId]; + if (pendingTo != 0) { + rewardCredit[toId] += pendingTo; + } + + rewardCredit[fromId] = 0; + + rewardDebt[toId] = + (newToAmountMultiplied * accRewardPerShare) / + ACC_REWARD_PRECISION; + } + + /// @notice Returns the amount of pending rewardToken for a position from this rewarder. + /// @param rewardAmount Amount of reward token owed for this position from the Reliquary. + function pendingTokens( + uint, //relicId + uint rewardAmount + ) + external + view + override(IRewarder, SingleAssetRewarder) + returns (address[] memory, uint[] memory) + {} + + function _updateDistributionPeriod( + uint256 _newDistributionPeriod + ) internal { + distributionPeriod = _newDistributionPeriod; + emit UpdateDistributionPeriod(_newDistributionPeriod); + } + + function updateDistributionPeriod( + uint256 _newDistributionPeriod + ) external onlyOwner { + _updateDistributionPeriod(_newDistributionPeriod); + } + + function rewardPerSecond() public view returns (uint256) { + return _rewardPerSecond; + } + + function getRewardAmount(uint seconds_) public view returns (uint256) { + return ((_rewardPerSecond * seconds_) / REWARD_PER_SECOND_PRECISION); + } + + function _fund(uint256 _amount) internal { + require(_amount != 0, "cannot fund 0"); + + uint256 _lastIssuanceTimestamp = lastIssuanceTimestamp; //last time token was distributed + uint256 _lastDistributionTime = lastDistributionTime; //timestamp of the final distribution of tokens + uint256 amount = _amount; //amount of tokens to add to the distribution + if (_lastIssuanceTimestamp < _lastDistributionTime) { + uint256 timeLeft = _lastDistributionTime - _lastIssuanceTimestamp; //time left until final distribution + uint256 notIssued = getRewardAmount(timeLeft); //how many tokens are left to issue + amount = amount + (notIssued); // add to the funding amount that hasnt been issued + } + + uint256 _distributionPeriod = distributionPeriod; //how many days will we distribute these assets over + _rewardPerSecond = + (amount * REWARD_PER_SECOND_PRECISION) / + _distributionPeriod; //how many tokens per second will be distributed + lastDistributionTime = block.timestamp + _distributionPeriod; //when will the new final distribution be + lastIssuanceTimestamp = block.timestamp; //when was the last time tokens were distributed -- now + + IERC20(rewardToken).safeTransferFrom( + rewardsPool, + address(this), + _amount + ); //transfer the tokens to the contract + } + + /// @notice Issues tokens. + /// @param poolBalance Amount of tokens in the pool. This must be passed because the pool balance may have changed. + /// @return issuance Amount of tokens issued. + function _issueTokens( + uint256 poolBalance + ) internal returns (uint256 issuance) { + uint256 _lastIssuanceTimestamp = lastIssuanceTimestamp; //last time token was distributed + uint256 _lastDistributionTime = lastDistributionTime; //timestamp of the final distribution of tokens + uint256 _totalIssued = totalIssued; //how many tokens to issue + if (_lastIssuanceTimestamp < _lastDistributionTime) { + uint256 endTimestamp = block.timestamp > _lastDistributionTime + ? _lastDistributionTime + : block.timestamp; + uint256 timePassed = endTimestamp - _lastIssuanceTimestamp; + issuance = getRewardAmount(timePassed); + if (poolBalance != 0) { + accRewardPerShare += + (issuance * ACC_REWARD_PRECISION) / + poolBalance; + + _totalIssued = _totalIssued + issuance; + totalIssued = _totalIssued; + } + } + + lastIssuanceTimestamp = block.timestamp; + } + + function _poolBalance() internal view returns (uint256 total) { + LevelInfo memory levelInfo = IReliquary(reliquary).getLevelInfo(poolId); + uint length = levelInfo.balance.length; + for (uint i; i < length; ) { + total += levelInfo.balance[i] * levelInfo.multipliers[i]; + unchecked { + ++i; + } + } + } + + function fund() external { + require(msg.sender == rewardsPool, "only rewards pool can fund"); + _fund(IERC20(rewardToken).balanceOf(rewardsPool)); + } + + function setRewardsPool(address _rewardsPool) external { + require(msg.sender == parent, "only parent can set rewards pool"); + rewardsPool = _rewardsPool; + } +} diff --git a/contracts/rewarders/SingleAssetRewarder.sol b/contracts/rewarders/SingleAssetRewarder.sol index e9466bb..ec7dee8 100644 --- a/contracts/rewarders/SingleAssetRewarder.sol +++ b/contracts/rewarders/SingleAssetRewarder.sol @@ -5,7 +5,7 @@ pragma solidity ^0.8.15; import "../interfaces/IRewarder.sol"; abstract contract SingleAssetRewarder is IRewarder { - address public immutable rewardToken; + address public rewardToken; address public immutable reliquary; /// @dev Limits function calls to address of Reliquary contract `reliquary` @@ -29,13 +29,32 @@ abstract contract SingleAssetRewarder is IRewarder { * @param rewardAmount Amount of reward token owed for this position from the Reliquary. * @param to Address to send rewards to. */ - function onReward(uint relicId, uint rewardAmount, address to) external virtual override {} - + function onReward( + uint relicId, + uint rewardAmount, + address to, + uint oldAmount, + uint oldLevel, + uint newLevel + ) external virtual override {} + /// @notice Called by Reliquary _deposit function. - function onDeposit(uint relicId, uint depositAmount) external virtual override {} + function onDeposit( + uint relicId, + uint depositAmount, + uint oldAmount, + uint oldLevel, + uint newLevel + ) external virtual override {} /// @notice Called by Reliquary withdraw or withdrawAndHarvest function. - function onWithdraw(uint relicId, uint withdrawalAmount) external virtual override {} + function onWithdraw( + uint relicId, + uint withdrawalAmount, + uint oldAmount, + uint oldLevel, + uint newLevel + ) external virtual override {} /** * @notice Returns the amount of pending tokens for a position from this rewarder. diff --git a/test/foundry/MultipleRollingRewarder.t.sol b/test/foundry/MultipleRollingRewarder.t.sol new file mode 100644 index 0000000..3f61bc0 --- /dev/null +++ b/test/foundry/MultipleRollingRewarder.t.sol @@ -0,0 +1,287 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.13; + +import "forge-std/Test.sol"; +import "openzeppelin-contracts/contracts/mocks/ERC20DecimalsMock.sol"; +import "openzeppelin-contracts/contracts/mocks/ERC4626Mock.sol"; +import "openzeppelin-contracts/contracts/token/ERC721/utils/ERC721Holder.sol"; +import {WETH} from "solmate/tokens/WETH.sol"; +import "contracts/emission_curves/Constant.sol"; +import "contracts/helpers/DepositHelperERC4626.sol"; +import "contracts/nft_descriptors/NFTDescriptorSingle4626.sol"; +import "contracts/Reliquary.sol"; +import "contracts/rewarders/RollingRewarder.sol"; +import "contracts/rewarders/ParentRewarder-Rolling.sol"; +import "contracts/rewarders/RewardsPool.sol"; + +contract MultipleRollingRewarderTest is ERC721Holder, Test { + Reliquary reliquary; + RollingRewarder rollingRewarderETH; + RollingRewarder rollingRewarderUSDC; + ERC20DecimalsMock grain; + ERC20DecimalsMock depositToken; //BPT + ERC20DecimalsMock rewardToken1; //WETH + ERC20DecimalsMock rewardToken2; //USDC + ParentRewarderRolling parent; + RewardsPool rewardsPoolETH; + RewardsPool rewardsPoolUSDC; + + uint[] requiredMaturity = [0, 7 days, 14 days, 21 days, 28 days, 90 days, 180 days, 365 days]; + uint[] levelMultipliers = [100, 120, 150, 200, 300, 400, 500, 750]; + + function setUp() public { + grain = new ERC20DecimalsMock("Grain", "GRAIN", 18); + depositToken = new ERC20DecimalsMock("Grain-BPT", "BPT", 18); + rewardToken1 = new ERC20DecimalsMock("WETH", "WETH", 18); + rewardToken2 = new ERC20DecimalsMock("USDC", "USDC", 6); + + reliquary = new Reliquary( + address(grain), + address(new Constant()), + "Reliquary Deposit", + "RELIC" + ); + + reliquary.addPool( + 1000, address(depositToken), address(0), requiredMaturity, levelMultipliers, "whole-grain", address(0), true + ); + + uint256 pid = reliquary.poolLength() - 1; + + parent = new ParentRewarderRolling( + 0, + address(grain), + address(reliquary), + pid + ); + + parent.grantRole(keccak256("CHILD_SETTER"), address(this)); + parent.grantRole(keccak256("REWARD_SETTER"), address(this)); + reliquary.grantRole(keccak256("OPERATOR"), address(this)); + reliquary.modifyPool(0, 1000, address(parent), "whole-grain", address(0), true); + rollingRewarderETH = RollingRewarder(parent.createChild(address(rewardToken1), address(this))); + rollingRewarderUSDC = RollingRewarder(parent.createChild(address(rewardToken2), address(this))); + + grain.mint(address(this), 100_000 ether); + grain.mint(address(reliquary), 100_000 ether); + grain.approve(address(reliquary), type(uint).max); + + rewardsPoolUSDC = new RewardsPool(address(rewardToken2), address(rollingRewarderUSDC)); + rewardsPoolETH = new RewardsPool(address(rewardToken1), address(rollingRewarderETH)); + + parent.setChildsRewardPool(address(rollingRewarderETH), address(rewardsPoolETH)); + parent.setChildsRewardPool(address(rollingRewarderUSDC), address(rewardsPoolUSDC)); + + } + + function testPoolLength() public { + assertTrue(reliquary.poolLength() == 1); + } + + function testCorrectNumberOfChildren() public { + address[] memory children = parent.getChildrenRewarders(); + assertTrue(children.length == 2); + } + + function testDeposit() public { + depositToken.mint(address(this), 100 ether); + depositToken.approve(address(reliquary), type(uint).max); + uint256 relicId = reliquary.createRelicAndDeposit(address(this), 0, 100 ether); + + assertEq(reliquary.balanceOf(address(this)), 1, "no Relic given"); + assertEq(reliquary.getPositionForId(relicId).amount, 100 ether, "deposited amount not expected amount"); + } + + function testEmissions() public { + uint256 ethReward = 1 ether; + uint256 usdcReward = 1000 * 10**6; + + //Fund the rewarders + rewardToken1.mint(address(rewardsPoolETH), ethReward); + rewardToken2.mint(address(rewardsPoolUSDC), usdcReward); + + //Call Fund on the pools + rewardsPoolETH.fundRewarder(); + rewardsPoolUSDC.fundRewarder(); + + depositToken.mint(address(this), 100 ether); + depositToken.approve(address(reliquary), type(uint).max); + uint256 relicId = reliquary.createRelicAndDeposit(address(this), 0, 100 ether); + + skip(7 days); + + reliquary.harvest(relicId, address(this)); + + uint256 earnedEth = IERC20(rewardToken1).balanceOf(address(this)); + uint256 earnedUsdc = IERC20(rewardToken2).balanceOf(address(this)); + assertApproxEqAbs(earnedEth, ethReward, 1e5); + assertApproxEqAbs(earnedUsdc, usdcReward, 1e5); + } + + function testDistribution() public { + parent.removeChild(address(rollingRewarderUSDC)); + rewardToken1.mint(address(rewardsPoolETH), 10 ether); + rewardsPoolETH.fundRewarder(); + + address user1 = makeAddr("user1"); + depositToken.mint(user1, 100 ether); + + vm.startPrank(user1); + depositToken.approve(address(reliquary), type(uint).max); + reliquary.createRelicAndDeposit(user1, 0, 100 ether); + vm.stopPrank(); + + skip(3.5 days); + + address user2 = makeAddr("user2"); + depositToken.mint(user2, 100 ether); + + vm.startPrank(user2); + depositToken.approve(address(reliquary), type(uint).max); + reliquary.createRelicAndDeposit(user2, 0, 100 ether); + vm.stopPrank(); + + skip(3.5 days); + + vm.prank(user1); + reliquary.harvest(1, user1); + + vm.prank(user2); + reliquary.harvest(2, user2); + + uint256 rewardUser1 = IERC20(rewardToken1).balanceOf(user1); + uint256 rewardUser2 = IERC20(rewardToken1).balanceOf(user2); + + assertApproxEqAbs(rewardUser1, 7.5 ether, 1e5, "user1 reward not expected"); + assertApproxEqAbs(rewardUser2, 2.5 ether, 1e5, "user2 reward not expected"); + + rewardToken1.mint(address(rewardsPoolETH), 10 ether); + rewardsPoolETH.fundRewarder(); + + vm.startPrank(user2); + reliquary.withdraw(100 ether, 2); + reliquary.createRelicAndDeposit(user2, 0, 100 ether); + vm.stopPrank(); + + skip(7 days); + + vm.prank(user1); + reliquary.harvest(1, user1); + + rewardUser1 = IERC20(rewardToken1).balanceOf(user1) - rewardUser1; + // factor in maturity = 20% boost for user1 + // 220 is the calculated pool size with multipliers + assertApproxEqAbs(rewardUser1, 10 ether * uint(120) / 220, 1e5, "user1 reward not expected"); + } + + function testSplit() public { + rewardToken1.mint(address(rewardsPoolETH), 10 ether); + rewardsPoolETH.fundRewarder(); + + depositToken.mint(address(this), 100 ether); + depositToken.approve(address(reliquary), type(uint).max); + reliquary.createRelicAndDeposit(address(this), 0, 100 ether); + + skip(7 days); + + address user1 = makeAddr("user1"); + depositToken.mint(user1, 120 ether); // simulate the 20% maturity boost that the other positions got + vm.startPrank(user1); + depositToken.approve(address(reliquary), type(uint).max); + reliquary.createRelicAndDeposit(user1, 0, 100 ether); + vm.stopPrank(); + + reliquary.split(1, 50 ether, address(this)); + + rewardToken1.mint(address(rewardsPoolETH), 20 ether); + rewardsPoolETH.fundRewarder(); + skip(7 days); + + reliquary.harvest(1, address(this)); + + uint256 rewardRelic1 = IERC20(rewardToken1).balanceOf(address(this)); + assertApproxEqAbs(rewardRelic1, 10 ether + 5 ether, 1e5, "reward not expected"); + + reliquary.harvest(3, address(this)); + uint256 rewardRelic2 = IERC20(rewardToken1).balanceOf(address(this)) - rewardRelic1; + assertApproxEqAbs(rewardRelic2, 5 ether, 1e5, "reward not expected"); + } + + function testMerge() public { + rewardToken1.mint(address(rewardsPoolETH), 10 ether); + rewardsPoolETH.fundRewarder(); + + depositToken.mint(address(this), 100 ether); + depositToken.approve(address(reliquary), type(uint).max); + reliquary.createRelicAndDeposit(address(this), 0, 100 ether); + + address user1 = makeAddr("user1"); + depositToken.mint(user1, 200 ether); + vm.startPrank(user1); + depositToken.approve(address(reliquary), type(uint).max); + reliquary.createRelicAndDeposit(user1, 0, 100 ether); + vm.stopPrank(); + + skip(7 days); + reliquary.harvest(1, address(this)); + + vm.startPrank(user1); + reliquary.createRelicAndDeposit(user1, 0, 100 ether); + reliquary.merge(2, 3); + + rewardToken1.mint(address(rewardsPoolETH), 10 ether); + rewardsPoolETH.fundRewarder(); + + skip(7 days); + reliquary.harvest(3, user1); + vm.stopPrank(); + + uint256 rewardMergedRelic = IERC20(rewardToken1).balanceOf(user1); + assertApproxEqAbs( + rewardMergedRelic, + 5 ether + (10 ether * uint(200) / 320), + 1e5, + "reward not expected" + ); + } + + function testShift() public { + rewardToken1.mint(address(rewardsPoolETH), 10 ether); + rewardsPoolETH.fundRewarder(); + + depositToken.mint(address(this), 100 ether); + depositToken.approve(address(reliquary), type(uint).max); + reliquary.createRelicAndDeposit(address(this), 0, 100 ether); + + address user1 = makeAddr("user1"); + depositToken.mint(user1, 200 ether); + vm.startPrank(user1); + depositToken.approve(address(reliquary), type(uint).max); + reliquary.createRelicAndDeposit(user1, 0, 100 ether); + vm.stopPrank(); + + skip(7 days); + reliquary.harvest(1, address(this)); + + vm.startPrank(user1); + reliquary.createRelicAndDeposit(user1, 0, 100 ether); + reliquary.shift(2, 3, 100 ether); + + rewardToken1.mint(address(rewardsPoolETH), 10 ether); + rewardsPoolETH.fundRewarder(); + + skip(7 days); + reliquary.harvest(3, user1); + reliquary.harvest(2, user1); + vm.stopPrank(); + + uint256 rewardMergedRelic = IERC20(rewardToken1).balanceOf(user1); + assertApproxEqAbs( + rewardMergedRelic, + 5 ether + (10 ether * uint(200) / 320), + 1e5, + "reward not expected" + ); + } + +}