to understand the valid and correct vulnerability here is an example to learn from it and find in the future more valid vulnerability when i ask you in tis contract // SPDX-License-Identifier: MIT
/

/ __ <br/>| / / ___ _ ____ _____ _ __ __ _ ___ _ __ ___
| | / _ | ’ \ \ / / _ \ '/ ` |/ _ \ ’ \ / / _ <br/>| _/\ () | | | \ V / / | | (| | __/ | | | (| /
_/_/|| ||_/ _|| _, |_|| ||___|
/ |
|/
*/
pragma solidity ^0.8.0;

import “@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol”;
import “@openzeppelin/contracts/token/ERC20/IERC20.sol”;
import “…/interfaces/ICvgControlTower.sol”;

/
* @title Cvg-Finance - LockingPositionService
* @notice Allows to lock CVG, gives yield and governance power.
* @dev When a position is minted, the amount can be split between 2 different type of CVG :
* veCVG : used for voting power ( associated with MgCVG meta-governance voting power )
* | ysCVG : used for treasury shares( allow the user to claim a part of the treasury at each TDE ( treasury distribution event ) )
* | the amount of ys/Ve the user will receive for each CVG locked is proportional with the duration of the lock.
/
contract LockingPositionService is Ownable2StepUpgradeable {
struct LockingPosition {
uint96 startCycle;
uint96 lastEndCycle;
/* @dev Percentage of the NFT dedicated to ysCvg. /
uint64 ysPercentage;
/* @dev Number of CVG Locked. /
uint256 totalCvgLocked;
/* @dev Meta Governance CVG amount. /
uint256 mgCvgAmount;
}

struct TokenView {
uint256 tokenId;
uint128 startCycle;
uint128 endCycle;
uint256 cvgLocked;
uint256 ysActual;
uint256 ysTotal;
uint256 veCvgActual;
uint256 mgCvg;
uint256 ysPercentage;
}

struct TrackingBalance {
uint256 ysToAdd;
uint256 ysToSub;
}

struct LockingExtension {
uint128 cycleId;
uint128 endCycle;
uint256 cvgLocked;
uint256 mgCvgAdded;
}

event MintLockingPosition(uint256 tokenId, LockingPosition lockingPosition, LockingExtension lockingExtension);
event IncreaseLockAmount(uint256 tokenId, LockingPosition lockingPosition, LockingExtension lockingExtension);
event IncreaseLockTime(uint256 tokenId, LockingPosition lockingPosition, uint256 oldEndCycle);
event IncreaseLockTimeAndAmount(
uint256 tokenId,
LockingPosition lockingPosition,
LockingExtension lockingExtension,
uint256 oldEndCycle
);
event UpdateTotalSupplies(uint256 newYsSupply, uint256 veCvgSupply, uint256 cycle);
event LockingPositionBurn(uint256 tokenId);

/* @dev Maximum locking time in cycle(weeks) /
uint256 public constant MAX_LOCK = 96;
/* @dev TDE duration in weeks /
uint256 public constant TDE_DURATION = 12;
uint256 public constant MAX_PERCENTAGE = 100;
/* @dev pourcentage can only used as multiple of this value /
uint256 public constant RANGE_PERCENTAGE = 10;

/* @dev Convergence ControlTower. /
ICvgControlTower public cvgControlTower;
/* @dev Convergence CVG. /
ICvg public cvg;

/* @dev Total supply of ysCvg. /
uint256 public totalSupplyYsCvg;

/* @dev Keeps global data of a LockingPosition. /
mapping(uint256 => LockingPosition) public lockingPositions;

/* @dev Keep track of the ySCvg supply changes for each cycle, so we can compute the totalSupply of ysCvg at each cycle. /
mapping(uint256 => TrackingBalance) public totalSuppliesTracking;

/* @dev Keep track of the ysCvg supply at each cycle. /
mapping(uint256 => uint256) public totalSupplyYsCvgHistories;

/* @dev Keep track of the update of locking positions. /
mapping(uint256 => LockingExtension[]) public lockExtensions;

/* @dev Address => contract is whitelisted to perform locks. /
mapping(address => bool) public isContractLocker;

/// @custom:oz-upgrades-unsafe-allow constructor
constructor() {
disableInitializers();
}

/
* @notice Initialize the contract with the ConvergenceControlTower address, set the cvgToken address, transfer ownership to the initializer.
* @param _cvgControlTower ConvergenceControlTower address.
*/
function initialize(ICvgControlTower _cvgControlTower) external initializer {
cvgControlTower = _cvgControlTower;
_transferOwnership(msg.sender);
ICvg _cvg = _cvgControlTower.cvgToken();
require(address(_cvg) != address(0), “CVG_ZERO”);
cvg = _cvg;
}

/
* @dev Some methods that are called by wallet ,
* can also be called by cvgUtilities meta functionalities
* this modifier allow to check both case.
* it also check that the token is not time locked.
/
modifier checkCompliance(uint256 tokenId, address operator) {
_checkCompliance(tokenId, operator);
_;
}

/
* @dev Some methods that are called by wallet ,
* can also be called by cvgUtilities meta functionalities
* this modifier allow to check both case.
*/
modifier onlyWalletOrWhiteListedContract() {
_onlyWalletOrWhiteListedContract();
_;
}

/
* @notice Check the owner of the token taking into consideration the operator and the msg.sender.
* @dev For the swap and bond function, the caller is the cvgUtilities contract, in which case the token property is checked with the operator, otherwise the sender msg.sender is used.
* @param _tokenId ID of the token.
* @param _operator address of the operator.
/
function _checkTokenOwnerShip(uint256 _tokenId, address _operator) internal view {
address tokenOwner = cvgControlTower.lockingPositionManager().ownerOf(_tokenId);
if (msg.sender == cvgControlTower.cvgUtilities()) {
require(operator == tokenOwner, “TOKEN_NOT_OWNED”);
} else {
require(msg.sender == tokenOwner, “TOKEN_NOT_OWNED”);
}
}

/
* @notice Check if the token is compliant to be manipulated
* this function is used on methods that can be called by the wallet or the cvgUtilities contract
* the check of ownership is done in both case , it also check that the token is not time locked
* Time lock is a feature that protects a potential buyer of a token from a malicious front run from the seller.
* @param tokenId ID of the token.
* @param operator address of the operator.
*/
function _checkCompliance(uint256 tokenId, address operator) internal view {
(address ownerOf, uint256 unlockTimestamp) = cvgControlTower.lockingPositionManager().getComplianceInfo(
tokenId
);
if (msg.sender == cvgControlTower.cvgUtilities()) {
require(operator == ownerOf, “TOKEN_NOT_OWNED”);
} else {
require(msg.sender == ownerOf, “TOKEN_NOT_OWNED”);
}
require(unlockTimestamp < block.timestamp, “TOKEN_TIMELOCKED”);
}

/
* @notice Check if the caller is a wallet or a whitelisted contract.
/
function _onlyWalletOrWhiteListedContract() internal view {
require(
// solhint-disable-next-line avoid-tx-origin
msg.sender == tx.origin || isContractLocker[msg.sender],
“NOT_CONTRACT_OR_WL”
);
}

/ =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=–=-=-=-=
INFO
=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=–=-=-=-= /
struct LockingInfo {
uint256 tokenId;
uint256 cvgLocked;
uint256 lockEnd;
uint256 ysPercentage;
uint256 mgCvg;
}

/
* @notice Get position information for a given tokenId, used by the CVG display of the token.
* @param tokenId is the token ID of the position.
/
function lockingInfo(uint256 tokenId) external view returns (LockingInfo memory) {
uint256 _cvgCycle = cvgControlTower.cvgCycle();
uint256 tokenLastEndCycle = lockingPositions[tokenId].lastEndCycle;

return
LockingInfo({
tokenId: tokenId,
cvgLocked: lockingPositions[tokenId].totalCvgLocked,
lockEnd: tokenLastEndCycle,
ysPercentage: lockingPositions[tokenId].ysPercentage,
mgCvg: _cvgCycle > tokenLastEndCycle ? 0 : lockingPositions[tokenId].mgCvgAmount
});
}

/ =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=–=-=-=-=
PUBLIC FUNCTIONS
=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=–=-=-=-= */

/
* @notice Mint a locking position (ERC721) for the user.
* @dev Lock can’t be greater than the Maximum locking time / The end of the lock must finish on a TDE event cycle | The percentage of ys determines the repartition in veCVG,mgCVG/YsCVG.
* @param lockDuration is the duration in cycle(week) of the lock.
* @param amount is the amount of cvg to lock in the position.
* @param ysPercentage percentage of lock dedicated to treasury shares (ysCVG).
* @param receiver address of the receiver of the locking position.
* @param isAddToManagedTokens add the created token in managed tokens(voting power) directly.
/
function mintPosition(
uint96 lockDuration,
uint256 amount,
uint64 ysPercentage,
address receiver,
bool isAddToManagedTokens
) external onlyWalletOrWhiteListedContract {
require(amount > 0, “LTE”);
/ @dev Percentage cannot be over 100%. */
require(ysPercentage <= MAX_PERCENTAGE, "YS%_OVER_100");
/ @dev Only percentage with multiple of 10 are possible to use. */
require(ysPercentage % RANGE_PERCENTAGE == 0, "YS%_10_MULTIPLE");
/* @dev Lock cannot be longer than MAX_LOCK. /
require(lockDuration <= MAX_LOCK, “MAX_LOCK_96_CYCLES”);

ICvgControlTower _cvgControlTower = cvgControlTower;

/* @dev Retrieve actual staking cycle. /
uint96 actualCycle = uint96(_cvgControlTower.cvgCycle());
uint96 endLockCycle = actualCycle + lockDuration;
/* @dev End of lock must finish on TDE. /
require(endLockCycle % TDE_DURATION == 0, “END_MUST_BE_TDE_MULTIPLE”);

ILockingPositionManager _lockingPositionManager = _cvgControlTower.lockingPositionManager();

/// @dev get the nextId on the LockingPosition manager
uint256 tokenId = _lockingPositionManager.nextId();

uint256 _mgCvgCreated;
/* @dev Update checkpoints for YsCvg TotalSupply and Supply by NFT. /
if (ysPercentage != 0) {
_ysCvgCheckpoint(lockDuration, (amount * ysPercentage) / MAX_PERCENTAGE, actualCycle, endLockCycle);
}

/* @dev Create voting power through Curve contract, link voting power to the token (NFT). /
if (ysPercentage != MAX_PERCENTAGE) {
uint256 amountVote = amount * (MAX_PERCENTAGE - ysPercentage);

/* @dev Timestamp of the end of locking. /
_cvgControlTower.votingPowerEscrow().create_lock(
tokenId,
amountVote / MAX_PERCENTAGE,
block.timestamp + (lockDuration + 1) * 7 days
);
/// @dev compute the amount of mgCvg
_mgCvgCreated = (amountVote * lockDuration) / (MAX_LOCK * MAX_PERCENTAGE);

/// @dev Automatically add the veCVG and mgCVG in the balance taken from Snapshot.
if (isAddToManagedTokens) {
_cvgControlTower.lockingPositionDelegate().addTokenAtMint(tokenId, receiver);
}
}

LockingPosition memory lockingPosition = LockingPosition({
startCycle: actualCycle,
lastEndCycle: endLockCycle,
totalCvgLocked: amount,
mgCvgAmount: _mgCvgCreated,
ysPercentage: ysPercentage
});

/* @dev Associate this Locking position on the tokenId. /
lockingPositions[tokenId] = lockingPosition;

LockingExtension memory lockingExtension = LockingExtension({
cycleId: actualCycle,
endCycle: endLockCycle,
cvgLocked: amount,
mgCvgAdded: _mgCvgCreated
});

/* @dev Add a lock extension with the initial params of the token minted. /
lockExtensions[tokenId].push(lockingExtension);

/* @dev Transfer CVG from user wallet to here. /
cvg.transferFrom(msg.sender, address(this), amount);

/* @dev Mint the ERC721 representing the user position. /
_lockingPositionManager.mint(receiver);

emit MintLockingPosition(tokenId, lockingPosition, lockingExtension);
}

/
* @notice Increase the amount of CVG token in the locking position proportionally from the actual cycle to the end of lock.
* @dev CheckCompliance is not used in this function, as an increase in the amount cannot be detrimental to a potential buyer.
* @param tokenId is the token ID of the position to extend
* @param amount of cvg to add to the position
* @param operator address of token owner (used when call from cvgUtilities)
/
function increaseLockAmount(
uint256 tokenId,
uint256 amount,
address operator
) external onlyWalletOrWhiteListedContract {
require(amount > 0, “LTE”);
_checkTokenOwnerShip(tokenId, operator);

ICvgControlTower _cvgControlTower = cvgControlTower;
LockingPosition memory lockingPosition = lockingPositions[tokenId];

/ @dev Retrieve actual staking cycle. /
uint128 actualCycle = _cvgControlTower.cvgCycle();

/ @dev Impossible to increase the lock in amount after the end of the lock. /
require(lockingPosition.lastEndCycle > actualCycle, “LOCK_OVER”);

/ @dev YsCvg TotalSupply Part, access only if some % has been given to ys on the NFT. /
if (lockingPosition.ysPercentage != 0) {
_ysCvgCheckpoint(
lockingPosition.lastEndCycle - actualCycle,
(amount * lockingPosition.ysPercentage) / MAX_PERCENTAGE,
actualCycle,
lockingPosition.lastEndCycle
);
}

uint256 _newVotingPower;
/* @dev Update voting power through Curve contract, link voting power to the nft tokenId. /
if (lockingPosition.ysPercentage != MAX_PERCENTAGE) {
uint256 amountVote = amount * (MAX_PERCENTAGE - lockingPosition.ysPercentage);
_cvgControlTower.votingPowerEscrow().increase_amount(tokenId, amountVote / MAX_PERCENTAGE);
_newVotingPower = (amountVote * (lockingPosition.lastEndCycle - actualCycle)) / (MAX_LOCK * MAX_PERCENTAGE);
lockingPositions[tokenId].mgCvgAmount += _newVotingPower;
}

/* @dev Update cvgLocked balance. /
lockingPositions[tokenId].totalCvgLocked += amount;

LockingExtension memory lockingExtension = LockingExtension({
cycleId: actualCycle,
endCycle: lockingPosition.lastEndCycle,
cvgLocked: amount,
mgCvgAdded: _newVotingPower
});

/* @dev Add a lock extension linked to the Amount Extension. /
lockExtensions[tokenId].push(lockingExtension);

/* @dev Transfer CVG from user wallet to here. /
cvg.transferFrom(msg.sender, address(this), amount);

emit IncreaseLockAmount(tokenId, lockingPosition, lockingExtension);
}

/
* @notice Increase the time of the lock
* Increasing the locking time will not increase the amount of ysCvg & mgCvg
* The amounts will be just extended on the new duration.
* @dev The token must not be time locked , as an increase in time can be detrimental to a potential buyer.
* @param tokenId is the token ID of the position
* @param durationAdd is the number of cycle to add to the position lockingTime
/
function increaseLockTime(
uint256 tokenId,
uint256 durationAdd
) external checkCompliance(tokenId, address(0)) onlyWalletOrWhiteListedContract {
ICvgControlTower _cvgControlTower = cvgControlTower;
/ @dev Retrieve actual staking cycle. /
uint128 actualCycle = _cvgControlTower.cvgCycle();

LockingPosition storage lockingPosition = lockingPositions[tokenId];
uint256 oldEndCycle = lockingPosition.lastEndCycle + 1;
uint256 newEndCycle = oldEndCycle + durationAdd;

/ @dev Not possible extend a lock in duration after it’s expiration. /
require(oldEndCycle > actualCycle, “LOCK_TIME_OVER”);

/ @dev Not possible to have an active lock longer than the MAX_LOCK. /
require(newEndCycle - actualCycle - 1 <= MAX_LOCK, “MAX_LOCK_96_CYCLES”);

/* @dev As the oldEnd cycle is a xTDE_DURATION. /
/* @dev We just need to verify that the time we add is a xTDE_DURATION to ensure new lock is ending on a xTDE_DURATION. /
require(durationAdd % TDE_DURATION == 0, “NEW_END_MUST_BE_TDE_MULTIPLE”);

/* @dev YsCvg TotalSupply Part, access only if some % has been given to ys on the NFT. /
if (lockingPosition.ysPercentage != 0) {
/* @dev Retrieve the balance registered at the cycle where the ysBalance is supposed to drop. /
uint256 _ysToReport = balanceOfYsCvgAt(tokenId, oldEndCycle - 1);
/* @dev Add this value to the tracking on the oldEndCycle. /
totalSuppliesTracking[oldEndCycle].ysToAdd += _ysToReport;
/* @dev Report this value in the newEndCycle in the Sub part. /
totalSuppliesTracking[newEndCycle].ysToSub += _ysToReport;
}

/* @dev Vote part, access here only if some % has been given to ve/mg on the NFT. /
if (lockingPosition.ysPercentage != MAX_PERCENTAGE) {
/* @dev Increase Locking time to a new timestamp, computed with the cycle. /
_cvgControlTower.votingPowerEscrow().increase_unlock_time(
tokenId,
block.timestamp + ((newEndCycle - actualCycle) * 7 days)
);
}

/* @dev Update the new end cycle on the locking position. /
lockingPosition.lastEndCycle = uint96(newEndCycle - 1);

emit IncreaseLockTime(tokenId, lockingPosition, oldEndCycle - 1);
}

/
* @notice Increase first the time THEN the amount in the position proportionally from the actual cycle to the end of lock.
* @dev The token must not be time locked, as an increase in the time can be detrimental to a potential buyer.
* @param tokenId is the token ID of the position
* @param durationAdd is the number of cycle to add to the position lockingTime
* @param amount of cvg to add to the position
* @param operator address of token owner (used when call from cvgUtilities)
/
function increaseLockTimeAndAmount(
uint256 tokenId,
uint256 durationAdd,
uint256 amount,
address operator
) external checkCompliance(tokenId, operator) onlyWalletOrWhiteListedContract {
require(amount > 0, “LTE”);
ICvgControlTower _cvgControlTower = cvgControlTower;
/ @dev Retrieve actual staking cycle. /
uint128 actualCycle = _cvgControlTower.cvgCycle();

LockingPosition storage lockingPosition = lockingPositions[tokenId];
uint256 oldEndCycle = lockingPosition.lastEndCycle + 1;
/ @dev Calculating the new end cycle. /
uint256 newEndCycle = oldEndCycle + durationAdd;
/ @dev Check the new end cycle. /
require(oldEndCycle > actualCycle, “LOCK_OVER”);
require(newEndCycle - actualCycle - 1 <= MAX_LOCK, “MAX_LOCK_96_CYCLES”);
require(durationAdd % TDE_DURATION == 0, “END_MUST_BE_TDE_MULTIPLE”);

if (lockingPosition.ysPercentage != 0) {
/* @dev Taking in account the change of YsCvg TotalSupply update. /
uint256 _ysToReport = balanceOfYsCvgAt(tokenId, oldEndCycle - 1);
totalSuppliesTracking[oldEndCycle].ysToAdd += _ysToReport;
totalSuppliesTracking[newEndCycle].ysToSub += _ysToReport;

_ysCvgCheckpoint(
newEndCycle - actualCycle - 1,
(amount * lockingPosition.ysPercentage) / MAX_PERCENTAGE,
actualCycle,
newEndCycle - 1
);
}

uint256 _newVotingPower;

if (lockingPosition.ysPercentage != MAX_PERCENTAGE) {
/* @dev Update voting power through veCVG contract, link voting power to the nft tokenId. /
uint256 amountVote = amount * (MAX_PERCENTAGE - lockingPosition.ysPercentage);
_newVotingPower = (amountVote * (newEndCycle - actualCycle - 1)) / (MAX_LOCK * MAX_PERCENTAGE);
lockingPosition.mgCvgAmount += _newVotingPower;

_cvgControlTower.votingPowerEscrow().increase_unlock_time_and_amount(
tokenId,
block.timestamp + ((newEndCycle - actualCycle) * 7 days),
amountVote / MAX_PERCENTAGE
);
}

/* @dev Update the new end cycle on the locking position. /
lockingPosition.lastEndCycle = uint96(newEndCycle - 1);
lockingPosition.totalCvgLocked += amount;

LockingExtension memory _lockingExtension = LockingExtension({
cycleId: actualCycle,
endCycle: uint128(newEndCycle - 1),
cvgLocked: amount,
mgCvgAdded: _newVotingPower
});
/* @dev Keep track of the update on the lock , including mgCvg part. /
lockExtensions[tokenId].push(_lockingExtension);

/* @dev Transfer CVG /
cvg.transferFrom(msg.sender, address(this), amount);

emit IncreaseLockTimeAndAmount(tokenId, lockingPosition, _lockingExtension, oldEndCycle - 1);
}

/
* @notice Unlock CVG tokens under the NFT Locking Position : Burn the NFT, Transfer back the CVG to the user. Rewards from YsDistributor must be claimed before or they will be lost. * @dev The locking time must be over
* @param tokenId to burn
/
function burnPosition(uint256 tokenId) external {
_checkTokenOwnerShip(tokenId, address(0));
ICvgControlTower _cvgControlTower = cvgControlTower;
uint256 lastEndCycle = lockingPositions[tokenId].lastEndCycle;
uint256 ysPercentage = lockingPositions[tokenId].ysPercentage;
uint256 totalCvgLocked = lockingPositions[tokenId].totalCvgLocked;

require(_cvgControlTower.cvgCycle() > lastEndCycle, “LOCKED”);

/ @dev if the position contains veCvg , we must remove it from the voting escrow /
if (ysPercentage != MAX_PERCENTAGE) {
_cvgControlTower.votingPowerEscrow().withdraw(tokenId);
}

/ @dev Burn the NFT representing the position. /
_cvgControlTower.lockingPositionManager().burn(tokenId);

/ @dev Transfer CVG back to the user. /
cvg.transfer(msg.sender, totalCvgLocked);

emit LockingPositionBurn(tokenId);
}

/ =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=–=-=-=-=
ONLY CONTROL TOWER
=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=–=-=-=-= /
/
* @notice Compute the new Ys total supply by adding and subtracting checkpoints formerly created on mint & increaseLock by the _YsCvgCheckpoint().
* @dev Only callable by ControlTower ( DAO ).
/
function updateYsTotalSupply() external {
ICvgControlTower _cvgControlTower = cvgControlTower;

require(msg.sender == address(_cvgControlTower), “NOT_CONTROL_TOWER”);
uint256 actualCycle = _cvgControlTower.cvgCycle();

uint256 totalSupplyYsCvgBeforeUpdate = totalSupplyYsCvg;

/ @dev Register the last totalSupply for the past cycle. /
totalSupplyYsCvgHistories[actualCycle - 1] = totalSupplyYsCvgBeforeUpdate;

/ @dev Update ysCVG total supply with checkpoints for the actual cycle /
totalSupplyYsCvg =
totalSupplyYsCvgBeforeUpdate +
totalSuppliesTracking[actualCycle].ysToAdd -
totalSuppliesTracking[actualCycle].ysToSub;

emit UpdateTotalSupplies(
totalSupplyYsCvgBeforeUpdate,
_cvgControlTower.votingPowerEscrow().total_supply(),
actualCycle - 1
);
}

/ =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=–=-=-=-=
INTERNAL FUNCTIONS
=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=–=-=-=-= /
/
* @notice Compute the new Ys by adding and subtracting
* checkpoints formerly created on mint & increaseLock by the _YsCvgCheckpoint().
* @dev Only callable by ControlTower ( DAO ).
* @param lockDuration is the duration in cycle(week) of the lock
* @param cvgLockAmount is the amount of cvg to lock in the position
* @param actualCycle is the actual cycle of the cvg
* @param endLockCycle is the end cycle of the lock
/
function _ysCvgCheckpoint(
uint256 lockDuration,
uint256 cvgLockAmount,
uint256 actualCycle,
uint256 endLockCycle
) internal {
/ @dev Compute the amount of ysCVG on this Locking Position proportionally with the ratio of lockDuration and MAX LOCK duration. /
uint256 ysTotalAmount = (lockDuration * cvgLockAmount) / MAX_LOCK;
uint256 realStartCycle = actualCycle + 1;
uint256 realEndCycle = endLockCycle + 1;
/ @dev If the lock is not made on a TDE cycle, we need to compute the ratio of ysCVG for the current partial TDE /
if (actualCycle % TDE_DURATION != 0) {
/ @dev Get the cycle id of next TDE to be taken into account for this LockingPosition. /
uint256 nextTdeCycle = (actualCycle / TDE_DURATION + 1) * TDE_DURATION + 1;
/ @dev Represent the amount of ysCvg to be taken into account on the next TDE of this LockingPosition. /
uint256 ysNextTdeAmount = ((nextTdeCycle - realStartCycle) * ysTotalAmount) / TDE_DURATION;

totalSuppliesTracking[realStartCycle].ysToAdd += ysNextTdeAmount;

/* @dev When a lock is greater than a TDE_DURATION /
if (lockDuration >= TDE_DURATION) {
/* @dev we add the calculations for the next full TDE /
totalSuppliesTracking[nextTdeCycle].ysToAdd += ysTotalAmount - ysNextTdeAmount;
totalSuppliesTracking[realEndCycle].ysToSub += ysTotalAmount;
}
/* @dev If the lock less than TDE_DURATION. /
else {
/* @dev We simply remove the amount from the supply calculation at the end of the TDE /
totalSuppliesTracking[realEndCycle].ysToSub += ysNextTdeAmount;
}
}
/* @dev If the lock is performed on a TDE cycle /
else {
totalSuppliesTracking[realStartCycle].ysToAdd += ysTotalAmount;
totalSuppliesTracking[realEndCycle].ysToSub += ysTotalAmount;
}
}

/ =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=–=-=-=-=
VIEW FUNCTIONS
=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=–=-=-=-= /

/
* @notice Returns the information needed to display the lock position display svg.
* @param _tokenId id of the token
*/
function tokenInfos(uint256 _tokenId) external view returns (TokenView memory) {
LockingPosition memory _lockingPosition = lockingPositions[_tokenId];
ICvgControlTower _cvgControlTower = cvgControlTower;

uint256 _cvgCycle = _cvgControlTower.cvgCycle();

return
TokenView({
tokenId: _tokenId,
cvgLocked: _lockingPosition.totalCvgLocked,
startCycle: _lockingPosition.startCycle,
endCycle: _lockingPosition.lastEndCycle,
veCvgActual: _cvgControlTower.votingPowerEscrow().balanceOf(_tokenId),
ysTotal: balanceOfYsCvgAt(_tokenId, _lockingPosition.lastEndCycle),
ysActual: balanceOfYsCvgAt(_tokenId, _cvgCycle),
mgCvg: _cvgCycle > _lockingPosition.lastEndCycle ? 0 : _lockingPosition.mgCvgAmount,
ysPercentage: _lockingPosition.ysPercentage
});
}

/
* @notice Fetch the balance of veCVG (gauge voting power) for a specified tokenId.
* @param _tokenId id of the token
/
function balanceOfVeCvg(uint256 _tokenId) public view returns (uint256) {
return cvgControlTower.votingPowerEscrow().balanceOf(_tokenId);
}

/
* @notice Fetch the balance of ysCVG (treasury share) for a specified tokenId and at a specified cycle, can be in the future.
* @param _tokenId id of the token
* @param _cycleId id of the cycle
/
function balanceOfYsCvgAt(uint256 _tokenId, uint256 _cycleId) public view returns (uint256) {
require(_cycleId != 0, “NOT_EXISTING_CYCLE”);

LockingPosition memory _lockingPosition = lockingPositions[_tokenId];
LockingExtension[] memory _extensions = lockExtensions[_tokenId];
uint256 _ysCvgBalance;

/ @dev If the requested cycle is before or after the lock , there is no balance. /
if (_lockingPosition.startCycle >= _cycleId || _cycleId > _lockingPosition.lastEndCycle) {
return 0;
}
/ @dev We go through the extensions to compute the balance of ysCvg at the cycleId /
for (uint256 i; i < _extensions.length; ) {
/ @dev Don’t take into account the extensions if in the future. /
if (_extensions[i].cycleId < _cycleId) {
LockingExtension memory _extension = _extensions[i];
uint256 _firstTdeCycle = TDE_DURATION * (_extension.cycleId / TDE_DURATION + 1);
uint256 _ysTotal = (((_extension.endCycle - _extension.cycleId)
_extension.cvgLocked
_lockingPosition.ysPercentage) / MAX_PERCENTAGE) / MAX_LOCK;
uint256 _ysPartial = ((_firstTdeCycle - _extension.cycleId) * _ysTotal) / TDE_DURATION;
/ @dev For locks that last less than 1 TDE. /
if (_extension.endCycle - _extension.cycleId <= TDE_DURATION) {
_ysCvgBalance += _ysPartial;
} else {
_ysCvgBalance += _cycleId <= _firstTdeCycle ? _ysPartial : _ysTotal;
}
}
++i;
}
return _ysCvgBalance;
}

/
* @notice Fetch the balance of mgCVG (meta-governance voting power ) for a specified tokenId and at a specified cycle, this can be in the future.
/
function balanceOfMgCvgAt(uint256 _tokenId, uint256 _cycleId) public view returns (uint256) {
require(_cycleId != 0, “NOT_EXISTING_CYCLE”);

LockingPosition memory _lockingPosition = lockingPositions[_tokenId];
LockingExtension[] memory _extensions = lockExtensions[_tokenId];
uint256 _mgCvgBalance;

/ @dev If the requested cycle is before or after the lock , there is no balance. /
if (_lockingPosition.startCycle > _cycleId || _cycleId > _lockingPosition.lastEndCycle) {
return 0;
}
/ @dev We go through the extensions to compute the balance of mgCvg at the cycleId /
for (uint256 i; i < _extensions.length; ) {
LockingExtension memory _extension = _extensions[i];
if (_extension.cycleId <= _cycleId) {
_mgCvgBalance += _extension.mgCvgAdded;
}
++i;
}

return _mgCvgBalance;
}

/
* @notice Fetch the balance of mgCVG (meta-governance voting power ) for a specified tokenId.
* @param _tokenId id of the token
*/
function balanceOfMgCvg(uint256 _tokenId) public view returns (uint256) {
return balanceOfMgCvgAt(_tokenId, cvgControlTower.cvgCycle());
}

/
* @notice Fetch the voting power (in veCvg) for a specified address, used in the Cvg Governance proposal strategy.
* @param _user is the address that we want to fetch voting power from
/
function veCvgVotingPowerPerAddress(address _user) external view returns (uint256) {
uint256 _totalVotingPower;

ILockingPositionDelegate _lockingPositionDelegate = cvgControlTower.lockingPositionDelegate();

(uint256[] memory tokenIdsOwneds, uint256[] memory tokenIdsDelegateds) = _lockingPositionDelegate
.getTokenVeOwnedAndDelegated(_user);

/ @dev Sum voting power from delegated tokenIds to _user. /
for (uint256 i; i < tokenIdsDelegateds.length; ) {
uint256 _tokenId = tokenIdsDelegateds[i];
/ @dev Check if is really delegated, if not ve voting power for this tokenId is 0. /
if (_user == _lockingPositionDelegate.delegatedVeCvg(_tokenId)) {
_totalVotingPower += balanceOfVeCvg(_tokenId);
}

unchecked {
++i;
}
}

ILockingPositionManager _lockingPositionManager = cvgControlTower.lockingPositionManager();

/ @dev Sum voting power from _user owned tokenIds. /
for (uint256 i; i < tokenIdsOwneds.length; ) {
uint256 _tokenId = tokenIdsOwneds[i];
/ @dev Check if is really owned AND not delegated to another user,if not ve voting power for this tokenId is 0. /
if (
_lockingPositionDelegate.delegatedVeCvg(_tokenId) == address(0) &&
_user == _lockingPositionManager.ownerOf(_tokenId)
) {
_totalVotingPower += balanceOfVeCvg(_tokenId);
}

unchecked {
++i;
}
}

return _totalVotingPower;
}

/
* @notice Fetch the voting power (in mgCVG) for a specified address, used in Meta-governance strategy
* @param _user is the address that we want to fetch voting power from
/
function mgCvgVotingPowerPerAddress(address _user) public view returns (uint256) {
uint256 _totalMetaGovernance;

ILockingPositionDelegate _lockingPositionDelegate = cvgControlTower.lockingPositionDelegate();

(uint256[] memory tokenIdsOwneds, uint256[] memory tokenIdsDelegateds) = _lockingPositionDelegate
.getTokenMgOwnedAndDelegated(_user);

/ @dev Sum voting power from delegated (allowed) tokenIds to _user. /
for (uint256 i; i < tokenIdsDelegateds.length; ) {
uint256 _tokenId = tokenIdsDelegateds[i];
(uint256 _toPercentage, , uint256 _toIndex) = _lockingPositionDelegate.getMgDelegateeInfoPerTokenAndAddress(
_tokenId,
_user
);
/ @dev Check if is really delegated, if not mg voting power for this tokenId is 0. /
if (_toIndex < 999) {
uint256 _tokenBalance = balanceOfMgCvg(_tokenId);
_totalMetaGovernance += (_tokenBalance * _toPercentage) / MAX_PERCENTAGE;
}

unchecked {
++i;
}
}

ILockingPositionManager _lockingPositionManager = cvgControlTower.lockingPositionManager();

/ @dev Sum voting power from _user owned (allowed) tokenIds. /
for (uint256 i; i < tokenIdsOwneds.length; ) {
uint256 _tokenId = tokenIdsOwneds[i];
/* @dev Check if is really owned,if not mg voting power for this tokenId is 0. /
if (_user == _lockingPositionManager.ownerOf(_tokenId)) {
(, uint256 _totalPercentageDelegated, ) = _lockingPositionDelegate.getMgDelegateeInfoPerTokenAndAddress(
_tokenId,
_user
);
uint256 _tokenBalance = balanceOfMgCvg(_tokenId);

_totalMetaGovernance += (_tokenBalance * (MAX_PERCENTAGE - _totalPercentageDelegated)) / MAX_PERCENTAGE;
}

unchecked {
++i;
}
}

return _totalMetaGovernance;
}

/
* @notice Get the supply of YsCvg at a given cycle, can be in the future.
* @param _at cycle requested
/
function totalSupplyOfYsCvgAt(uint256 _at) external view returns (uint256) {
require(_at != 0, “NOT_EXISTING_CYCLE”);

uint256 actualCycle = cvgControlTower.cvgCycle();
uint256 _ysCvgAt;

if (actualCycle <= _at) {
/ @dev If the requested cycle is in the future/actual cycle, we compute the future balance with the tracking. /
/ @dev Start from the last known totalSupply . /
_ysCvgAt = totalSupplyYsCvgHistories[actualCycle - 1];
for (uint256 i = actualCycle; i <= _at; ) {
_ysCvgAt += totalSuppliesTracking[i].ysToAdd;
_ysCvgAt -= totalSuppliesTracking[i].ysToSub;
++i;
}
} else {
/ @dev If the requested cycle is in the past, we can directly return the balance. */
_ysCvgAt = totalSupplyYsCvgHistories[_at];
}
return _ysCvgAt;
}

/
* @notice Get the reward amount at a given cycle for a given tokenId and a reward token.
* @param tokenId id of the token
* @param tdeId id of the TDE
* @param _token address of the reward token
/
function getTokenRewardAmount(uint256 tokenId, uint256 tdeId, IERC20 _token) external view returns (uint256) {
uint256 cycleClaimed = tdeId * TDE_DURATION;
/ @dev Processing the share of this token ID /
uint256 share = (balanceOfYsCvgAt(tokenId, cycleClaimed) * 10 ** 20) / totalSupplyYsCvgHistories[cycleClaimed];

/ @dev Return the amount of reward for this share. */
return cvgControlTower.ysDistributor().getTokenRewardAmountForTde(_token, tdeId, share);
}

/**
* @notice Add/remove a contract address to the whitelist.
* @param contractWL address of the contract
/
function toggleContractLocker(address contractWL) external onlyOwner {
isContractLocker[contractWL] = !isContractLocker[contractWL];
}
} this correct vulnerbaility medium
Division difference can result in a revert when claiming treasury yield and excess rewards to some users
Summary

Different ordering of calculations are used to compute ysTotal in different situations. This causes the totalShares tracked to be less than the claimable amount of shares
Vulnerability Detail

ysTotal is calculated differently when adding to totalSuppliesTracking and when computing balanceOfYsCvgAt.
When adding to totalSuppliesTracking, the calculation of ysTotal is as follows:

uint256 cvgLockAmount = (amount * ysPercentage) / MAX_PERCENTAGE;
uint256 ysTotal = (lockDuration * cvgLockAmount) / MAX_LOCK;

In balanceOfYsCvgAt, ysTotal is calculated as follows

uint256 ysTotal = (((endCycle - startCycle) * amount * ysPercentage) / MAX_PERCENTAGE) / MAX_LOCK;

This difference allows the balanceOfYsCvgAt to be greater than what is added to totalSuppliesTracking
POC

startCycle 357
endCycle 420
lockDuration 63
amount 2
ysPercentage 80

Calculation in totalSuppliesTracking gives:

uint256 cvgLockAmount = (2 * 80) / 100; == 1
uint256 ysTotal = (63 * 1) / 96; == 0

Calculation in balanceOfYsCvgAt gives:

uint256 ysTotal = ((63 * 2 * 80) / 100) / 96; == 10080 / 100 / 96 == 1

Example Scenario

Alice, Bob and Jake locks cvg for 1 TDE and obtains rounded up balanceOfYsCvgAt. A user who is aware of this issue can exploit this issue further by using increaseLockAmount with small amount values by which the total difference difference b/w the user’s calculated balanceOfYsCvgAt and the accounted amount in totalSuppliesTracking can be increased. Bob and Jake claims the reward at the end of reward cycle. When Alice attempts to claim rewards, it reverts since there is not enough reward to be sent.
Impact

This breaks the shares accounting of the treasury rewards. Some user’s will get more than the actual intended rewards while the last withdrawals will result in a revert
Code Snippet

totalSuppliesTracking calculation

In mintPosition
https://github.com/sherlock-audit/2023-11-convergence/blob/main/sherlock-cvg/contracts/Locking/LockingPositionService.sol#L261-L263

In increaseLockAmount
https://github.com/sherlock-audit/2023-11-convergence/blob/e894be3e36614a385cf409dc7e278d5b8f16d6f2/sherlock-cvg/contracts/Locking/LockingPositionService.sol#L339-L345

In increaseLockTimeAndAmount
https://github.com/sherlock-audit/2023-11-convergence/blob/main/sherlock-cvg/contracts/Locking/LockingPositionService.sol#L465-L470

_ysCvgCheckpoint
https://github.com/sherlock-audit/2023-11-convergence/blob/main/sherlock-cvg/contracts/Locking/LockingPositionService.sol#L577-L584

balanceOfYsCvgAt calculation
https://github.com/sherlock-audit/2023-11-convergence/blob/main/sherlock-cvg/contracts/Locking/LockingPositionService.sol#L673-L675
Tool used

Manual Review
Recommendation

Perform the same calculation in both places

+++ uint256 _ysTotal = (_extension.endCycle - _extension.cycleId) ((_extension.cvgLocked * _lockingPosition.ysPercentage) / MAX_PERCENTAGE) / MAX_LOCK;
— uint256 ysTotal = (((endCycle - startCycle) * amount * ysPercentage) / MAX_PERCENTAGE) / MAX_LOCK;
Based on the given information, the identified vulnerability in the LockingPositionService contract of the Convergence Finance platform is a rounding error due to inconsistent calculation of ysTotal (treasury shares total). This discrepancy leads to a scenario where the actual balance of ysCvg (balanceOfYsCvgAt) can incorrectly exceed the amount tracked in totalSuppliesTracking, potentially allowing some users to claim more rewards than intended and causing reverts for later claims when the reward pool is insufficient.

### Vulnerable Parts of the Code:

When calculating totalSuppliesTracking:


// In the function mintPosition, increaseLockAmount, and increaseLockTimeAndAmount
uint256 ysTotalAmount = (lockDuration * cvgLockAmount) / MAX_LOCK;


Vs.


// In the function balanceOfYsCvgAt
uint256 ysTotal = (((endCycle - startCycle) * amount * ysPercentage) / MAX_PERCENTAGE) / MAX_LOCK;



### Explanation of the Vulnerability:

The root of the issue lies in two different calculations of ysCvg shares:

1. When adding to totalSuppliesTracking, the calculation uses intermediate cvgLockAmount:


   uint256 cvgLockAmount = (amount * ysPercentage) / MAX_PERCENTAGE;
   uint256 ysTotal = (lockDuration * cvgLockAmount) / MAX_LOCK;


   This approach introduces potential for rounding down to zero due to Solidity’s integer division.

2. In balanceOfYsCvgAt, the calculation directly incorporates all terms:


   uint256 ysTotal = (((endCycle - startCycle) * amount * ysPercentage) / MAX_PERCENTAGE) / MAX_LOCK;


   This calculation could result in a non-zero value for cases where the previous method rounds down to zero.

This discrepancy allows users to have an inaccurately higher share count than what was added to the total supply tracking through totalSuppliesTracking. Consequently, reward claims computed using balanceOfYsCvgAt could deplete the reward pool more than intended, leading to a failure in later reward claims when insufficient funds are left in the pool due to earlier excessive withdrawals.

### Impact:

- Users claiming rewards earlier could receive more than their fair share due to rounding errors, depleting the reward pool.
- Users attempting to claim rewards later, when the pool is drained, would experience transaction reverts, resulting in an inability to claim their deserved shares.
- Trust in the platform’s reward distribution mechanism could be compromised if users observe inconsistent reward calculations or face issues when claiming.

### Recommendation:

To address this vulnerability, calculations for ysTotal should be made consistently across the contract to prevent rounding errors and ensure accurate tracking of supply and claims. Specifically, the calculations in totalSuppliesTracking and balanceOfYsCvgAt should be aligned, possibly by refactoring both to a common internal function that is used in all relevant places. Additionally:

- Implement thorough testing of edge cases where rounding may occur and compare the computed values for discrepancies.
- Consider using fixed-point mathematics libraries to handle division and multiplication operations to minimize rounding errors.
- Review the economic model to determine appropriate ways to handle any rounding benefits, potentially buffering the reward pool to cover minor discrepancies.

By performing the same calculation consistently and accurately accounting for ysCvg shares in both tracking and claiming, the reward distribution system’s integrity can be maintained, ensuring users receive the correct amount of rewards and building trust in the platform.

review this contract line by line with deeper and higher understanding in solidity and find the correct and vulnerability or bug that is exist in the contract and take previous correct vulenrbaility that i give as an example to find valid vulenrbaility that affect the contract // SPDX-License-Identifier: GPL-3.0-or-later
pragma solidity 0.8.13;

import {CoreRef} from “@src/core/CoreRef.sol”;
import {CoreRoles} from “@src/core/CoreRoles.sol”;
import {SimplePSM} from “@src/loan/SimplePSM.sol”;
import {GuildToken} from “@src/tokens/GuildToken.sol”;
import {CreditToken} from “@src/tokens/CreditToken.sol”;

/**
@title ProfitManager
@author eswak
@notice This contract manages profits generated in the system and how it is distributed
between the various stakeholders.

This contract also manages a surplus buffer, which acts as first-loss capital in case of
bad debt. When bad debt is created beyond the surplus buffer, this contract decrements
the creditMultiplier value held in its storage, which has the effect of reducing the
value of CREDIT everywhere in the system.

When a loan generates profit (interests), the profit is traced back to users voting for
this lending term (gauge), which subsequently allows pro-rata distribution of profits to
GUILD holders that vote for the most productive gauges.

Seniority stack of the debt, in case of losses :
- per term surplus buffer (donated to global surplus buffer when loss is reported)
- global surplus buffer
- finally, credit holders (by updating down the creditMultiplier)
*/
contract ProfitManager is CoreRef {
/// @notice reference to GUILD token.
address public guild;

/// @notice reference to CREDIT token.
address public credit;

/// @notice reference to CREDIT token PSM.
address public psm;

/// @notice profit index of a given gauge
mapping(address => uint256) public gaugeProfitIndex;

/// @notice profit index of a given user in a given gauge
mapping(address => mapping(address => uint256)) public userGaugeProfitIndex;

/// @dev internal structure used to optimize storage read, public functions use
/// uint256 numbers with 18 decimals.
struct ProfitSharingConfig {
uint32 surplusBufferSplit; // percentage, with 9 decimals (!) that go to surplus buffer
uint32 guildSplit; // percentage, with 9 decimals (!) that go to GUILD holders
uint32 otherSplit; // percentage, with 9 decimals (!) that go to other address if != address(0)
address otherRecipient; // address receiving otherSplit
}

/// @notice configuration of profit sharing.
/// surplusBufferSplit, guildSplit, and otherSplit are expressed as percentages with 9 decimals,
/// so a value of 1e9 would direct 100% of profits. The sum should be <= 1e9.
/// The rest (if the sum of guildSplit + otherSplit is < 1e9) is distributed to lenders of the
/// system, CREDIT holders, through the rebasing mechanism (CreditToken.distribute()).
/// If otherRecipient is set to address(0), otherSplit must equal 0.
/// The share of profit to otherRecipient is sent through a regular ERC20.transfer().
/// This structure is optimized for storage packing, all external interfaces reference
/// percentages encoded as uint256 with 18 decimals.
ProfitSharingConfig internal profitSharingConfig;

/// @notice amount of first-loss capital in the system.
/// This is a number of CREDIT token held on this contract that can be used to absorb losses in
/// cases where a loss is reported through notifyPnL. The surplus buffer is depleted first, and
/// if the loss is greater than the surplus buffer, the creditMultiplier is updated down.
uint256 public surplusBuffer;

/// @notice amount of first-loss capital for a given term.
/// This is a number of CREDIT token held on this contract that can be used to absorb losses in
/// cases where a loss is reported through notifyPnL in a given term.
/// When a loss is reported in a given term, its termSuplusBuffer is donated to the general
/// surplusBuffer before calculating the loss.
mapping(address => uint256) public termSurplusBuffer;

/// @notice multiplier for CREDIT value in the system.
/// e.g. a value of 0.7e18 would mean that CREDIT has been discounted by 30% so far in the system,
/// and that all lending terms will allow 1/0.7=1.42 times more CREDIT to be borrowed per collateral
/// tokens, and all active debts are also affected by this multiplier during the update (e.g. if an
/// address owed 1000 CREDIT in an active loan, they now owe 1428 CREDIT).
/// The CREDIT multiplier can only go down (CREDIT can only lose value over time, when bad debt
/// is created in the system). To make CREDIT a valuable asset to hold, profits generated by the system
/// shall be redistributed to holders through a savings rate or another mechanism.
uint256 public creditMultiplier = 1e18;

/// @notice minimum size of CREDIT loans.
/// this parameter is here to ensure that the gas costs of liquidation do not
/// outsize minimum overcollateralization (which could result in bad debt
/// on otherwise sound loans).
/// This value is adjusted up when the creditMultiplier goes down.
uint256 internal _minBorrow = 100e18;

/// @notice tolerance on new borrows regarding gauge weights.
/// For a total supply or 100 credit, and 2 gauges each at 50% weight,
/// the ideal borrow amount for each gauge is 50 credit. To facilitate
/// growth of the protocol, a tolerance is allowed compared to the ideal
/// gauge weights.
/// This tolerance is expressed as a percentage with 18 decimals.
/// A tolerance of 1e18 (100% - or 0% deviation compared to ideal weights)
/// can result in a deadlock situation where no new borrows are allowed.
uint256 public gaugeWeightTolerance = 1.2e18; // 120%

constructor(address _core) CoreRef(_core) {
emit MinBorrowUpdate(block.timestamp, 100e18);
}

/// @notice emitted when a profit or loss in a gauge is notified.
event GaugePnL(address indexed gauge, uint256 indexed when, int256 pnl);

/// @notice emitted when surplus buffer is updated.
event SurplusBufferUpdate(uint256 indexed when, uint256 newValue);

/// @notice emitted when surplus buffer of a given term is updated.
event TermSurplusBufferUpdate(
uint256 indexed when,
address indexed term,
uint256 newValue
);

/// @notice emitted when CREDIT multiplier is updated.
event CreditMultiplierUpdate(uint256 indexed when, uint256 newValue);

/// @notice emitted when GUILD profit sharing is updated.
event ProfitSharingConfigUpdate(
uint256 indexed when,
uint256 surplusBufferSplit,
uint256 creditSplit,
uint256 guildSplit,
uint256 otherSplit,
address otherRecipient
);

/// @notice emitted when a GUILD member claims their CREDIT rewards.
event ClaimRewards(
uint256 indexed when,
address indexed user,
address indexed gauge,
uint256 amount
);

/// @notice emitted when minBorrow is updated
event MinBorrowUpdate(uint256 indexed when, uint256 newValue);

/// @notice emitted when gaugeWeightTolerance is updated
event GaugeWeightToleranceUpdate(uint256 indexed when, uint256 newValue);

/// @notice get the minimum borrow amount
function minBorrow() external view returns (uint256) {
return (_minBorrow * 1e18) / creditMultiplier;
}

/// @notice initialize references to GUILD & CREDIT tokens.
function initializeReferences(
address _credit,
address _guild,
address _psm
) external onlyCoreRole(CoreRoles.GOVERNOR) {
assert(
credit == address(0) && guild == address(0) && psm == address(0)
);
credit = _credit;
guild = _guild;
psm = _psm;
}

/// @notice returns the sum of all borrowed CREDIT, not including unpaid interests
/// and creditMultiplier changes that could make debt amounts higher than the initial
/// borrowed CREDIT amounts.
function totalBorrowedCredit() external view returns (uint256) {
return
CreditToken(credit).targetTotalSupply() -
SimplePSM(psm).redeemableCredit();
}

/// @notice set the minimum borrow amount
function setMinBorrow(
uint256 newValue
) external onlyCoreRole(CoreRoles.GOVERNOR) {
_minBorrow = newValue;
emit MinBorrowUpdate(block.timestamp, newValue);
}

/// @notice set the gauge weight tolerance
function setGaugeWeightTolerance(
uint256 newValue
) external onlyCoreRole(CoreRoles.GOVERNOR) {
gaugeWeightTolerance = newValue;
emit GaugeWeightToleranceUpdate(block.timestamp, newValue);
}

/// @notice set the profit sharing config.
function setProfitSharingConfig(
uint256 surplusBufferSplit,
uint256 creditSplit,
uint256 guildSplit,
uint256 otherSplit,
address otherRecipient
) external onlyCoreRole(CoreRoles.GOVERNOR) {
if (otherRecipient == address(0)) {
require(otherSplit == 0, “GuildToken: invalid config”);
} else {
require(otherSplit != 0, “GuildToken: invalid config”);
}
require(
surplusBufferSplit + otherSplit + guildSplit + creditSplit == 1e18,
“GuildToken: invalid config”
);

profitSharingConfig = ProfitSharingConfig({
surplusBufferSplit: uint32(surplusBufferSplit / 1e9),
guildSplit: uint32(guildSplit / 1e9),
otherSplit: uint32(otherSplit / 1e9),
otherRecipient: otherRecipient
});

emit ProfitSharingConfigUpdate(
block.timestamp,
surplusBufferSplit,
creditSplit,
guildSplit,
otherSplit,
otherRecipient
);
}

/// @notice get the profit sharing config.
function getProfitSharingConfig()
external
view
returns (
uint256 surplusBufferSplit,
uint256 creditSplit,
uint256 guildSplit,
uint256 otherSplit,
address otherRecipient
)
{
surplusBufferSplit =
uint256(profitSharingConfig.surplusBufferSplit) *
1e9;
guildSplit = uint256(profitSharingConfig.guildSplit) * 1e9;
otherSplit = uint256(profitSharingConfig.otherSplit) * 1e9;
creditSplit = 1e18 - surplusBufferSplit - guildSplit - otherSplit;
otherRecipient = profitSharingConfig.otherRecipient;
}

/// @notice donate to surplus buffer
function donateToSurplusBuffer(uint256 amount) external {
CreditToken(credit).transferFrom(msg.sender, address(this), amount);
uint256 newSurplusBuffer = surplusBuffer + amount;
surplusBuffer = newSurplusBuffer;
emit SurplusBufferUpdate(block.timestamp, newSurplusBuffer);
}

/// @notice donate to surplus buffer of a given term
function donateToTermSurplusBuffer(address term, uint256 amount) external {
CreditToken(credit).transferFrom(msg.sender, address(this), amount);
uint256 newSurplusBuffer = termSurplusBuffer[term] + amount;
termSurplusBuffer[term] = newSurplusBuffer;
emit TermSurplusBufferUpdate(block.timestamp, term, newSurplusBuffer);
}

/// @notice withdraw from surplus buffer
function withdrawFromSurplusBuffer(
address to,
uint256 amount
) external onlyCoreRole(CoreRoles.GUILD_SURPLUS_BUFFER_WITHDRAW) {
uint256 newSurplusBuffer = surplusBuffer - amount; // this would revert due to underflow if withdrawing > surplusBuffer
surplusBuffer = newSurplusBuffer;
CreditToken(credit).transfer(to, amount);
emit SurplusBufferUpdate(block.timestamp, newSurplusBuffer);
}

/// @notice withdraw from surplus buffer of a given term
function withdrawFromTermSurplusBuffer(
address term,
address to,
uint256 amount
) external onlyCoreRole(CoreRoles.GUILD_SURPLUS_BUFFER_WITHDRAW) {
uint256 newSurplusBuffer = termSurplusBuffer[term] - amount; // this would revert due to underflow if withdrawing > termSurplusBuffer
termSurplusBuffer[term] = newSurplusBuffer;
CreditToken(credit).transfer(to, amount);
emit TermSurplusBufferUpdate(block.timestamp, term, newSurplusBuffer);
}

/// @notice notify profit and loss in a given gauge
/// if amount is > 0, the same number of CREDIT tokens are expected to be transferred to this contract
/// before notifyPnL is called.
function notifyPnL(
address gauge,
int256 amount
) external onlyCoreRole(CoreRoles.GAUGE_PNL_NOTIFIER) {
uint256 _surplusBuffer = surplusBuffer;
uint256 _termSurplusBuffer = termSurplusBuffer[gauge];
address _credit = credit;

// handling loss
if (amount < 0) {
uint256 loss = uint256(-amount);

// save gauge loss
GuildToken(guild).notifyGaugeLoss(gauge);

// deplete the term surplus buffer, if any, and
// donate its content to the general surplus buffer
if (_termSurplusBuffer != 0) {
termSurplusBuffer[gauge] = 0;
emit TermSurplusBufferUpdate(block.timestamp, gauge, 0);
_surplusBuffer += _termSurplusBuffer;
}

if (loss < _surplusBuffer) {
// deplete the surplus buffer
surplusBuffer = _surplusBuffer - loss;
emit SurplusBufferUpdate(
block.timestamp,
_surplusBuffer - loss
);
CreditToken(_credit).burn(loss);
} else {
// empty the surplus buffer
loss -= _surplusBuffer;
surplusBuffer = 0;
CreditToken(_credit).burn(_surplusBuffer);
emit SurplusBufferUpdate(block.timestamp, 0);

// update the CREDIT multiplier
uint256 creditTotalSupply = CreditToken(_credit).totalSupply();
uint256 newCreditMultiplier = (creditMultiplier *
(creditTotalSupply - loss)) / creditTotalSupply;
creditMultiplier = newCreditMultiplier;
emit CreditMultiplierUpdate(
block.timestamp,
newCreditMultiplier
);
}
}
// handling profit
else if (amount > 0) {
ProfitSharingConfig
memory _profitSharingConfig = profitSharingConfig;

uint256 amountForSurplusBuffer = (uint256(amount) *
uint256(_profitSharingConfig.surplusBufferSplit)) / 1e9;

uint256 amountForGuild = (uint256(amount) *
uint256(_profitSharingConfig.guildSplit)) / 1e9;

uint256 amountForOther = (uint256(amount) *
uint256(_profitSharingConfig.otherSplit)) / 1e9;

uint256 amountForCredit = uint256(amount) -
amountForSurplusBuffer -
amountForGuild -
amountForOther;

// distribute to surplus buffer
if (amountForSurplusBuffer != 0) {
surplusBuffer = _surplusBuffer + amountForSurplusBuffer;
emit SurplusBufferUpdate(
block.timestamp,
_surplusBuffer + amountForSurplusBuffer
);
}

// distribute to other
if (amountForOther != 0) {
CreditToken(_credit).transfer(
_profitSharingConfig.otherRecipient,
amountForOther
);
}

// distribute to lenders
if (amountForCredit != 0) {
CreditToken(_credit).distribute(amountForCredit);
}

// distribute to the guild
if (amountForGuild != 0) {
// update the gauge profit index
// if the gauge has 0 weight, does not update the profit index, this is unnecessary
// because the profit index is used to reattribute profit to users voting for the gauge,
// and if the weigth is 0, there are no users voting for the gauge.
uint256 _gaugeWeight = uint256(
GuildToken(guild).getGaugeWeight(gauge)
);
if (_gaugeWeight != 0) {
uint256 _gaugeProfitIndex = gaugeProfitIndex[gauge];
if (_gaugeProfitIndex == 0) {
_gaugeProfitIndex = 1e18;
}
gaugeProfitIndex[gauge] =
_gaugeProfitIndex +
(amountForGuild * 1e18) /
_gaugeWeight;
}
}
}

emit GaugePnL(gauge, block.timestamp, amount);
}

/// @notice claim a user’s rewards for a given gauge.
/// @dev This should be called every time the user’s weight changes in the gauge.
function claimGaugeRewards(
address user,
address gauge
) public returns (uint256 creditEarned) {
uint256 _userGaugeWeight = uint256(
GuildToken(guild).getUserGaugeWeight(user, gauge)
);
if (_userGaugeWeight == 0) {
return 0;
}
uint256 _gaugeProfitIndex = gaugeProfitIndex[gauge];
uint256 _userGaugeProfitIndex = userGaugeProfitIndex[user][gauge];
if (_gaugeProfitIndex == 0) {
_gaugeProfitIndex = 1e18;
}
if (_userGaugeProfitIndex == 0) {
_userGaugeProfitIndex = 1e18;
}
uint256 deltaIndex = _gaugeProfitIndex - _userGaugeProfitIndex;
if (deltaIndex != 0) {
creditEarned = (_userGaugeWeight * deltaIndex) / 1e18;
userGaugeProfitIndex[user][gauge] = _gaugeProfitIndex;
}
if (creditEarned != 0) {
emit ClaimRewards(block.timestamp, user, gauge, creditEarned);
CreditToken(credit).transfer(user, creditEarned);
}
}

/// @notice claim a user’s rewards across all their active gauges.
function claimRewards(
address user
) external returns (uint256 creditEarned) {
address[] memory gauges = GuildToken(guild).userGauges(user);
for (uint256 i = 0; i < gauges.length; ) {
creditEarned += claimGaugeRewards(user, gauges[i]);
unchecked {
++i;
}
}
}

/// @notice read & return pending undistributed rewards for a given user
function getPendingRewards(
address user
)
external
view
returns (
address[] memory gauges,
uint256[] memory creditEarned,
uint256 totalCreditEarned
)
{
address _guild = guild;
gauges = GuildToken(_guild).userGauges(user);
creditEarned = new uint256;

for (uint256 i = 0; i < gauges.length; ) {
address gauge = gauges[i];
uint256 _gaugeProfitIndex = gaugeProfitIndex[gauge];
uint256 _userGaugeProfitIndex = userGaugeProfitIndex[user][gauge];

if (_gaugeProfitIndex == 0) {
_gaugeProfitIndex = 1e18;
}
if (_userGaugeProfitIndex == 0) {
_userGaugeProfitIndex = 1e18;
}
uint256 deltaIndex = _gaugeProfitIndex - _userGaugeProfitIndex;
if (deltaIndex != 0) {
uint256 _userGaugeWeight = uint256(
GuildToken(_guild).getUserGaugeWeight(user, gauge)
);
creditEarned[i] = (_userGaugeWeight * deltaIndex) / 1e18;
totalCreditEarned += creditEarned[i];
}

unchecked {
++i;
}
}
}
}