ERC20Gauges.sol

Created Diff never expires
148 removals
581 lines
249 additions
644 lines
// SPDX-License-Identifier: MIT
// SPDX-License-Identifier: GPL-3.0-or-later

pragma solidity =0.8.13;
pragma solidity ^0.8.0;


import "solmate/auth/Auth.sol";
import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "solmate/tokens/ERC20.sol";
import {EnumerableSet} from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol";
import "solmate/utils/SafeCastLib.sol";
import "../../lib/EnumerableSet.sol";
import "../interfaces/Errors.sol";


/**
/**
@title An ERC20 with an embedded "Gauge" style vote with liquid weights
@title An ERC20 with an embedded "Gauge" style vote with liquid weights
@author Tribe DAO
@author joeysantoro, eswak
@notice This contract is meant to be used to support gauge style votes with weights associated with resource allocation.
@notice This contract is meant to be used to support gauge style votes with weights associated with resource allocation.
Holders can allocate weight in any proportion to supported gauges.
Holders can allocate weight in any proportion to supported gauges.
A "gauge" is represented by an address which would receive the resources periodically or continuously.
A "gauge" is represented by an address which would receive the resources periodically or continuously.
For example, gauges can be used to direct token emissions, similar to Curve or Tokemak.
For example, gauges can be used to direct token emissions, similar to Curve or Tokemak.
Alternatively, gauges can be used to direct another quantity such as relative access to a line of credit.
Alternatively, gauges can be used to direct another quantity such as relative access to a line of credit.
The contract's Authority <https://github.com/Rari-Capital/solmate/blob/main/src/auth/Auth.sol> manages the gauge set and cap.
This contract is abstract, and a parent shall implement public setter with adequate access control to manage
"Live" gauges are in the set.
the gauge set and caps.
Users can only add weight to live gauges but can remove weight from live or deprecated gauges.
"Live" gauges are in the set `_gauges`.
Gauges can be deprecated and reinstated, and will maintain any non-removed weight from before.
Users can only add weight to live gauges but can remove weight from live or deprecated gauges.
@dev SECURITY NOTES: `maxGauges` is a critical variable to protect against gas DOS attacks upon token transfer.
Gauges can be deprecated and reinstated, and will maintain any non-removed weight from before.
This must be low enough to allow complicated transactions to fit in a block.
@dev SECURITY NOTES: `maxGauges` is a critical variable to protect against gas DOS attacks upon token transfer.
This must be low enough to allow complicated transactions to fit in a block.
Weight state is preserved on the gauge and user level even when a gauge is removed, in case it is re-added.
Weight state is preserved on the gauge and user level even when a gauge is removed, in case it is re-added.
This maintains state efficiently, and global accounting is managed only on the `_totalWeight`
This maintains state efficiently, and global accounting is managed only on the `_totalWeight`
@dev This contract was originally published as part of TribeDAO's flywheel-v2 repo, please see:
https://github.com/fei-protocol/flywheel-v2/blob/main/src/token/ERC20Gauges.sol
The original version was included in 2 audits :
- https://code4rena.com/reports/2022-04-xtribe/
- https://consensys.net/diligence/audits/2022/04/tribe-dao-flywheel-v2-xtribe-xerc4626/
Volt Protocol made the following changes to the original flywheel-v2 version :
- Does not inherit Solmate's Auth (all requiresAuth functions are now internal, see below)
-> This contract is abstract, and permissioned public functions can be added in parent.
-> permissioned public functions to add in parent:
- function addGauge(address) external returns (uint112)
- function removeGauge(address) external
- function setMaxGauges(uint256) external
- function setCanExceedMaxGauges(address, bool) external
- Remove public addGauge(address) requiresAuth method
- Remove public removeGauge(address) requiresAuth method
- Remove public replaceGauge(address, address) requiresAuth method
- Remove public setMaxGauges(uint256) requiresAuth method
... Add internal _setMaxGauges(uint256) method
- Remove public setContractExceedMaxGauges(address, bool) requiresAuth method
... Add internal _setCanExceedMaxGauges(address, bool) method
... Remove check of "target address has nonzero code size"
... Rename to remove "contract" from name because we don't check if target is a contract
- Consistency: make incrementGauges return a uint112 instead of uint256
- Import OpenZeppelin ERC20 & EnumerableSet instead of Solmate's
- Update error management style (use require + messages instead of Solidity errors)
- Remove SafeCast (it was only used in one place, to convert block.timestamp)
- Implement C4 audit fixes for [M-03], [M-04], [M-07], [G-02], and [G-04].
*/
*/
abstract contract ERC20Gauges is ERC20, Auth {
abstract contract ERC20Gauges is ERC20 {
using EnumerableSet for EnumerableSet.AddressSet;
using EnumerableSet for EnumerableSet.AddressSet;
using SafeCastLib for *;


constructor(uint32 _gaugeCycleLength, uint32 _incrementFreezeWindow) {
constructor(uint32 _gaugeCycleLength, uint32 _incrementFreezeWindow) {
if (_incrementFreezeWindow >= _gaugeCycleLength) revert IncrementFreezeError();
require(
_incrementFreezeWindow < _gaugeCycleLength,
"ERC20Gauges: invalid increment freeze"
);
gaugeCycleLength = _gaugeCycleLength;
gaugeCycleLength = _gaugeCycleLength;
incrementFreezeWindow = _incrementFreezeWindow;
incrementFreezeWindow = _incrementFreezeWindow;
}
}


/*///////////////////////////////////////////////////////////////
/*///////////////////////////////////////////////////////////////
GAUGE STATE
GAUGE STATE
//////////////////////////////////////////////////////////////*/
//////////////////////////////////////////////////////////////*/


/// @notice the length of a gauge cycle
/// @notice the length of a gauge cycle
uint32 public immutable gaugeCycleLength;
uint32 public immutable gaugeCycleLength;


/// @notice the period at the end of a cycle where votes cannot increment
/// @notice the period at the end of a cycle where votes cannot increment
uint32 public immutable incrementFreezeWindow;
uint32 public immutable incrementFreezeWindow;


struct Weight {
struct Weight {
uint112 storedWeight;
uint112 storedWeight;
uint112 currentWeight;
uint112 currentWeight;
uint32 currentCycle;
uint32 currentCycle;
}
}


/// @notice a mapping from users to gauges to a user's allocated weight to that gauge
/// @notice a mapping from users to gauges to a user's allocated weight to that gauge
mapping(address => mapping(address => uint112)) public getUserGaugeWeight;
mapping(address => mapping(address => uint112)) public getUserGaugeWeight;


/// @notice a mapping from a user to their total allocated weight across all gauges
/// @notice a mapping from a user to their total allocated weight across all gauges
/// @dev NOTE this may contain weights for deprecated gauges
/// @dev NOTE this may contain weights for deprecated gauges
mapping(address => uint112) public getUserWeight;
mapping(address => uint112) public getUserWeight;


/// @notice a mapping from a gauge to the total weight allocated to it
/// @notice a mapping from a gauge to the total weight allocated to it
/// @dev NOTE this may contain weights for deprecated gauges
/// @dev NOTE this may contain weights for deprecated gauges
mapping(address => Weight) internal _getGaugeWeight;
mapping(address => Weight) internal _getGaugeWeight;


/// @notice the total global allocated weight ONLY of live gauges
/// @notice the total global allocated weight ONLY of live gauges
Weight internal _totalWeight;
Weight internal _totalWeight;


mapping(address => EnumerableSet.AddressSet) internal _userGauges;
mapping(address => EnumerableSet.AddressSet) internal _userGauges;


EnumerableSet.AddressSet internal _gauges;
EnumerableSet.AddressSet internal _gauges;


// Store deprecated gauges in case a user needs to free dead weight
// Store deprecated gauges in case a user needs to free dead weight
EnumerableSet.AddressSet internal _deprecatedGauges;
EnumerableSet.AddressSet internal _deprecatedGauges;


/*///////////////////////////////////////////////////////////////
/*///////////////////////////////////////////////////////////////
VIEW HELPERS
VIEW HELPERS
//////////////////////////////////////////////////////////////*/
//////////////////////////////////////////////////////////////*/


/// @notice return the end of the current cycle. This is the next unix timestamp which evenly divides `gaugeCycleLength`
/// @notice return the end of the current cycle. This is the next unix timestamp which evenly divides `gaugeCycleLength`
function getGaugeCycleEnd() public view returns (uint32) {
function getGaugeCycleEnd() public view returns (uint32) {
return _getGaugeCycleEnd();
return _getGaugeCycleEnd();
}
}


/// @notice see `getGaugeCycleEnd()`
/// @notice see `getGaugeCycleEnd()`
function _getGaugeCycleEnd() internal view returns (uint32) {
function _getGaugeCycleEnd() internal view returns (uint32) {
uint32 nowPlusOneCycle = block.timestamp.safeCastTo32() + gaugeCycleLength;
// @dev timestamp of type(uint32).max is Feb 07 2106
uint32 nowPlusOneCycle = uint32(block.timestamp) + gaugeCycleLength;
unchecked {
unchecked {
return (nowPlusOneCycle / gaugeCycleLength) * gaugeCycleLength; // cannot divide by zero and always <= nowPlusOneCycle so no overflow
return (nowPlusOneCycle / gaugeCycleLength) * gaugeCycleLength; // cannot divide by zero and always <= nowPlusOneCycle so no overflow
}
}
}
}


/// @notice returns the current weight of a given gauge
/// @notice returns the current weight of a given gauge
function getGaugeWeight(address gauge) public view returns (uint112) {
function getGaugeWeight(address gauge) public view returns (uint112) {
return _getGaugeWeight[gauge].currentWeight;
return _getGaugeWeight[gauge].currentWeight;
}
}


/// @notice returns the stored weight of a given gauge. This is the snapshotted weight as-of the end of the last cycle.
/// @notice returns the stored weight of a given gauge. This is the snapshotted weight as-of the end of the last cycle.
function getStoredGaugeWeight(address gauge) public view returns (uint112) {
function getStoredGaugeWeight(address gauge) public view returns (uint112) {
if (_deprecatedGauges.contains(gauge)) return 0;
if (_deprecatedGauges.contains(gauge)) return 0;
return _getStoredWeight(_getGaugeWeight[gauge], _getGaugeCycleEnd());
return _getStoredWeight(_getGaugeWeight[gauge], _getGaugeCycleEnd());
}
}


/// @notice see `getStoredGaugeWeight()`
/// @notice see `getStoredGaugeWeight()`
function _getStoredWeight(Weight storage gaugeWeight, uint32 currentCycle) internal view returns (uint112) {
function _getStoredWeight(
return gaugeWeight.currentCycle < currentCycle ? gaugeWeight.currentWeight : gaugeWeight.storedWeight;
Weight storage gaugeWeight,
uint32 currentCycle
) internal view returns (uint112) {
return
gaugeWeight.currentCycle < currentCycle
? gaugeWeight.currentWeight
: gaugeWeight.storedWeight;
}
}


/// @notice returns the current total allocated weight
/// @notice returns the current total allocated weight
function totalWeight() external view returns (uint112) {
function totalWeight() external view returns (uint112) {
return _totalWeight.currentWeight;
return _totalWeight.currentWeight;
}
}


/// @notice returns the stored total allocated weight
/// @notice returns the stored total allocated weight
function storedTotalWeight() external view returns (uint112) {
function storedTotalWeight() external view returns (uint112) {
return _getStoredWeight(_totalWeight, _getGaugeCycleEnd());
return _getStoredWeight(_totalWeight, _getGaugeCycleEnd());
}
}


/// @notice returns the set of live gauges
/// @notice returns the set of live gauges
function gauges() external view returns (address[] memory) {
function gauges() external view returns (address[] memory) {
return _gauges.values();
return _gauges.values();
}
}


/**
/**
@notice returns a paginated subset of live gauges
@notice returns a paginated subset of live gauges
@param offset the index of the first gauge element to read
@param offset the index of the first gauge element to read
@param num the number of gauges to return
@param num the number of gauges to return
*/
*/
function gauges(uint256 offset, uint256 num) external view returns (address[] memory values) {
function gauges(
uint256 offset,
uint256 num
) external view returns (address[] memory values) {
values = new address[](num);
values = new address[](num);
for (uint256 i = 0; i < num; ) {
for (uint256 i = 0; i < num; ) {
unchecked {
unchecked {
values[i] = _gauges.at(offset + i); // will revert if out of bounds
values[i] = _gauges.at(offset + i); // will revert if out of bounds
i++;
++i;
}
}
}
}
}
}


/// @notice returns true if `gauge` is not in deprecated gauges
/// @notice returns true if `gauge` is not in deprecated gauges
function isGauge(address gauge) external view returns (bool) {
function isGauge(address gauge) public view returns (bool) {
return _gauges.contains(gauge) && !_deprecatedGauges.contains(gauge);
return _gauges.contains(gauge) && !_deprecatedGauges.contains(gauge);
}
}


/// @notice returns the number of live gauges
/// @notice returns the number of live gauges
function numGauges() external view returns (uint256) {
function numGauges() external view returns (uint256) {
return _gauges.length();
return _gauges.length();
}
}


/// @notice returns the set of previously live but now deprecated gauges
/// @notice returns the set of previously live but now deprecated gauges
function deprecatedGauges() external view returns (address[] memory) {
function deprecatedGauges() external view returns (address[] memory) {
return _deprecatedGauges.values();
return _deprecatedGauges.values();
}
}


/// @notice returns the number of live gauges
/// @notice returns the number of live gauges
function numDeprecatedGauges() external view returns (uint256) {
function numDeprecatedGauges() external view returns (uint256) {
return _deprecatedGauges.length();
return _deprecatedGauges.length();
}
}


/// @notice returns the set of gauges the user has allocated to, may be live or deprecated.
/// @notice returns the set of gauges the user has allocated to, may be live or deprecated.
function userGauges(address user) external view returns (address[] memory) {
function userGauges(address user) external view returns (address[] memory) {
return _userGauges[user].values();
return _userGauges[user].values();
}
}


/// @notice returns true if `gauge` is in user gauges
/// @notice returns true if `gauge` is in user gauges
function isUserGauge(address user, address gauge) external view returns (bool) {
function isUserGauge(
address user,
address gauge
) external view returns (bool) {
return _userGauges[user].contains(gauge);
return _userGauges[user].contains(gauge);
}
}


/**
/**
@notice returns a paginated subset of gauges the user has allocated to, may be live or deprecated.
@notice returns a paginated subset of gauges the user has allocated to, may be live or deprecated.
@param user the user to return gauges from.
@param user the user to return gauges from.
@param offset the index of the first gauge element to read.
@param offset the index of the first gauge element to read.
@param num the number of gauges to return.
@param num the number of gauges to return.
*/
*/
function userGauges(
function userGauges(
address user,
address user,
uint256 offset,
uint256 offset,
uint256 num
uint256 num
) external view returns (address[] memory values) {
) external view returns (address[] memory values) {
values = new address[](num);
values = new address[](num);
for (uint256 i = 0; i < num; ) {
for (uint256 i = 0; i < num; ) {
unchecked {
unchecked {
values[i] = _userGauges[user].at(offset + i); // will revert if out of bounds
values[i] = _userGauges[user].at(offset + i); // will revert if out of bounds
i++;
++i;
}
}
}
}
}
}


/// @notice returns the number of user gauges
/// @notice returns the number of user gauges
function numUserGauges(address user) external view returns (uint256) {
function numUserGauges(address user) external view returns (uint256) {
return _userGauges[user].length();
return _userGauges[user].length();
}
}


/// @notice helper function exposing the amount of weight available to allocate for a user
/// @notice helper function exposing the amount of weight available to allocate for a user
function userUnusedWeight(address user) external view returns (uint256) {
function userUnusedWeight(address user) external view returns (uint256) {
return balanceOf[user] - getUserWeight[user];
return balanceOf(user) - getUserWeight[user];
}
}


/**
/**
@notice helper function for calculating the proportion of a `quantity` allocated to a gauge
@notice helper function for calculating the proportion of a `quantity` allocated to a gauge
@param gauge the gauge to calculate allocation of
@param gauge the gauge to calculate allocation of
@param quantity a representation of a resource to be shared among all gauges
@param quantity a representation of a resource to be shared among all gauges
@return the proportion of `quantity` allocated to `gauge`. Returns 0 if gauge is not live, even if it has weight.
@return the proportion of `quantity` allocated to `gauge`. Returns 0 if gauge is not live, even if it has weight.
*/
*/
function calculateGaugeAllocation(address gauge, uint256 quantity) external view returns (uint256) {
function calculateGaugeAllocation(
address gauge,
uint256 quantity
) external view returns (uint256) {
if (_deprecatedGauges.contains(gauge)) return 0;
if (_deprecatedGauges.contains(gauge)) return 0;
uint32 currentCycle = _getGaugeCycleEnd();
uint32 currentCycle = _getGaugeCycleEnd();


uint112 total = _getStoredWeight(_totalWeight, currentCycle);
uint112 total = _getStoredWeight(_totalWeight, currentCycle);
uint112 weight = _getStoredWeight(_getGaugeWeight[gauge], currentCycle);
uint112 weight = _getStoredWeight(_getGaugeWeight[gauge], currentCycle);
return (quantity * weight) / total;
return (quantity * weight) / total;
}
}


/*///////////////////////////////////////////////////////////////
/*///////////////////////////////////////////////////////////////
USER GAUGE OPERATIONS
USER GAUGE OPERATIONS
//////////////////////////////////////////////////////////////*/
//////////////////////////////////////////////////////////////*/


/// @notice thrown when trying to increment/decrement a mismatched number of gauges and weights.
error SizeMismatchError();

/// @notice thrown when trying to increment over the max allowed gauges.
error MaxGaugeError();

/// @notice thrown when incrementing over a users free weight.
error OverWeightError();

/// @notice thrown when incremending during the freeze window.
error IncrementFreezeError();

/// @notice emitted when incrementing a gauge
/// @notice emitted when incrementing a gauge
event IncrementGaugeWeight(address indexed user, address indexed gauge, uint256 weight, uint32 cycleEnd);
event IncrementGaugeWeight(
address indexed user,
address indexed gauge,
uint256 weight,
uint32 cycleEnd
);


/// @notice emitted when decrementing a gauge
/// @notice emitted when decrementing a gauge
event DecrementGaugeWeight(address indexed user, address indexed gauge, uint256 weight, uint32 cycleEnd);
event DecrementGaugeWeight(
address indexed user,
address indexed gauge,
uint256 weight,
uint32 cycleEnd
);


/**
/**
@notice increment a gauge with some weight for the caller
@notice increment a gauge with some weight for the caller
@param gauge the gauge to increment
@param gauge the gauge to increment
@param weight the amount of weight to increment on gauge
@param weight the amount of weight to increment on gauge
@return newUserWeight the new user weight
@return newUserWeight the new user weight
*/
*/
function incrementGauge(address gauge, uint112 weight) external returns (uint112 newUserWeight) {
function incrementGauge(
address gauge,
uint112 weight
) external returns (uint112 newUserWeight) {
require(isGauge(gauge), "ERC20Gauges: invalid gauge");
uint32 currentCycle = _getGaugeCycleEnd();
uint32 currentCycle = _getGaugeCycleEnd();
_incrementGaugeWeight(msg.sender, gauge, weight, currentCycle);
_incrementGaugeWeight(msg.sender, gauge, weight, currentCycle);
return _incrementUserAndGlobalWeights(msg.sender, weight, currentCycle);
return _incrementUserAndGlobalWeights(msg.sender, weight, currentCycle);
}
}


/// @dev this function does not check if the gauge exists, this is performed
/// in the calling function.
function _incrementGaugeWeight(
function _incrementGaugeWeight(
address user,
address user,
address gauge,
address gauge,
uint112 weight,
uint112 weight,
uint32 cycle
uint32 cycle
) internal {
) internal {
if (_deprecatedGauges.contains(gauge)) revert InvalidGaugeError();
unchecked {
unchecked {
if (cycle - block.timestamp <= incrementFreezeWindow) revert IncrementFreezeError();
require(
cycle - block.timestamp > incrementFreezeWindow,
"ERC20Gauges: freeze period"
);
}
}


bool added = _userGauges[user].add(gauge); // idempotent add
bool added = _userGauges[user].add(gauge); // idempotent add
if (added && _userGauges[user].length() > maxGauges && !canContractExceedMaxGauges[user])
if (added && _userGauges[user].length() > maxGauges) {
revert MaxGaugeError();
require(canExceedMaxGauges[user], "ERC20Gauges: exceed max gauges");
}


getUserGaugeWeight[user][gauge] += weight;
getUserGaugeWeight[user][gauge] += weight;


_writeGaugeWeight(_getGaugeWeight[gauge], _add, weight, cycle);
_writeGaugeWeight(_getGaugeWeight[gauge], _add, weight, cycle);


emit IncrementGaugeWeight(user, gauge, weight, cycle);
emit IncrementGaugeWeight(user, gauge, weight, cycle);
}
}


function _incrementUserAndGlobalWeights(
function _incrementUserAndGlobalWeights(
address user,
address user,
uint112 weight,
uint112 weight,
uint32 cycle
uint32 cycle
) internal returns (uint112 newUserWeight) {
) internal returns (uint112 newUserWeight) {
newUserWeight = getUserWeight[user] + weight;
newUserWeight = getUserWeight[user] + weight;
// Ensure under weight
// Ensure under weight
if (newUserWeight > balanceOf[user]) revert OverWeightError();
require(newUserWeight <= balanceOf(user), "ERC20Gauges: overweight");


// Update gauge state
// Update gauge state
getUserWeight[user] = newUserWeight;
getUserWeight[user] = newUserWeight;


_writeGaugeWeight(_totalWeight, _add, weight, cycle);
_writeGaugeWeight(_totalWeight, _add, weight, cycle);
}
}


/**
/**
@notice increment a list of gauges with some weights for the caller
@notice increment a list of gauges with some weights for the caller
@param gaugeList the gauges to increment
@param gaugeList the gauges to increment
@param weights the weights to increment by
@param weights the weights to increment by
@return newUserWeight the new user weight
@return newUserWeight the new user weight
*/
*/
function incrementGauges(address[] calldata gaugeList, uint112[] calldata weights)
function incrementGauges(
external
address[] calldata gaugeList,
returns (uint256 newUserWeight)
uint112[] calldata weights
{
) external returns (uint112 newUserWeight) {
uint256 size = gaugeList.length;
uint256 size = gaugeList.length;
if (weights.length != size) revert SizeMismatchError();
require(weights.length == size, "ERC20Gauges: size mismatch");


// store total in summary for batch update on user/global state
// store total in summary for batch update on user/global state
uint112 weightsSum;
uint112 weightsSum;


uint32 currentCycle = _getGaugeCycleEnd();
uint32 currentCycle = _getGaugeCycleEnd();


// Update gauge specific state
// Update gauge specific state
for (uint256 i = 0; i < size; ) {
for (uint256 i = 0; i < size; ) {
address gauge = gaugeList[i];
address gauge = gaugeList[i];
uint112 weight = weights[i];
uint112 weight = weights[i];
weightsSum += weight;
weightsSum += weight;


require(isGauge(gauge), "ERC20Gauges: invalid gauge");

_incrementGaugeWeight(msg.sender, gauge, weight, currentCycle);
_incrementGaugeWeight(msg.sender, gauge, weight, currentCycle);
unchecked {
unchecked {
i++;
++i;
}
}
}
}
return _incrementUserAndGlobalWeights(msg.sender, weightsSum, currentCycle);
return
_incrementUserAndGlobalWeights(
msg.sender,
weightsSum,
currentCycle
);
}
}


/**
/**
@notice decrement a gauge with some weight for the caller
@notice decrement a gauge with some weight for the caller
@param gauge the gauge to decrement
@param gauge the gauge to decrement
@param weight the amount of weight to decrement on gauge
@param weight the amount of weight to decrement on gauge
@return newUserWeight the new user weight
@return newUserWeight the new user weight
*/
*/
function decrementGauge(address gauge, uint112 weight) external returns (uint112 newUserWeight) {
function decrementGauge(
address gauge,
uint112 weight
) external returns (uint112 newUserWeight) {
uint32 currentCycle = _getGaugeCycleEnd();
uint32 currentCycle = _getGaugeCycleEnd();


// All operations will revert on underflow, protecting against bad inputs
// All operations will revert on underflow, protecting against bad inputs
_decrementGaugeWeight(msg.sender, gauge, weight, currentCycle);
_decrementGaugeWeight(msg.sender, gauge, weight, currentCycle);
return _decrementUserAndGlobalWeights(msg.sender, weight, currentCycle);
return _decrementUserAndGlobalWeights(msg.sender, weight, currentCycle);
}
}


function _decrementGaugeWeight(
function _decrementGaugeWeight(
address user,
address user,
address gauge,
address gauge,
uint112 weight,
uint112 weight,
uint32 cycle
uint32 cycle
) internal {
) internal {
uint112 oldWeight = getUserGaugeWeight[user][gauge];
uint112 oldWeight = getUserGaugeWeight[user][gauge];


getUserGaugeWeight[user][gauge] = oldWeight - weight;
getUserGaugeWeight[user][gauge] = oldWeight - weight;
if (oldWeight == weight) {
if (oldWeight == weight) {
// If removing all weight, remove gauge from user list.
// If removing all weight, remove gauge from user list.
require(_userGauges[user].remove(gauge));
require(_userGauges[user].remove(gauge));
}
}


_writeGaugeWeight(_getGaugeWeight[gauge], _subtract, weight, cycle);
_writeGaugeWeight(_getGaugeWeight[gauge], _subtract, weight, cycle);


emit DecrementGaugeWeight(user, gauge, weight, cycle);
emit DecrementGaugeWeight(user, gauge, weight, cycle);
}
}


function _decrementUserAndGlobalWeights(
function _decrementUserAndGlobalWeights(
address user,
address user,
uint112 weight,
uint112 weight,
uint32 cycle
uint32 cycle
) internal returns (uint112 newUserWeight) {
) internal returns (uint112 newUserWeight) {
newUserWeight = getUserWeight[user] - weight;
newUserWeight = getUserWeight[user] - weight;


getUserWeight[user] = newUserWeight;
getUserWeight[user] = newUserWeight;
_writeGaugeWeight(_totalWeight, _subtract, weight, cycle);
_writeGaugeWeight(_totalWeight, _subtract, weight, cycle);
}
}


/**
/**
@notice decrement a list of gauges with some weights for the caller
@notice decrement a list of gauges with some weights for the caller
@param gaugeList the gauges to decrement
@param gaugeList the gauges to decrement
@param weights the list of weights to decrement on the gauges
@param weights the list of weights to decrement on the gauges
@return newUserWeight the new user weight
@return newUserWeight the new user weight
*/
*/
function decrementGauges(address[] calldata gaugeList, uint112[] calldata weights)
function decrementGauges(
external
address[] calldata gaugeList,
returns (uint112 newUserWeight)
uint112[] calldata weights
{
) external returns (uint112 newUserWeight) {
uint256 size = gaugeList.length;
uint256 size = gaugeList.length;
if (weights.length != size) revert SizeMismatchError();
require(weights.length == size, "ERC20Gauges: size mismatch");


// store total in summary for batch update on user/global state
// store total in summary for batch update on user/global state
uint112 weightsSum;
uint112 weightsSum;


uint32 currentCycle = _getGaugeCycleEnd();
uint32 currentCycle = _getGaugeCycleEnd();


// Update gauge specific state
// Update gauge specific state
// All operations will revert on underflow, protecting against bad inputs
// All operations will revert on underflow, protecting against bad inputs
for (uint256 i = 0; i < size; ) {
for (uint256 i = 0; i < size; ) {
address gauge = gaugeList[i];
address gauge = gaugeList[i];
uint112 weight = weights[i];
uint112 weight = weights[i];
weightsSum += weight;
weightsSum += weight;


_decrementGaugeWeight(msg.sender, gauge, weight, currentCycle);
_decrementGaugeWeight(msg.sender, gauge, weight, currentCycle);
unchecked {
unchecked {
i++;
++i;
}
}
}
}
return _decrementUserAndGlobalWeights(msg.sender, weightsSum, currentCycle);
return
_decrementUserAndGlobalWeights(
msg.sender,
weightsSum,
currentCycle
);
}
}


/**
/**
@dev this function is the key to the entire contract.
@dev this function is the key to the entire contract.
The storage weight it operates on is either a global or gauge-specific weight.
The storage weight it operates on is either a global or gauge-specific weight.
The operation applied is either addition for incrementing gauges or subtraction for decrementing a gauge.
The operation applied is either addition for incrementing gauges or subtraction for decrementing a gauge.
*/
*/
function _writeGaugeWeight(
function _writeGaugeWeight(
Weight storage weight,
Weight storage weight,
function(uint112, uint112) view returns (uint112) op,
function(uint112, uint112) view returns (uint112) op,
uint112 delta,
uint112 delta,
uint32 cycle
uint32 cycle
) private {
) private {
uint112 currentWeight = weight.currentWeight;
uint112 currentWeight = weight.currentWeight;
// If the last cycle of the weight is before the current cycle, use the current weight as the stored.
// If the last cycle of the weight is before the current cycle, use the current weight as the stored.
uint112 stored = weight.currentCycle < cycle ? currentWeight : weight.storedWeight;
uint112 stored = weight.currentCycle < cycle
? currentWeight
: weight.storedWeight;
uint112 newWeight = op(currentWeight, delta);
uint112 newWeight = op(currentWeight, delta);


weight.storedWeight = stored;
weight.storedWeight = stored;
weight.currentWeight = newWeight;
weight.currentWeight = newWeight;
weight.currentCycle = cycle;
weight.currentCycle = cycle;
}
}


function _add(uint112 a, uint112 b) private pure returns (uint112) {
function _add(uint112 a, uint112 b) private pure returns (uint112) {
return a + b;
return a + b;
}
}


function _subtract(uint112 a, uint112 b) private pure returns (uint112) {
function _subtract(uint112 a, uint112 b) private pure returns (uint112) {
return a - b;
return a - b;
}
}


/*///////////////////////////////////////////////////////////////
/*///////////////////////////////////////////////////////////////
ADMIN GAUGE OPERATIONS
ADMIN GAUGE OPERATIONS
//////////////////////////////////////////////////////////////*/
//////////////////////////////////////////////////////////////*/


/// @notice thrown when trying to increment or remove a non-live gauge, or add a live gauge.
error InvalidGaugeError();

/// @notice emitted when adding a new gauge to the live set.
/// @notice emitted when adding a new gauge to the live set.
event AddGauge(address indexed gauge);
event AddGauge(address indexed gauge);


/// @notice emitted when removing a gauge from the live set.
/// @notice emitted when removing a gauge from the live set.
event RemoveGauge(address indexed gauge);
event RemoveGauge(address indexed gauge);


/// @notice emitted when updating the max number of gauges a user can delegate to.
/// @notice emitted when updating the max number of gauges a user can delegate to.
event MaxGaugesUpdate(uint256 oldMaxGauges, uint256 newMaxGauges);
event MaxGaugesUpdate(uint256 oldMaxGauges, uint256 newMaxGauges);


/// @notice emitted when changing a contract's approval to go over the max gauges.
/// @notice emitted when changing a contract's approval to go over the max gauges.
event CanContractExceedMaxGaugesUpdate(address indexed account, bool canContractExceedMaxGauges);
event CanExceedMaxGaugesUpdate(
address indexed account,
bool canExceedMaxGauges
);


/// @notice the default maximum amount of gauges a user can allocate to.
/// @notice the default maximum amount of gauges a user can allocate to.
/// @dev if this number is ever lowered, or a contract has an override, then existing addresses MAY have more gauges allocated to. Use `numUserGauges` to check this.
/// @dev if this number is ever lowered, or a contract has an override, then existing addresses MAY have more gauges allocated to. Use `numUserGauges` to check this.
uint256 public maxGauges;
uint256 public maxGauges;


/// @notice an approve list for contracts to go above the max gauge limit.
/// @notice an approve list for contracts to go above the max gauge limit.
mapping(address => bool) public canContractExceedMaxGauges;
mapping(address => bool) public canExceedMaxGauges;

/// @notice add a new gauge. Requires auth by `authority`.
function addGauge(address gauge) external requiresAuth returns (uint112) {
return _addGauge(gauge);
}


function _addGauge(address gauge) internal returns (uint112 weight) {
function _addGauge(address gauge) internal returns (uint112 weight) {
bool newAdd = _gauges.add(gauge);
bool newAdd = _gauges.add(gauge);
bool previouslyDeprecated = _deprecatedGauges.remove(gauge);
bool previouslyDeprecated = _deprecatedGauges.remove(gauge);
// add and fail loud if zero address or already present and not deprecated
// add and fail loud if zero address or already present and not deprecated
if (gauge == address(0) || !(newAdd || previouslyDeprecated)) revert InvalidGaugeError();
require(
gauge != address(0) && (newAdd || previouslyDeprecated),
"ERC20Gauges: invalid gauge"
);


uint32 currentCycle = _getGaugeCycleEnd();
uint32 currentCycle = _getGaugeCycleEnd();


// Check if some previous weight exists and re-add to total. Gauge and user weights are preserved.
// Check if some previous weight exists and re-add to total. Gauge and user weights are preserved.
weight = _getGaugeWeight[gauge].currentWeight;
weight = _getGaugeWeight[gauge].currentWeight;
if (weight > 0) {
if (weight != 0) {
_writeGaugeWeight(_totalWeight, _add, weight, currentCycle);
_writeGaugeWeight(_totalWeight, _add, weight, currentCycle);
}
}


emit AddGauge(gauge);
emit AddGauge(gauge);
}
}


/// @notice remove a new gauge. Requires auth by `authority`.
function removeGauge(address gauge) external requiresAuth {
_removeGauge(gauge);
}

function _removeGauge(address gauge) internal {
function _removeGauge(address gauge) internal {
// add to deprecated and fail loud if not present
// add to deprecated and fail loud if not present
if (!_deprecatedGauges.add(gauge)) revert InvalidGaugeError();
require(_deprecatedGauges.add(gauge), "ERC20Gauges: invalid gauge");


uint32 currentCycle = _getGaugeCycleEnd();
uint32 currentCycle = _getGaugeCycleEnd();


// Remove weight from total but keep the gauge and user weights in storage in case gauge is re-added.
// Remove weight from total but keep the gauge and user weights in storage in case gauge is re-added.
uint112 weight = _getGaugeWeight[gauge].currentWeight;
uint112 weight = _getGaugeWeight[gauge].currentWeight;
if (weight > 0) {
if (weight != 0) {
_writeGaugeWeight(_totalWeight, _subtract, weight, currentCycle);
_writeGaugeWeight(_totalWeight, _subtract, weight, currentCycle);
}
}


emit RemoveGauge(gauge);
emit RemoveGauge(gauge);
}
}


/// @notice replace a gauge. Requires auth by `authority`.
function replaceGauge(address oldGauge, address newGauge) external requiresAuth {
_removeGauge(oldGauge);
_addGauge(newGauge);
}

/// @notice set the new max gauges. Requires auth by `authority`.
/// @notice set the new max gauges. Requires auth by `authority`.
/// @dev if this is set to a lower number than the current max, users MAY have more gauges active than the max. Use `numUserGauges` to check this.
/// @dev if this is set to a lower number than the current max, users MAY have more gauges active than the max. Use `numUserGauges` to check this.
function setMaxGauges(uint256 newMax) external requiresAuth {
function _setMaxGauges(uint256 newMax) internal {
uint256 oldMax = maxGauges;
uint256 oldMax = maxGauges;
maxGauges = newMax;
maxGauges = newMax;


emit MaxGaugesUpdate(oldMax, newMax);
emit MaxGaugesUpdate(oldMax, newMax);
}
}


/// @notice set the canContractExceedMaxGauges flag for an account.
/// @notice set the canExceedMaxGauges flag for an account.
function setContractExceedMaxGauges(address account, bool canExceedMax) external requiresAuth {
function _setCanExceedMaxGauges(
if (canExceedMax && account.code.length == 0) revert Errors.NonContractError(); // can only approve contracts
address account,

bool canExceedMax
canContractExceedMaxGauges[account] = canExceedMax;
) internal {
canExceedMaxGauges[account] = canExceedMax;


emit CanContractExceedMaxGaugesUpdate(account, canExceedMax);
emit CanExceedMaxGaugesUpdate(account, canExceedMax);
}
}


/*///////////////////////////////////////////////////////////////
/*///////////////////////////////////////////////////////////////
ERC20 LOGIC
ERC20 LOGIC
//////////////////////////////////////////////////////////////*/
//////////////////////////////////////////////////////////////*/


/// NOTE: any "removal" of tokens from a user requires userUnusedWeight < amount.
/// NOTE: any "removal" of tokens from a user requires userUnusedWeight < amount.
/// _decrementWeightUntilFree is called as a greedy algorithm to free up weight.
/// _decrementWeightUntilFree is called as a greedy algorithm to free up weight.
/// It may be more gas efficient to free weight before burning or transferring tokens.
/// It may be more gas efficient to free weight before burning or transferring tokens.


function _burn(address from, uint256 amount) internal virtual override {
function _burn(address from, uint256 amount) internal virtual override {
_decrementWeightUntilFree(from, amount);
_decrementWeightUntilFree(from, amount);
super._burn(from, amount);
super._burn(from, amount);
}
}


function transfer(address to, uint256 amount) public virtual override returns (bool) {
function transfer(
address to,
uint256 amount
) public virtual override returns (bool) {
_decrementWeightUntilFree(msg.sender, amount);
_decrementWeightUntilFree(msg.sender, amount);
return super.transfer(to, amount);
return super.transfer(to, amount);
}
}


function transferFrom(
function transferFrom(
address from,
address from,
address to,
address to,
uint256 amount
uint256 amount
) public virtual override returns (bool) {
) public virtual override returns (bool) {
_decrementWeightUntilFree(from, amount);
_decrementWeightUntilFree(from, amount);
return super.transferFrom(from, to, amount);
return super.transferFrom(from, to, amount);
}
}


/// a greedy algorithm for freeing weight before a token burn/transfer
/// a greedy algorithm for freeing weight before a token burn/transfer
/// frees up entire gauges, so likely will free more than `weight`
/// frees up entire gauges, so likely will free more than `weight`
function _decrementWeightUntilFree(address user, uint256 weight) internal {
function _decrementWeightUntilFree(address user, uint256 weight) internal {
uint256 userFreeWeight = balanceOf[user] - getUserWeight[user];
uint256 userFreeWeight = balanceOf(user) - getUserWeight[user];


// early return if already free
// early return if already free
if (userFreeWeight >= weight) return;
if (userFreeWeight >= weight) return;


uint32 currentCycle = _getGaugeCycleEnd();
uint32 currentCycle = _getGaugeCycleEnd();


// cache totals for batch updates
// cache totals for batch updates
uint112 userFreed;
uint112 userFreed;
uint112 totalFreed;
uint112 totalFreed;


// Loop through all user gauges, live and deprecated
// Loop through all user gauges, live and deprecated
address[] memory gaugeList = _userGauges[user].values();
address[] memory gaugeList = _userGauges[user].values();


// Free gauges until through entire list or under weight
// Free gauges until through entire list or under weight
uint256 size = gaugeList.length;
uint256 size = gaugeList.length;
for (uint256 i = 0; i < size && (userFreeWeight + totalFreed) < weight; ) {
for (
uint256 i = 0;
i < size && (userFreeWeight + userFreed) < weight;

) {
address gauge = gaugeList[i];
address gauge = gaugeList[i];
uint112 userGaugeWeight = getUserGaugeWeight[user][gauge];
uint112 userGaugeWeight = getUserGaugeWeight[user][gauge];
if (userGaugeWeight != 0) {
if (userGaugeWeight != 0) {
// If the gauge is live (not deprecated), include its weight in the total to remove
// If the gauge is live (not deprecated), include its weight in the total to remove
if (!_deprecatedGauges.contains(gauge)) {
if (!_deprecatedGauges.contains(gauge)) {
totalFreed += userGaugeWeight;
totalFreed += userGaugeWeight;
}
}
userFreed += userGaugeWeight;
userFreed += userGaugeWeight;
_decrementGaugeWeight(user, gauge, userGaugeWeight, currentCycle);
_decrementGaugeWeight(
user,
gauge,
userGaugeWeight,
currentCycle
);


unchecked {
unchecked {
i++;
++i;
}
}
}
}
}
}


getUserWeight[user] -= userFreed;
getUserWeight[user] -= userFreed;
_writeGaugeWeight(_totalWeight, _subtract, totalFreed, currentCycle);

}
}