Contract Source Code:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
import {IERC20} from "openzeppelin/contracts/interfaces/IERC20.sol";
import {SafeERC20} from "openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import {ReentrancyGuard} from "openzeppelin/contracts/utils/ReentrancyGuard.sol";
import {IGovernance} from "./interfaces/IGovernance.sol";
import {IInitiative} from "./interfaces/IInitiative.sol";
import {ILQTYStaking} from "./interfaces/ILQTYStaking.sol";
import {UserProxy} from "./UserProxy.sol";
import {UserProxyFactory} from "./UserProxyFactory.sol";
import {add, max} from "./utils/Math.sol";
import {_requireNoDuplicates, _requireNoNegatives} from "./utils/UniqueArray.sol";
import {Multicall} from "./utils/Multicall.sol";
import {WAD, PermitParams} from "./utils/Types.sol";
import {safeCallWithMinGas} from "./utils/SafeCallMinGas.sol";
import {Ownable} from "./utils/Ownable.sol";
/// @title Governance: Modular Initiative based Governance
contract Governance is Multicall, UserProxyFactory, ReentrancyGuard, Ownable, IGovernance {
using SafeERC20 for IERC20;
uint256 constant MIN_GAS_TO_HOOK = 350_000;
/// Replace this to ensure hooks have sufficient gas
/// @inheritdoc IGovernance
ILQTYStaking public immutable stakingV1;
/// @inheritdoc IGovernance
IERC20 public immutable lqty;
/// @inheritdoc IGovernance
IERC20 public immutable bold;
/// @inheritdoc IGovernance
uint256 public immutable EPOCH_START;
/// @inheritdoc IGovernance
uint256 public immutable EPOCH_DURATION;
/// @inheritdoc IGovernance
uint256 public immutable EPOCH_VOTING_CUTOFF;
/// @inheritdoc IGovernance
uint256 public immutable MIN_CLAIM;
/// @inheritdoc IGovernance
uint256 public immutable MIN_ACCRUAL;
/// @inheritdoc IGovernance
uint256 public immutable REGISTRATION_FEE;
/// @inheritdoc IGovernance
uint256 public immutable REGISTRATION_THRESHOLD_FACTOR;
/// @inheritdoc IGovernance
uint256 public immutable UNREGISTRATION_THRESHOLD_FACTOR;
/// @inheritdoc IGovernance
uint256 public immutable REGISTRATION_WARM_UP_PERIOD;
/// @inheritdoc IGovernance
uint256 public immutable UNREGISTRATION_AFTER_EPOCHS;
/// @inheritdoc IGovernance
uint256 public immutable VOTING_THRESHOLD_FACTOR;
/// @inheritdoc IGovernance
uint256 public boldAccrued;
/// @inheritdoc IGovernance
VoteSnapshot public votesSnapshot;
/// @inheritdoc IGovernance
mapping(address => InitiativeVoteSnapshot) public votesForInitiativeSnapshot;
/// @inheritdoc IGovernance
GlobalState public globalState;
/// @inheritdoc IGovernance
mapping(address => UserState) public userStates;
/// @inheritdoc IGovernance
mapping(address => InitiativeState) public initiativeStates;
/// @inheritdoc IGovernance
mapping(address => mapping(address => Allocation)) public lqtyAllocatedByUserToInitiative;
/// @inheritdoc IGovernance
mapping(address => uint16) public override registeredInitiatives;
uint16 constant UNREGISTERED_INITIATIVE = type(uint16).max;
// 100 Million LQTY will be necessary to make the rounding error cause 1 second of loss per operation
uint120 public constant TIMESTAMP_PRECISION = 1e26;
constructor(
address _lqty,
address _lusd,
address _stakingV1,
address _bold,
Configuration memory _config,
address _owner,
address[] memory _initiatives
) UserProxyFactory(_lqty, _lusd, _stakingV1) Ownable(_owner) {
stakingV1 = ILQTYStaking(_stakingV1);
lqty = IERC20(_lqty);
bold = IERC20(_bold);
require(_config.minClaim <= _config.minAccrual, "Gov: min-claim-gt-min-accrual");
REGISTRATION_FEE = _config.registrationFee;
// Registration threshold must be below 100% of votes
require(_config.registrationThresholdFactor < WAD, "Gov: registration-config");
REGISTRATION_THRESHOLD_FACTOR = _config.registrationThresholdFactor;
// Unregistration must be X times above the `votingThreshold`
require(_config.unregistrationThresholdFactor > WAD, "Gov: unregistration-config");
UNREGISTRATION_THRESHOLD_FACTOR = _config.unregistrationThresholdFactor;
REGISTRATION_WARM_UP_PERIOD = _config.registrationWarmUpPeriod;
UNREGISTRATION_AFTER_EPOCHS = _config.unregistrationAfterEpochs;
// Voting threshold must be below 100% of votes
require(_config.votingThresholdFactor < WAD, "Gov: voting-config");
VOTING_THRESHOLD_FACTOR = _config.votingThresholdFactor;
MIN_CLAIM = _config.minClaim;
MIN_ACCRUAL = _config.minAccrual;
EPOCH_START = _config.epochStart;
require(_config.epochDuration > 0, "Gov: epoch-duration-zero");
EPOCH_DURATION = _config.epochDuration;
require(_config.epochVotingCutoff < _config.epochDuration, "Gov: epoch-voting-cutoff-gt-epoch-duration");
EPOCH_VOTING_CUTOFF = _config.epochVotingCutoff;
if (_initiatives.length > 0) {
registerInitialInitiatives(_initiatives);
}
}
function registerInitialInitiatives(address[] memory _initiatives) public onlyOwner {
for (uint256 i = 0; i < _initiatives.length; i++) {
// Register initial initiatives in the earliest possible epoch, which lets us make them votable immediately
// post-deployment if we so choose, by backdating the first epoch at least EPOCH_DURATION in the past.
registeredInitiatives[_initiatives[i]] = 1;
emit RegisterInitiative(_initiatives[i], msg.sender, 1);
}
_renounceOwnership();
}
function _averageAge(uint120 _currentTimestamp, uint120 _averageTimestamp) internal pure returns (uint120) {
if (_averageTimestamp == 0 || _currentTimestamp < _averageTimestamp) return 0;
return _currentTimestamp - _averageTimestamp;
}
function _calculateAverageTimestamp(
uint120 _prevOuterAverageTimestamp,
uint120 _newInnerAverageTimestamp,
uint88 _prevLQTYBalance,
uint88 _newLQTYBalance
) internal view returns (uint120) {
if (_newLQTYBalance == 0) return 0;
// NOTE: Truncation
// NOTE: u32 -> u120
// While we upscale the Timestamp, the system will stop working at type(uint32).max
// Because the rest of the type is used for precision
uint120 currentTime = uint120(uint32(block.timestamp)) * uint120(TIMESTAMP_PRECISION);
uint120 prevOuterAverageAge = _averageAge(currentTime, _prevOuterAverageTimestamp);
uint120 newInnerAverageAge = _averageAge(currentTime, _newInnerAverageTimestamp);
// 120 for timestamps = 2^32 * 1e18 | 2^32 * 1e26
// 208 for voting power = 2^120 * 2^88
// NOTE: 208 / X can go past u120!
// Therefore we keep `newOuterAverageAge` as u208
uint208 newOuterAverageAge;
if (_prevLQTYBalance <= _newLQTYBalance) {
uint88 deltaLQTY = _newLQTYBalance - _prevLQTYBalance;
uint208 prevVotes = uint208(_prevLQTYBalance) * uint208(prevOuterAverageAge);
uint208 newVotes = uint208(deltaLQTY) * uint208(newInnerAverageAge);
uint208 votes = prevVotes + newVotes;
newOuterAverageAge = votes / _newLQTYBalance;
} else {
uint88 deltaLQTY = _prevLQTYBalance - _newLQTYBalance;
uint208 prevVotes = uint208(_prevLQTYBalance) * uint208(prevOuterAverageAge);
uint208 newVotes = uint208(deltaLQTY) * uint208(newInnerAverageAge);
uint208 votes = (prevVotes >= newVotes) ? prevVotes - newVotes : 0;
newOuterAverageAge = votes / _newLQTYBalance;
}
if (newOuterAverageAge > currentTime) return 0;
return uint120(currentTime - newOuterAverageAge);
}
/*//////////////////////////////////////////////////////////////
STAKING
//////////////////////////////////////////////////////////////*/
function _updateUserTimestamp(uint88 _lqtyAmount) private returns (UserProxy) {
require(_lqtyAmount > 0, "Governance: zero-lqty-amount");
// Assert that we have resetted here
UserState memory userState = userStates[msg.sender];
require(userState.allocatedLQTY == 0, "Governance: must-be-zero-allocation");
address userProxyAddress = deriveUserProxyAddress(msg.sender);
if (userProxyAddress.code.length == 0) {
deployUserProxy();
}
UserProxy userProxy = UserProxy(payable(userProxyAddress));
uint88 lqtyStaked = uint88(stakingV1.stakes(userProxyAddress));
// update the average staked timestamp for LQTY staked by the user
// NOTE: Upscale user TS by `TIMESTAMP_PRECISION`
userState.averageStakingTimestamp = _calculateAverageTimestamp(
userState.averageStakingTimestamp,
uint120(block.timestamp) * uint120(TIMESTAMP_PRECISION),
lqtyStaked,
lqtyStaked + _lqtyAmount
);
userStates[msg.sender] = userState;
emit DepositLQTY(msg.sender, _lqtyAmount);
return userProxy;
}
/// @inheritdoc IGovernance
function depositLQTY(uint88 _lqtyAmount) external {
depositLQTY(_lqtyAmount, false, msg.sender);
}
function depositLQTY(uint88 _lqtyAmount, bool _doSendRewards, address _recipient) public nonReentrant {
UserProxy userProxy = _updateUserTimestamp(_lqtyAmount);
userProxy.stake(_lqtyAmount, msg.sender, _doSendRewards, _recipient);
}
/// @inheritdoc IGovernance
function depositLQTYViaPermit(uint88 _lqtyAmount, PermitParams calldata _permitParams) external {
depositLQTYViaPermit(_lqtyAmount, _permitParams, false, msg.sender);
}
function depositLQTYViaPermit(
uint88 _lqtyAmount,
PermitParams calldata _permitParams,
bool _doSendRewards,
address _recipient
) public nonReentrant {
UserProxy userProxy = _updateUserTimestamp(_lqtyAmount);
userProxy.stakeViaPermit(_lqtyAmount, msg.sender, _permitParams, _doSendRewards, _recipient);
}
/// @inheritdoc IGovernance
function withdrawLQTY(uint88 _lqtyAmount) external {
withdrawLQTY(_lqtyAmount, true, msg.sender);
}
function withdrawLQTY(uint88 _lqtyAmount, bool _doSendRewards, address _recipient) public nonReentrant {
// check that user has reset before changing lqty balance
UserState storage userState = userStates[msg.sender];
require(userState.allocatedLQTY == 0, "Governance: must-allocate-zero");
UserProxy userProxy = UserProxy(payable(deriveUserProxyAddress(msg.sender)));
require(address(userProxy).code.length != 0, "Governance: user-proxy-not-deployed");
uint88 lqtyStaked = uint88(stakingV1.stakes(address(userProxy)));
(uint256 accruedLUSD, uint256 accruedETH) = userProxy.unstake(_lqtyAmount, _doSendRewards, _recipient);
emit WithdrawLQTY(msg.sender, _lqtyAmount, accruedLUSD, accruedETH);
}
/// @inheritdoc IGovernance
function claimFromStakingV1(address _rewardRecipient) external returns (uint256 accruedLUSD, uint256 accruedETH) {
address payable userProxyAddress = payable(deriveUserProxyAddress(msg.sender));
require(userProxyAddress.code.length != 0, "Governance: user-proxy-not-deployed");
return UserProxy(userProxyAddress).unstake(0, true, _rewardRecipient);
}
/*//////////////////////////////////////////////////////////////
VOTING
//////////////////////////////////////////////////////////////*/
/// @inheritdoc IGovernance
function epoch() public view returns (uint16) {
if (block.timestamp < EPOCH_START) {
return 0;
}
return uint16(((block.timestamp - EPOCH_START) / EPOCH_DURATION) + 1);
}
/// @inheritdoc IGovernance
function epochStart() public view returns (uint32) {
uint16 currentEpoch = epoch();
if (currentEpoch == 0) return 0;
return uint32(EPOCH_START + (currentEpoch - 1) * EPOCH_DURATION);
}
/// @inheritdoc IGovernance
function secondsWithinEpoch() public view returns (uint32) {
if (block.timestamp < EPOCH_START) return 0;
return uint32((block.timestamp - EPOCH_START) % EPOCH_DURATION);
}
/// @inheritdoc IGovernance
function lqtyToVotes(uint88 _lqtyAmount, uint120 _currentTimestamp, uint120 _averageTimestamp)
public
pure
returns (uint208)
{
return uint208(_lqtyAmount) * uint208(_averageAge(_currentTimestamp, _averageTimestamp));
}
/*//////////////////////////////////////////////////////////////
SNAPSHOTS
//////////////////////////////////////////////////////////////*/
/// @inheritdoc IGovernance
function getLatestVotingThreshold() public view returns (uint256) {
uint256 snapshotVotes = votesSnapshot.votes;
/// @audit technically can be out of synch
return calculateVotingThreshold(snapshotVotes);
}
/// @dev Returns the most up to date voting threshold
/// In contrast to `getLatestVotingThreshold` this function updates the snapshot
/// This ensures that the value returned is always the latest
function calculateVotingThreshold() public returns (uint256) {
(VoteSnapshot memory snapshot,) = _snapshotVotes();
return calculateVotingThreshold(snapshot.votes);
}
/// @dev Utility function to compute the threshold votes without recomputing the snapshot
/// Note that `boldAccrued` is a cached value, this function works correctly only when called after an accrual
function calculateVotingThreshold(uint256 _votes) public view returns (uint256) {
if (_votes == 0) return 0;
uint256 minVotes; // to reach MIN_CLAIM: snapshotVotes * MIN_CLAIM / boldAccrued
uint256 payoutPerVote = boldAccrued * WAD / _votes;
if (payoutPerVote != 0) {
minVotes = MIN_CLAIM * WAD / payoutPerVote;
}
return max(_votes * VOTING_THRESHOLD_FACTOR / WAD, minVotes);
}
// Snapshots votes at the end of the previous epoch
// Accrues funds until the first activity of the current epoch, which are valid throughout all of the current epoch
function _snapshotVotes() internal returns (VoteSnapshot memory snapshot, GlobalState memory state) {
bool shouldUpdate;
(snapshot, state, shouldUpdate) = getTotalVotesAndState();
if (shouldUpdate) {
votesSnapshot = snapshot;
uint256 boldBalance = bold.balanceOf(address(this));
boldAccrued = (boldBalance < MIN_ACCRUAL) ? 0 : boldBalance;
emit SnapshotVotes(snapshot.votes, snapshot.forEpoch);
}
}
/// @notice Return the most up to date global snapshot and state as well as a flag to notify whether the state can be updated
/// This is a convenience function to always retrieve the most up to date state values
function getTotalVotesAndState()
public
view
returns (VoteSnapshot memory snapshot, GlobalState memory state, bool shouldUpdate)
{
uint16 currentEpoch = epoch();
snapshot = votesSnapshot;
state = globalState;
if (snapshot.forEpoch < currentEpoch - 1) {
shouldUpdate = true;
snapshot.votes = lqtyToVotes(
state.countedVoteLQTY,
uint120(epochStart()) * uint120(TIMESTAMP_PRECISION),
state.countedVoteLQTYAverageTimestamp
);
snapshot.forEpoch = currentEpoch - 1;
}
}
// Snapshots votes for an initiative for the previous epoch but only count the votes
// if the received votes meet the voting threshold
function _snapshotVotesForInitiative(address _initiative)
internal
returns (InitiativeVoteSnapshot memory initiativeSnapshot, InitiativeState memory initiativeState)
{
bool shouldUpdate;
(initiativeSnapshot, initiativeState, shouldUpdate) = getInitiativeSnapshotAndState(_initiative);
if (shouldUpdate) {
votesForInitiativeSnapshot[_initiative] = initiativeSnapshot;
emit SnapshotVotesForInitiative(_initiative, initiativeSnapshot.votes, initiativeSnapshot.forEpoch);
}
}
/// @dev Given an initiative address, return it's most up to date snapshot and state as well as a flag to notify whether the state can be updated
/// This is a convenience function to always retrieve the most up to date state values
function getInitiativeSnapshotAndState(address _initiative)
public
view
returns (
InitiativeVoteSnapshot memory initiativeSnapshot,
InitiativeState memory initiativeState,
bool shouldUpdate
)
{
// Get the storage data
uint16 currentEpoch = epoch();
initiativeSnapshot = votesForInitiativeSnapshot[_initiative];
initiativeState = initiativeStates[_initiative];
if (initiativeSnapshot.forEpoch < currentEpoch - 1) {
shouldUpdate = true;
uint120 start = uint120(epochStart()) * uint120(TIMESTAMP_PRECISION);
uint208 votes =
lqtyToVotes(initiativeState.voteLQTY, start, initiativeState.averageStakingTimestampVoteLQTY);
uint208 vetos =
lqtyToVotes(initiativeState.vetoLQTY, start, initiativeState.averageStakingTimestampVetoLQTY);
// NOTE: Upscaling to u224 is safe
initiativeSnapshot.votes = votes;
initiativeSnapshot.vetos = vetos;
initiativeSnapshot.forEpoch = currentEpoch - 1;
}
}
/// @inheritdoc IGovernance
function snapshotVotesForInitiative(address _initiative)
external
nonReentrant
returns (VoteSnapshot memory voteSnapshot, InitiativeVoteSnapshot memory initiativeVoteSnapshot)
{
(voteSnapshot,) = _snapshotVotes();
(initiativeVoteSnapshot,) = _snapshotVotesForInitiative(_initiative);
}
/*//////////////////////////////////////////////////////////////
FSM
//////////////////////////////////////////////////////////////*/
enum InitiativeStatus {
NONEXISTENT,
/// This Initiative Doesn't exist | This is never returned
WARM_UP,
/// This epoch was just registered
SKIP,
/// This epoch will result in no rewards and no unregistering
CLAIMABLE,
/// This epoch will result in claiming rewards
CLAIMED,
/// The rewards for this epoch have been claimed
UNREGISTERABLE,
/// Can be unregistered
DISABLED // It was already Unregistered
}
/// @notice Given an inititive address, updates all snapshots and return the initiative state
/// See the view version of `getInitiativeState` for the underlying logic on Initatives FSM
function getInitiativeState(address _initiative)
public
returns (InitiativeStatus status, uint16 lastEpochClaim, uint256 claimableAmount)
{
(VoteSnapshot memory votesSnapshot_,) = _snapshotVotes();
(InitiativeVoteSnapshot memory votesForInitiativeSnapshot_, InitiativeState memory initiativeState) =
_snapshotVotesForInitiative(_initiative);
return getInitiativeState(_initiative, votesSnapshot_, votesForInitiativeSnapshot_, initiativeState);
}
/// @dev Given an initiative address and its snapshot, determines the current state for an initiative
function getInitiativeState(
address _initiative,
VoteSnapshot memory _votesSnapshot,
InitiativeVoteSnapshot memory _votesForInitiativeSnapshot,
InitiativeState memory _initiativeState
) public view returns (InitiativeStatus status, uint16 lastEpochClaim, uint256 claimableAmount) {
// == Non existent Condition == //
if (registeredInitiatives[_initiative] == 0) {
return (InitiativeStatus.NONEXISTENT, 0, 0);
/// By definition it has zero rewards
}
// == Just Registered Condition == //
if (registeredInitiatives[_initiative] == epoch()) {
return (InitiativeStatus.WARM_UP, 0, 0);
/// Was registered this week, cannot have rewards
}
// Fetch last epoch at which we claimed
lastEpochClaim = initiativeStates[_initiative].lastEpochClaim;
// == Disabled Condition == //
if (registeredInitiatives[_initiative] == UNREGISTERED_INITIATIVE) {
return (InitiativeStatus.DISABLED, lastEpochClaim, 0);
/// By definition it has zero rewards
}
// == Already Claimed Condition == //
if (lastEpochClaim >= epoch() - 1) {
// early return, we have already claimed
return (InitiativeStatus.CLAIMED, lastEpochClaim, claimableAmount);
}
// NOTE: Pass the snapshot value so we get accurate result
uint256 votingTheshold = calculateVotingThreshold(_votesSnapshot.votes);
// If it's voted and can get rewards
// Votes > calculateVotingThreshold
// == Rewards Conditions (votes can be zero, logic is the same) == //
// By definition if _votesForInitiativeSnapshot.votes > 0 then _votesSnapshot.votes > 0
uint256 upscaledInitiativeVotes = uint256(_votesForInitiativeSnapshot.votes);
uint256 upscaledInitiativeVetos = uint256(_votesForInitiativeSnapshot.vetos);
uint256 upscaledTotalVotes = uint256(_votesSnapshot.votes);
if (upscaledInitiativeVotes > votingTheshold && !(upscaledInitiativeVetos >= upscaledInitiativeVotes)) {
/// @audit 2^208 means we only have 2^48 left
/// Therefore we need to scale the value down by 4 orders of magnitude to make it fit
assert(upscaledInitiativeVotes * 1e14 / (VOTING_THRESHOLD_FACTOR / 1e4) > upscaledTotalVotes);
// 34 times when using 0.03e18 -> 33.3 + 1-> 33 + 1 = 34
uint256 CUSTOM_PRECISION = WAD / VOTING_THRESHOLD_FACTOR + 1;
/// @audit Because of the updated timestamp, we can run into overflows if we multiply by `boldAccrued`
/// We use `CUSTOM_PRECISION` for this reason, a smaller multiplicative value
/// The change SHOULD be safe because we already check for `threshold` before getting into these lines
/// As an alternative, this line could be replaced by https://github.com/Uniswap/v3-core/blob/main/contracts/libraries/FullMath.sol
uint256 claim =
upscaledInitiativeVotes * CUSTOM_PRECISION / upscaledTotalVotes * boldAccrued / CUSTOM_PRECISION;
return (InitiativeStatus.CLAIMABLE, lastEpochClaim, claim);
}
// == Unregister Condition == //
// e.g. if `UNREGISTRATION_AFTER_EPOCHS` is 4, the 4th epoch flip that would result in SKIP, will result in the initiative being `UNREGISTERABLE`
if (
(_initiativeState.lastEpochClaim + UNREGISTRATION_AFTER_EPOCHS < epoch() - 1)
|| upscaledInitiativeVetos > upscaledInitiativeVotes
&& upscaledInitiativeVetos > votingTheshold * UNREGISTRATION_THRESHOLD_FACTOR / WAD
) {
return (InitiativeStatus.UNREGISTERABLE, lastEpochClaim, 0);
}
// == Not meeting threshold Condition == //
return (InitiativeStatus.SKIP, lastEpochClaim, 0);
}
/// @inheritdoc IGovernance
function registerInitiative(address _initiative) external nonReentrant {
bold.safeTransferFrom(msg.sender, address(this), REGISTRATION_FEE);
require(_initiative != address(0), "Governance: zero-address");
(InitiativeStatus status,,) = getInitiativeState(_initiative);
require(status == InitiativeStatus.NONEXISTENT, "Governance: initiative-already-registered");
address userProxyAddress = deriveUserProxyAddress(msg.sender);
(VoteSnapshot memory snapshot,) = _snapshotVotes();
UserState memory userState = userStates[msg.sender];
// an initiative can be registered if the registrant has more voting power (LQTY * age)
// than the registration threshold derived from the previous epoch's total global votes
uint256 upscaledSnapshotVotes = uint256(snapshot.votes);
require(
lqtyToVotes(
uint88(stakingV1.stakes(userProxyAddress)),
uint120(epochStart()) * uint120(TIMESTAMP_PRECISION),
userState.averageStakingTimestamp
) >= upscaledSnapshotVotes * REGISTRATION_THRESHOLD_FACTOR / WAD,
"Governance: insufficient-lqty"
);
uint16 currentEpoch = epoch();
registeredInitiatives[_initiative] = currentEpoch;
/// @audit This ensures that the initiatives has UNREGISTRATION_AFTER_EPOCHS even after the first epoch
initiativeStates[_initiative].lastEpochClaim = currentEpoch - 1;
emit RegisterInitiative(_initiative, msg.sender, currentEpoch);
// Replaces try / catch | Enforces sufficient gas is passed
safeCallWithMinGas(
_initiative, MIN_GAS_TO_HOOK, 0, abi.encodeCall(IInitiative.onRegisterInitiative, (currentEpoch))
);
}
struct ResetInitiativeData {
address initiative;
int88 LQTYVotes;
int88 LQTYVetos;
}
/// @dev Resets an initiative and return the previous votes
/// NOTE: Technically we don't need vetos
/// NOTE: Technically we want to populate the `ResetInitiativeData` only when `secondsWithinEpoch() > EPOCH_VOTING_CUTOFF`
function _resetInitiatives(address[] calldata _initiativesToReset)
internal
returns (ResetInitiativeData[] memory)
{
ResetInitiativeData[] memory cachedData = new ResetInitiativeData[](_initiativesToReset.length);
int88[] memory deltaLQTYVotes = new int88[](_initiativesToReset.length);
int88[] memory deltaLQTYVetos = new int88[](_initiativesToReset.length);
// Prepare reset data
for (uint256 i; i < _initiativesToReset.length; i++) {
Allocation memory alloc = lqtyAllocatedByUserToInitiative[msg.sender][_initiativesToReset[i]];
require(alloc.voteLQTY > 0 || alloc.vetoLQTY > 0, "Governance: nothing to reset");
// Must be below, else we cannot reset"
// Makes cast safe
/// @audit Check INVARIANT: property_ensure_user_alloc_cannot_dos
assert(alloc.voteLQTY <= uint88(type(int88).max));
assert(alloc.vetoLQTY <= uint88(type(int88).max));
// Cache, used to enforce limits later
cachedData[i] = ResetInitiativeData({
initiative: _initiativesToReset[i],
LQTYVotes: int88(alloc.voteLQTY),
LQTYVetos: int88(alloc.vetoLQTY)
});
// -0 is still 0, so its fine to flip both
deltaLQTYVotes[i] = -int88(cachedData[i].LQTYVotes);
deltaLQTYVetos[i] = -int88(cachedData[i].LQTYVetos);
}
// RESET HERE || All initiatives will receive most updated data and 0 votes / vetos
_allocateLQTY(_initiativesToReset, deltaLQTYVotes, deltaLQTYVetos);
return cachedData;
}
/// @notice Reset the allocations for the initiatives being passed, must pass all initiatives else it will revert
/// NOTE: If you reset at the last day of the epoch, you won't be able to vote again
/// Use `allocateLQTY` to reset and vote
function resetAllocations(address[] calldata _initiativesToReset, bool checkAll) external nonReentrant {
_requireNoDuplicates(_initiativesToReset);
_resetInitiatives(_initiativesToReset);
// NOTE: In most cases, the check will pass
// But if you allocate too many initiatives, we may run OOG
// As such the check is optional here
// All other calls to the system enforce this
// So it's recommended that your last call to `resetAllocations` passes the check
if (checkAll) {
require(userStates[msg.sender].allocatedLQTY == 0, "Governance: must be a reset");
}
}
/// @inheritdoc IGovernance
function allocateLQTY(
address[] calldata _initiativesToReset,
address[] calldata _initiatives,
int88[] calldata _absoluteLQTYVotes,
int88[] calldata _absoluteLQTYVetos
) external nonReentrant {
require(_initiatives.length == _absoluteLQTYVotes.length, "Length");
require(_absoluteLQTYVetos.length == _absoluteLQTYVotes.length, "Length");
// To ensure the change is safe, enforce uniqueness
_requireNoDuplicates(_initiativesToReset);
_requireNoDuplicates(_initiatives);
// Explicit >= 0 checks for all values since we reset values below
_requireNoNegatives(_absoluteLQTYVotes);
_requireNoNegatives(_absoluteLQTYVetos);
// If the goal is to remove all votes from an initiative, including in _initiativesToReset is enough
_requireNoNOP(_absoluteLQTYVotes, _absoluteLQTYVetos);
// You MUST always reset
ResetInitiativeData[] memory cachedData = _resetInitiatives(_initiativesToReset);
/// Invariant, 0 allocated = 0 votes
UserState memory userState = userStates[msg.sender];
require(userState.allocatedLQTY == 0, "must be a reset");
// After cutoff you can only re-apply the same vote
// Or vote less
// Or abstain
// You can always add a veto, hence we only validate the addition of Votes
// And ignore the addition of vetos
// Validate the data here to ensure that the voting is capped at the amount in the other case
if (secondsWithinEpoch() > EPOCH_VOTING_CUTOFF) {
// Cap the max votes to the previous cache value
// This means that no new votes can happen here
// Removing and VETOING is always accepted
for (uint256 x; x < _initiatives.length; x++) {
// If we find it, we ensure it cannot be an increase
bool found;
for (uint256 y; y < cachedData.length; y++) {
if (cachedData[y].initiative == _initiatives[x]) {
found = true;
require(_absoluteLQTYVotes[x] <= cachedData[y].LQTYVotes, "Cannot increase");
break;
}
}
// Else we assert that the change is a veto, because by definition the initiatives will have received zero votes past this line
if (!found) {
require(_absoluteLQTYVotes[x] == 0, "Must be zero for new initiatives");
}
}
}
// Vote here, all values are now absolute changes
_allocateLQTY(_initiatives, _absoluteLQTYVotes, _absoluteLQTYVetos);
}
/// @dev For each given initiative applies relative changes to the allocation
/// NOTE: Given the current usage the function either: Resets the value to 0, or sets the value to a new value
/// Review the flows as the function could be used in many ways, but it ends up being used in just those 2 ways
function _allocateLQTY(
address[] memory _initiatives,
int88[] memory _deltaLQTYVotes,
int88[] memory _deltaLQTYVetos
) internal {
require(
_initiatives.length == _deltaLQTYVotes.length && _initiatives.length == _deltaLQTYVetos.length,
"Governance: array-length-mismatch"
);
(VoteSnapshot memory votesSnapshot_, GlobalState memory state) = _snapshotVotes();
uint16 currentEpoch = epoch();
UserState memory userState = userStates[msg.sender];
for (uint256 i = 0; i < _initiatives.length; i++) {
address initiative = _initiatives[i];
int88 deltaLQTYVotes = _deltaLQTYVotes[i];
int88 deltaLQTYVetos = _deltaLQTYVetos[i];
assert(deltaLQTYVotes != 0 || deltaLQTYVetos != 0);
/// === Check FSM === ///
// Can vote positively in SKIP, CLAIMABLE, CLAIMED and UNREGISTERABLE states
// Force to remove votes if disabled
// Can remove votes and vetos in every stage
(InitiativeVoteSnapshot memory votesForInitiativeSnapshot_, InitiativeState memory initiativeState) =
_snapshotVotesForInitiative(initiative);
(InitiativeStatus status,,) =
getInitiativeState(initiative, votesSnapshot_, votesForInitiativeSnapshot_, initiativeState);
if (deltaLQTYVotes > 0 || deltaLQTYVetos > 0) {
/// @audit You cannot vote on `unregisterable` but a vote may have been there
require(
status == InitiativeStatus.SKIP || status == InitiativeStatus.CLAIMABLE
|| status == InitiativeStatus.CLAIMED,
"Governance: active-vote-fsm"
);
}
if (status == InitiativeStatus.DISABLED) {
require(deltaLQTYVotes <= 0 && deltaLQTYVetos <= 0, "Must be a withdrawal");
}
/// === UPDATE ACCOUNTING === ///
// == INITIATIVE STATE == //
// deep copy of the initiative's state before the allocation
InitiativeState memory prevInitiativeState = InitiativeState(
initiativeState.voteLQTY,
initiativeState.vetoLQTY,
initiativeState.averageStakingTimestampVoteLQTY,
initiativeState.averageStakingTimestampVetoLQTY,
initiativeState.lastEpochClaim
);
// update the average staking timestamp for the initiative based on the user's average staking timestamp
initiativeState.averageStakingTimestampVoteLQTY = _calculateAverageTimestamp(
initiativeState.averageStakingTimestampVoteLQTY,
userState.averageStakingTimestamp,
/// @audit This is wrong unless we enforce a reset on deposit and withdrawal
initiativeState.voteLQTY,
add(initiativeState.voteLQTY, deltaLQTYVotes)
);
initiativeState.averageStakingTimestampVetoLQTY = _calculateAverageTimestamp(
initiativeState.averageStakingTimestampVetoLQTY,
userState.averageStakingTimestamp,
/// @audit This is wrong unless we enforce a reset on deposit and withdrawal
initiativeState.vetoLQTY,
add(initiativeState.vetoLQTY, deltaLQTYVetos)
);
// allocate the voting and vetoing LQTY to the initiative
initiativeState.voteLQTY = add(initiativeState.voteLQTY, deltaLQTYVotes);
initiativeState.vetoLQTY = add(initiativeState.vetoLQTY, deltaLQTYVetos);
// update the initiative's state
initiativeStates[initiative] = initiativeState;
// == GLOBAL STATE == //
// TODO: Veto reducing total votes logic change
// TODO: Accounting invariants
// TODO: Let's say I want to cap the votes vs weights
// Then by definition, I add the effective LQTY
// And the effective TS
// I remove the previous one
// and add the next one
// Veto > Vote
// Reduce down by Vote (cap min)
// If Vote > Veto
// Increase by Veto - Veto (reduced max)
// update the average staking timestamp for all counted voting LQTY
/// Discount previous only if the initiative was not unregistered
/// @audit We update the state only for non-disabled initiaitives
/// Disabled initiaitves have had their totals subtracted already
/// Math is also non associative so we cannot easily compare values
if (status != InitiativeStatus.DISABLED) {
/// @audit Trophy: `test_property_sum_of_lqty_global_user_matches_0`
/// Removing votes from state desynchs the state until all users remove their votes from the initiative
/// The invariant that holds is: the one that removes the initiatives that have been unregistered
state.countedVoteLQTYAverageTimestamp = _calculateAverageTimestamp(
state.countedVoteLQTYAverageTimestamp,
prevInitiativeState.averageStakingTimestampVoteLQTY,
/// @audit We don't have a test that fails when this line is changed
state.countedVoteLQTY,
state.countedVoteLQTY - prevInitiativeState.voteLQTY
);
assert(state.countedVoteLQTY >= prevInitiativeState.voteLQTY);
/// @audit INVARIANT: Never overflows
state.countedVoteLQTY -= prevInitiativeState.voteLQTY;
state.countedVoteLQTYAverageTimestamp = _calculateAverageTimestamp(
state.countedVoteLQTYAverageTimestamp,
initiativeState.averageStakingTimestampVoteLQTY,
state.countedVoteLQTY,
state.countedVoteLQTY + initiativeState.voteLQTY
);
state.countedVoteLQTY += initiativeState.voteLQTY;
}
// == USER ALLOCATION == //
// allocate the voting and vetoing LQTY to the initiative
Allocation memory allocation = lqtyAllocatedByUserToInitiative[msg.sender][initiative];
allocation.voteLQTY = add(allocation.voteLQTY, deltaLQTYVotes);
allocation.vetoLQTY = add(allocation.vetoLQTY, deltaLQTYVetos);
allocation.atEpoch = currentEpoch;
require(!(allocation.voteLQTY != 0 && allocation.vetoLQTY != 0), "Governance: vote-and-veto");
lqtyAllocatedByUserToInitiative[msg.sender][initiative] = allocation;
// == USER STATE == //
userState.allocatedLQTY = add(userState.allocatedLQTY, deltaLQTYVotes + deltaLQTYVetos);
emit AllocateLQTY(msg.sender, initiative, deltaLQTYVotes, deltaLQTYVetos, currentEpoch);
// Replaces try / catch | Enforces sufficient gas is passed
safeCallWithMinGas(
initiative,
MIN_GAS_TO_HOOK,
0,
abi.encodeCall(
IInitiative.onAfterAllocateLQTY, (currentEpoch, msg.sender, userState, allocation, initiativeState)
)
);
}
require(
userState.allocatedLQTY == 0
|| userState.allocatedLQTY <= uint88(stakingV1.stakes(deriveUserProxyAddress(msg.sender))),
"Governance: insufficient-or-allocated-lqty"
);
globalState = state;
userStates[msg.sender] = userState;
}
/// @inheritdoc IGovernance
function unregisterInitiative(address _initiative) external nonReentrant {
/// Enforce FSM
(VoteSnapshot memory votesSnapshot_, GlobalState memory state) = _snapshotVotes();
(InitiativeVoteSnapshot memory votesForInitiativeSnapshot_, InitiativeState memory initiativeState) =
_snapshotVotesForInitiative(_initiative);
(InitiativeStatus status,,) =
getInitiativeState(_initiative, votesSnapshot_, votesForInitiativeSnapshot_, initiativeState);
require(status != InitiativeStatus.NONEXISTENT, "Governance: initiative-not-registered");
require(status != InitiativeStatus.WARM_UP, "Governance: initiative-in-warm-up");
require(status == InitiativeStatus.UNREGISTERABLE, "Governance: cannot-unregister-initiative");
// Remove weight from current state
uint16 currentEpoch = epoch();
/// @audit Invariant: Must only claim once or unregister
// NOTE: Safe to remove | See `check_claim_soundness`
assert(initiativeState.lastEpochClaim < currentEpoch - 1);
// recalculate the average staking timestamp for all counted voting LQTY if the initiative was counted in
/// @audit Trophy: `test_property_sum_of_lqty_global_user_matches_0`
// Removing votes from state desynchs the state until all users remove their votes from the initiative
state.countedVoteLQTYAverageTimestamp = _calculateAverageTimestamp(
state.countedVoteLQTYAverageTimestamp,
initiativeState.averageStakingTimestampVoteLQTY,
state.countedVoteLQTY,
state.countedVoteLQTY - initiativeState.voteLQTY
);
assert(state.countedVoteLQTY >= initiativeState.voteLQTY);
/// RECON: Overflow
state.countedVoteLQTY -= initiativeState.voteLQTY;
globalState = state;
/// weeks * 2^16 > u32 so the contract will stop working before this is an issue
registeredInitiatives[_initiative] = UNREGISTERED_INITIATIVE;
emit UnregisterInitiative(_initiative, currentEpoch);
// Replaces try / catch | Enforces sufficient gas is passed
safeCallWithMinGas(
_initiative, MIN_GAS_TO_HOOK, 0, abi.encodeCall(IInitiative.onUnregisterInitiative, (currentEpoch))
);
}
/// @inheritdoc IGovernance
function claimForInitiative(address _initiative) external nonReentrant returns (uint256) {
// Accrue and update state
(VoteSnapshot memory votesSnapshot_,) = _snapshotVotes();
(InitiativeVoteSnapshot memory votesForInitiativeSnapshot_, InitiativeState memory initiativeState) =
_snapshotVotesForInitiative(_initiative);
// Compute values on accrued state
(InitiativeStatus status,, uint256 claimableAmount) =
getInitiativeState(_initiative, votesSnapshot_, votesForInitiativeSnapshot_, initiativeState);
if (status != InitiativeStatus.CLAIMABLE) {
return 0;
}
/// @audit INVARIANT: You can only claim for previous epoch
assert(votesSnapshot_.forEpoch == epoch() - 1);
/// All unclaimed rewards are always recycled
/// Invariant `lastEpochClaim` is < epoch() - 1; |
/// If `lastEpochClaim` is older than epoch() - 1 it means the initiative couldn't claim any rewards this epoch
initiativeStates[_initiative].lastEpochClaim = epoch() - 1;
// @audit INVARIANT, because of rounding errors the system can overpay
/// We upscale the timestamp to reduce the impact of the loss
/// However this is still possible
uint256 available = bold.balanceOf(address(this));
if (claimableAmount > available) {
claimableAmount = available;
}
bold.safeTransfer(_initiative, claimableAmount);
emit ClaimForInitiative(_initiative, claimableAmount, votesSnapshot_.forEpoch);
// Replaces try / catch | Enforces sufficient gas is passed
safeCallWithMinGas(
_initiative,
MIN_GAS_TO_HOOK,
0,
abi.encodeCall(IInitiative.onClaimForInitiative, (votesSnapshot_.forEpoch, claimableAmount))
);
return claimableAmount;
}
function _requireNoNOP(int88[] memory _absoluteLQTYVotes, int88[] memory _absoluteLQTYVetos) internal pure {
for (uint256 i; i < _absoluteLQTYVotes.length; i++) {
require(_absoluteLQTYVotes[i] > 0 || _absoluteLQTYVetos[i] > 0, "Governance: voting nothing");
}
}
}
// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v5.0.0) (interfaces/IERC20.sol)
pragma solidity ^0.8.20;
import {IERC20} from "../token/ERC20/IERC20.sol";
// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v5.0.0) (token/ERC20/utils/SafeERC20.sol)
pragma solidity ^0.8.20;
import {IERC20} from "../IERC20.sol";
import {IERC20Permit} from "../extensions/IERC20Permit.sol";
import {Address} from "../../../utils/Address.sol";
/**
* @title SafeERC20
* @dev Wrappers around ERC20 operations that throw on failure (when the token
* contract returns false). Tokens that return no value (and instead revert or
* throw on failure) are also supported, non-reverting calls are assumed to be
* successful.
* To use this library you can add a `using SafeERC20 for IERC20;` statement to your contract,
* which allows you to call the safe operations as `token.safeTransfer(...)`, etc.
*/
library SafeERC20 {
using Address for address;
/**
* @dev An operation with an ERC20 token failed.
*/
error SafeERC20FailedOperation(address token);
/**
* @dev Indicates a failed `decreaseAllowance` request.
*/
error SafeERC20FailedDecreaseAllowance(address spender, uint256 currentAllowance, uint256 requestedDecrease);
/**
* @dev Transfer `value` amount of `token` from the calling contract to `to`. If `token` returns no value,
* non-reverting calls are assumed to be successful.
*/
function safeTransfer(IERC20 token, address to, uint256 value) internal {
_callOptionalReturn(token, abi.encodeCall(token.transfer, (to, value)));
}
/**
* @dev Transfer `value` amount of `token` from `from` to `to`, spending the approval given by `from` to the
* calling contract. If `token` returns no value, non-reverting calls are assumed to be successful.
*/
function safeTransferFrom(IERC20 token, address from, address to, uint256 value) internal {
_callOptionalReturn(token, abi.encodeCall(token.transferFrom, (from, to, value)));
}
/**
* @dev Increase the calling contract's allowance toward `spender` by `value`. If `token` returns no value,
* non-reverting calls are assumed to be successful.
*/
function safeIncreaseAllowance(IERC20 token, address spender, uint256 value) internal {
uint256 oldAllowance = token.allowance(address(this), spender);
forceApprove(token, spender, oldAllowance + value);
}
/**
* @dev Decrease the calling contract's allowance toward `spender` by `requestedDecrease`. If `token` returns no
* value, non-reverting calls are assumed to be successful.
*/
function safeDecreaseAllowance(IERC20 token, address spender, uint256 requestedDecrease) internal {
unchecked {
uint256 currentAllowance = token.allowance(address(this), spender);
if (currentAllowance < requestedDecrease) {
revert SafeERC20FailedDecreaseAllowance(spender, currentAllowance, requestedDecrease);
}
forceApprove(token, spender, currentAllowance - requestedDecrease);
}
}
/**
* @dev Set the calling contract's allowance toward `spender` to `value`. If `token` returns no value,
* non-reverting calls are assumed to be successful. Meant to be used with tokens that require the approval
* to be set to zero before setting it to a non-zero value, such as USDT.
*/
function forceApprove(IERC20 token, address spender, uint256 value) internal {
bytes memory approvalCall = abi.encodeCall(token.approve, (spender, value));
if (!_callOptionalReturnBool(token, approvalCall)) {
_callOptionalReturn(token, abi.encodeCall(token.approve, (spender, 0)));
_callOptionalReturn(token, approvalCall);
}
}
/**
* @dev Imitates a Solidity high-level call (i.e. a regular function call to a contract), relaxing the requirement
* on the return value: the return value is optional (but if data is returned, it must not be false).
* @param token The token targeted by the call.
* @param data The call data (encoded using abi.encode or one of its variants).
*/
function _callOptionalReturn(IERC20 token, bytes memory data) private {
// We need to perform a low level call here, to bypass Solidity's return data size checking mechanism, since
// we're implementing it ourselves. We use {Address-functionCall} to perform this call, which verifies that
// the target address contains contract code and also asserts for success in the low-level call.
bytes memory returndata = address(token).functionCall(data);
if (returndata.length != 0 && !abi.decode(returndata, (bool))) {
revert SafeERC20FailedOperation(address(token));
}
}
/**
* @dev Imitates a Solidity high-level call (i.e. a regular function call to a contract), relaxing the requirement
* on the return value: the return value is optional (but if data is returned, it must not be false).
* @param token The token targeted by the call.
* @param data The call data (encoded using abi.encode or one of its variants).
*
* This is a variant of {_callOptionalReturn} that silents catches all reverts and returns a bool instead.
*/
function _callOptionalReturnBool(IERC20 token, bytes memory data) private returns (bool) {
// We need to perform a low level call here, to bypass Solidity's return data size checking mechanism, since
// we're implementing it ourselves. We cannot use {Address-functionCall} here since this should return false
// and not revert is the subcall reverts.
(bool success, bytes memory returndata) = address(token).call(data);
return success && (returndata.length == 0 || abi.decode(returndata, (bool))) && address(token).code.length > 0;
}
}
// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v5.0.0) (utils/ReentrancyGuard.sol)
pragma solidity ^0.8.20;
/**
* @dev Contract module that helps prevent reentrant calls to a function.
*
* Inheriting from `ReentrancyGuard` will make the {nonReentrant} modifier
* available, which can be applied to functions to make sure there are no nested
* (reentrant) calls to them.
*
* Note that because there is a single `nonReentrant` guard, functions marked as
* `nonReentrant` may not call one another. This can be worked around by making
* those functions `private`, and then adding `external` `nonReentrant` entry
* points to them.
*
* TIP: If you would like to learn more about reentrancy and alternative ways
* to protect against it, check out our blog post
* https://blog.openzeppelin.com/reentrancy-after-istanbul/[Reentrancy After Istanbul].
*/
abstract contract ReentrancyGuard {
// Booleans are more expensive than uint256 or any type that takes up a full
// word because each write operation emits an extra SLOAD to first read the
// slot's contents, replace the bits taken up by the boolean, and then write
// back. This is the compiler's defense against contract upgrades and
// pointer aliasing, and it cannot be disabled.
// The values being non-zero value makes deployment a bit more expensive,
// but in exchange the refund on every call to nonReentrant will be lower in
// amount. Since refunds are capped to a percentage of the total
// transaction's gas, it is best to keep them low in cases like this one, to
// increase the likelihood of the full refund coming into effect.
uint256 private constant NOT_ENTERED = 1;
uint256 private constant ENTERED = 2;
uint256 private _status;
/**
* @dev Unauthorized reentrant call.
*/
error ReentrancyGuardReentrantCall();
constructor() {
_status = NOT_ENTERED;
}
/**
* @dev Prevents a contract from calling itself, directly or indirectly.
* Calling a `nonReentrant` function from another `nonReentrant`
* function is not supported. It is possible to prevent this from happening
* by making the `nonReentrant` function external, and making it call a
* `private` function that does the actual work.
*/
modifier nonReentrant() {
_nonReentrantBefore();
_;
_nonReentrantAfter();
}
function _nonReentrantBefore() private {
// On the first call to nonReentrant, _status will be NOT_ENTERED
if (_status == ENTERED) {
revert ReentrancyGuardReentrantCall();
}
// Any calls to nonReentrant after this point will fail
_status = ENTERED;
}
function _nonReentrantAfter() private {
// By storing the original value once again, a refund is triggered (see
// https://eips.ethereum.org/EIPS/eip-2200)
_status = NOT_ENTERED;
}
/**
* @dev Returns true if the reentrancy guard is currently set to "entered", which indicates there is a
* `nonReentrant` function in the call stack.
*/
function _reentrancyGuardEntered() internal view returns (bool) {
return _status == ENTERED;
}
}
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
import {IERC20} from "openzeppelin/contracts/interfaces/IERC20.sol";
import {ILQTYStaking} from "./ILQTYStaking.sol";
import {PermitParams} from "../utils/Types.sol";
interface IGovernance {
event DepositLQTY(address user, uint256 depositedLQTY);
event WithdrawLQTY(address user, uint256 withdrawnLQTY, uint256 accruedLUSD, uint256 accruedETH);
event SnapshotVotes(uint240 votes, uint16 forEpoch);
event SnapshotVotesForInitiative(address initiative, uint240 votes, uint16 forEpoch);
event RegisterInitiative(address initiative, address registrant, uint16 atEpoch);
event UnregisterInitiative(address initiative, uint16 atEpoch);
event AllocateLQTY(address user, address initiative, int256 deltaVoteLQTY, int256 deltaVetoLQTY, uint16 atEpoch);
event ClaimForInitiative(address initiative, uint256 bold, uint256 forEpoch);
struct Configuration {
uint128 registrationFee;
uint128 registrationThresholdFactor;
uint128 unregistrationThresholdFactor;
uint16 registrationWarmUpPeriod;
uint16 unregistrationAfterEpochs;
uint128 votingThresholdFactor;
uint88 minClaim;
uint88 minAccrual;
uint32 epochStart;
uint32 epochDuration;
uint32 epochVotingCutoff;
}
function registerInitialInitiatives(address[] memory _initiatives) external;
/// @notice Address of the LQTY StakingV1 contract
/// @return stakingV1 Address of the LQTY StakingV1 contract
function stakingV1() external view returns (ILQTYStaking stakingV1);
/// @notice Address of the LQTY token
/// @return lqty Address of the LQTY token
function lqty() external view returns (IERC20 lqty);
/// @notice Address of the BOLD token
/// @return bold Address of the BOLD token
function bold() external view returns (IERC20 bold);
/// @notice Timestamp at which the first epoch starts
/// @return epochStart Timestamp at which the first epoch starts
function EPOCH_START() external view returns (uint256 epochStart);
/// @notice Duration of an epoch in seconds (e.g. 1 week)
/// @return epochDuration Epoch duration
function EPOCH_DURATION() external view returns (uint256 epochDuration);
/// @notice Voting period of an epoch in seconds (e.g. 6 days)
/// @return epochVotingCutoff Epoch voting cutoff
function EPOCH_VOTING_CUTOFF() external view returns (uint256 epochVotingCutoff);
/// @notice Minimum BOLD amount that has to be claimed, if an initiative doesn't have enough votes to meet the
/// criteria then it's votes a excluded from the vote count and distribution
/// @return minClaim Minimum claim amount
function MIN_CLAIM() external view returns (uint256 minClaim);
/// @notice Minimum amount of BOLD that have to be accrued for an epoch, otherwise accrual will be skipped for
/// that epoch
/// @return minAccrual Minimum amount of BOLD
function MIN_ACCRUAL() external view returns (uint256 minAccrual);
/// @notice Amount of BOLD to be paid in order to register a new initiative
/// @return registrationFee Registration fee
function REGISTRATION_FEE() external view returns (uint256 registrationFee);
/// @notice Share of all votes that are necessary to register a new initiative
/// @return registrationThresholdFactor Threshold factor
function REGISTRATION_THRESHOLD_FACTOR() external view returns (uint256 registrationThresholdFactor);
/// @notice Multiple of the voting threshold in vetos that are necessary to unregister an initiative
/// @return unregistrationThresholdFactor Unregistration threshold factor
function UNREGISTRATION_THRESHOLD_FACTOR() external view returns (uint256 unregistrationThresholdFactor);
/// @notice Number of epochs an initiative has to exist before it can be unregistered
/// @return registrationWarmUpPeriod Number of epochs
function REGISTRATION_WARM_UP_PERIOD() external view returns (uint256 registrationWarmUpPeriod);
/// @notice Number of epochs an initiative has to be inactive before it can be unregistered
/// @return unregistrationAfterEpochs Number of epochs
function UNREGISTRATION_AFTER_EPOCHS() external view returns (uint256 unregistrationAfterEpochs);
/// @notice Share of all votes that are necessary for an initiative to be included in the vote count
/// @return votingThresholdFactor Voting threshold factor
function VOTING_THRESHOLD_FACTOR() external view returns (uint256 votingThresholdFactor);
/// @notice Returns the amount of BOLD accrued since last epoch (last snapshot)
/// @return boldAccrued BOLD accrued
function boldAccrued() external view returns (uint256 boldAccrued);
struct VoteSnapshot {
uint240 votes; // Votes at epoch transition
uint16 forEpoch; // Epoch for which the votes are counted
}
struct InitiativeVoteSnapshot {
uint224 votes; // Votes at epoch transition
uint16 forEpoch; // Epoch for which the votes are counted
uint16 lastCountedEpoch; // Epoch at which which the votes where counted last in the global snapshot
uint224 vetos; // Vetos at epoch transition
}
/// @notice Returns the vote count snapshot of the previous epoch
/// @return votes Number of votes
/// @return forEpoch Epoch for which the votes are counted
function votesSnapshot() external view returns (uint240 votes, uint16 forEpoch);
/// @notice Returns the vote count snapshot for an initiative of the previous epoch
/// @param _initiative Address of the initiative
/// @return votes Number of votes
/// @return forEpoch Epoch for which the votes are counted
/// @return lastCountedEpoch Epoch at which which the votes where counted last in the global snapshot
function votesForInitiativeSnapshot(address _initiative)
external
view
returns (uint224 votes, uint16 forEpoch, uint16 lastCountedEpoch, uint224 vetos);
struct Allocation {
uint88 voteLQTY; // LQTY allocated vouching for the initiative
uint88 vetoLQTY; // LQTY vetoing the initiative
uint16 atEpoch; // Epoch at which the allocation was last updated
}
struct UserState {
uint88 allocatedLQTY; // LQTY allocated by the user
uint120 averageStakingTimestamp; // Average timestamp at which LQTY was staked by the user
}
struct InitiativeState {
uint88 voteLQTY; // LQTY allocated vouching for the initiative
uint88 vetoLQTY; // LQTY allocated vetoing the initiative
uint120 averageStakingTimestampVoteLQTY; // Average staking timestamp of the voting LQTY for the initiative
uint120 averageStakingTimestampVetoLQTY; // Average staking timestamp of the vetoing LQTY for the initiative
uint16 lastEpochClaim;
}
struct GlobalState {
uint88 countedVoteLQTY; // Total LQTY that is included in vote counting
uint120 countedVoteLQTYAverageTimestamp; // Average timestamp: derived initiativeAllocation.averageTimestamp
}
/// TODO: Bold balance? Prob cheaper
/// @notice Returns the user's state
/// @param _user Address of the user
/// @return allocatedLQTY LQTY allocated by the user
/// @return averageStakingTimestamp Average timestamp at which LQTY was staked (deposited) by the user
function userStates(address _user) external view returns (uint88 allocatedLQTY, uint120 averageStakingTimestamp);
/// @notice Returns the initiative's state
/// @param _initiative Address of the initiative
/// @return voteLQTY LQTY allocated vouching for the initiative
/// @return vetoLQTY LQTY allocated vetoing the initiative
/// @return averageStakingTimestampVoteLQTY // Average staking timestamp of the voting LQTY for the initiative
/// @return averageStakingTimestampVetoLQTY // Average staking timestamp of the vetoing LQTY for the initiative
/// @return lastEpochClaim // Last epoch at which rewards were claimed
function initiativeStates(address _initiative)
external
view
returns (
uint88 voteLQTY,
uint88 vetoLQTY,
uint120 averageStakingTimestampVoteLQTY,
uint120 averageStakingTimestampVetoLQTY,
uint16 lastEpochClaim
);
/// @notice Returns the global state
/// @return countedVoteLQTY Total LQTY that is included in vote counting
/// @return countedVoteLQTYAverageTimestamp Average timestamp: derived initiativeAllocation.averageTimestamp
function globalState() external view returns (uint88 countedVoteLQTY, uint120 countedVoteLQTYAverageTimestamp);
/// @notice Returns the amount of voting and vetoing LQTY a user allocated to an initiative
/// @param _user Address of the user
/// @param _initiative Address of the initiative
/// @return voteLQTY LQTY allocated vouching for the initiative
/// @return vetoLQTY LQTY allocated vetoing the initiative
/// @return atEpoch Epoch at which the allocation was last updated
function lqtyAllocatedByUserToInitiative(address _user, address _initiative)
external
view
returns (uint88 voteLQTY, uint88 vetoLQTY, uint16 atEpoch);
/// @notice Returns when an initiative was registered
/// @param _initiative Address of the initiative
/// @return atEpoch Epoch at which the initiative was registered
function registeredInitiatives(address _initiative) external view returns (uint16 atEpoch);
/*//////////////////////////////////////////////////////////////
STAKING
//////////////////////////////////////////////////////////////*/
/// @notice Deposits LQTY
/// @dev The caller has to approve this contract to spend the LQTY tokens
/// @param _lqtyAmount Amount of LQTY to deposit
function depositLQTY(uint88 _lqtyAmount) external;
/// @notice Deposits LQTY via Permit
/// @param _lqtyAmount Amount of LQTY to deposit
/// @param _permitParams Permit parameters
function depositLQTYViaPermit(uint88 _lqtyAmount, PermitParams memory _permitParams) external;
/// @notice Withdraws LQTY and claims any accrued LUSD and ETH rewards from StakingV1
/// @param _lqtyAmount Amount of LQTY to withdraw
function withdrawLQTY(uint88 _lqtyAmount) external;
/// @notice Claims staking rewards from StakingV1 without unstaking
/// @param _rewardRecipient Address that will receive the rewards
/// @return accruedLUSD Amount of LUSD accrued
/// @return accruedETH Amount of ETH accrued
function claimFromStakingV1(address _rewardRecipient) external returns (uint256 accruedLUSD, uint256 accruedETH);
/*//////////////////////////////////////////////////////////////
VOTING
//////////////////////////////////////////////////////////////*/
/// @notice Returns the current epoch number
/// @return epoch Current epoch
function epoch() external view returns (uint16 epoch);
/// @notice Returns the timestamp at which the current epoch started
/// @return epochStart Epoch start of the current epoch
function epochStart() external view returns (uint32 epochStart);
/// @notice Returns the number of seconds that have gone by since the current epoch started
/// @return secondsWithinEpoch Seconds within the current epoch
function secondsWithinEpoch() external view returns (uint32 secondsWithinEpoch);
/// @notice Returns the number of votes per LQTY for a user
/// @param _lqtyAmount Amount of LQTY to convert to votes
/// @param _currentTimestamp Current timestamp
/// @param _averageTimestamp Average timestamp at which the LQTY was staked
/// @return votes Number of votes
function lqtyToVotes(uint88 _lqtyAmount, uint120 _currentTimestamp, uint120 _averageTimestamp)
external
pure
returns (uint208);
/// @notice Voting threshold is the max. of either:
/// - 4% of the total voting LQTY in the previous epoch
/// - or the minimum number of votes necessary to claim at least MIN_CLAIM BOLD
/// This value can be offsynch, use the non view `calculateVotingThreshold` to always retrieve the most up to date value
/// @return votingThreshold Voting threshold
function getLatestVotingThreshold() external view returns (uint256 votingThreshold);
/// @notice Snapshots votes for the previous epoch and accrues funds for the current epoch
/// @param _initiative Address of the initiative
/// @return voteSnapshot Vote snapshot
/// @return initiativeVoteSnapshot Vote snapshot of the initiative
function snapshotVotesForInitiative(address _initiative)
external
returns (VoteSnapshot memory voteSnapshot, InitiativeVoteSnapshot memory initiativeVoteSnapshot);
/// @notice Registers a new initiative
/// @param _initiative Address of the initiative
function registerInitiative(address _initiative) external;
// /// @notice Unregisters an initiative if it didn't receive enough votes in the last 4 epochs
// /// or if it received more vetos than votes and the number of vetos are greater than 3 times the voting threshold
// /// @param _initiative Address of the initiative
function unregisterInitiative(address _initiative) external;
/// @notice Allocates the user's LQTY to initiatives
/// @dev The user can only allocate to active initiatives (older than 1 epoch) and has to have enough unallocated
/// LQTY available, the initiatives listed must be unique, and towards the end of the epoch a user can only maintain or reduce their votes
/// @param _resetInitiatives Addresses of the initiatives the caller was previously allocated to, must be reset to prevent desynch of voting power
/// @param _initiatives Addresses of the initiatives to allocate to, can match or be different from `_resetInitiatives`
/// @param _absoluteLQTYVotes Delta LQTY to allocate to the initiatives as votes
/// @param absoluteLQTYVetos Delta LQTY to allocate to the initiatives as vetos
function allocateLQTY(
address[] calldata _resetInitiatives,
address[] memory _initiatives,
int88[] memory _absoluteLQTYVotes,
int88[] memory absoluteLQTYVetos
) external;
/// @notice Splits accrued funds according to votes received between all initiatives
/// @param _initiative Addresse of the initiative
/// @return claimed Amount of BOLD claimed
function claimForInitiative(address _initiative) external returns (uint256 claimed);
}
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
import {IGovernance} from "./IGovernance.sol";
interface IInitiative {
/// @notice Callback hook that is called by Governance after the initiative was successfully registered
/// @param _atEpoch Epoch at which the initiative is registered
function onRegisterInitiative(uint16 _atEpoch) external;
/// @notice Callback hook that is called by Governance after the initiative was unregistered
/// @param _atEpoch Epoch at which the initiative is unregistered
function onUnregisterInitiative(uint16 _atEpoch) external;
/// @notice Callback hook that is called by Governance after the LQTY allocation is updated by a user
/// @param _currentEpoch Epoch at which the LQTY allocation is updated
/// @param _user Address of the user that updated their LQTY allocation
/// @param _userState User state
/// @param _allocation Allocation state from user to initiative
/// @param _initiativeState Initiative state
function onAfterAllocateLQTY(
uint16 _currentEpoch,
address _user,
IGovernance.UserState calldata _userState,
IGovernance.Allocation calldata _allocation,
IGovernance.InitiativeState calldata _initiativeState
) external;
/// @notice Callback hook that is called by Governance after the claim for the last epoch was distributed
/// to the initiative
/// @param _claimEpoch Epoch at which the claim was distributed
/// @param _bold Amount of BOLD that was distributed
function onClaimForInitiative(uint16 _claimEpoch, uint256 _bold) external;
}
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
interface ILQTYStaking {
// --- Events --
event LQTYTokenAddressSet(address _lqtyTokenAddress);
event LUSDTokenAddressSet(address _lusdTokenAddress);
event TroveManagerAddressSet(address _troveManager);
event BorrowerOperationsAddressSet(address _borrowerOperationsAddress);
event ActivePoolAddressSet(address _activePoolAddress);
event StakeChanged(address indexed staker, uint256 newStake);
event StakingGainsWithdrawn(address indexed staker, uint256 LUSDGain, uint256 ETHGain);
event F_ETHUpdated(uint256 _F_ETH);
event F_LUSDUpdated(uint256 _F_LUSD);
event TotalLQTYStakedUpdated(uint256 _totalLQTYStaked);
event EtherSent(address _account, uint256 _amount);
event StakerSnapshotsUpdated(address _staker, uint256 _F_ETH, uint256 _F_LUSD);
// --- Functions ---
function setAddresses(
address _lqtyTokenAddress,
address _lusdTokenAddress,
address _troveManagerAddress,
address _borrowerOperationsAddress,
address _activePoolAddress
) external;
function stake(uint256 _LQTYamount) external;
function unstake(uint256 _LQTYamount) external;
function increaseF_ETH(uint256 _ETHFee) external;
function increaseF_LUSD(uint256 _LQTYFee) external;
function getPendingETHGain(address _user) external view returns (uint256);
function getPendingLUSDGain(address _user) external view returns (uint256);
function stakes(address _user) external view returns (uint256);
function totalLQTYStaked() external view returns (uint256);
}
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
import {IERC20} from "openzeppelin/contracts/interfaces/IERC20.sol";
import {IERC20Permit} from "openzeppelin/contracts/token/ERC20/extensions/IERC20Permit.sol";
import {IUserProxy} from "./interfaces/IUserProxy.sol";
import {ILQTYStaking} from "./interfaces/ILQTYStaking.sol";
import {PermitParams} from "./utils/Types.sol";
contract UserProxy is IUserProxy {
/// @inheritdoc IUserProxy
IERC20 public immutable lqty;
/// @inheritdoc IUserProxy
IERC20 public immutable lusd;
/// @inheritdoc IUserProxy
ILQTYStaking public immutable stakingV1;
/// @inheritdoc IUserProxy
address public immutable stakingV2;
constructor(address _lqty, address _lusd, address _stakingV1) {
lqty = IERC20(_lqty);
lusd = IERC20(_lusd);
stakingV1 = ILQTYStaking(_stakingV1);
stakingV2 = msg.sender;
}
modifier onlyStakingV2() {
require(msg.sender == stakingV2, "UserProxy: caller-not-stakingV2");
_;
}
/// @inheritdoc IUserProxy
function stake(uint256 _amount, address _lqtyFrom, bool _doSendRewards, address _recipient)
public
onlyStakingV2
returns (uint256 lusdAmount, uint256 ethAmount)
{
uint256 initialLUSDAmount = lusd.balanceOf(address(this));
uint256 initialETHAmount = address(this).balance;
lqty.transferFrom(_lqtyFrom, address(this), _amount);
stakingV1.stake(_amount);
emit Stake(_amount, _lqtyFrom);
if (_doSendRewards) {
(lusdAmount, ethAmount) = _sendRewards(_recipient, initialLUSDAmount, initialETHAmount);
}
}
/// @inheritdoc IUserProxy
function stakeViaPermit(
uint256 _amount,
address _lqtyFrom,
PermitParams calldata _permitParams,
bool _doSendRewards,
address _recipient
) public onlyStakingV2 returns (uint256 lusdAmount, uint256 ethAmount) {
require(_lqtyFrom == _permitParams.owner, "UserProxy: owner-not-sender");
uint256 initialLUSDAmount = lusd.balanceOf(address(this));
uint256 initialETHAmount = address(this).balance;
try IERC20Permit(address(lqty)).permit(
_permitParams.owner,
_permitParams.spender,
_permitParams.value,
_permitParams.deadline,
_permitParams.v,
_permitParams.r,
_permitParams.s
) {} catch {}
stake(_amount, _lqtyFrom, _doSendRewards, _recipient);
if (_doSendRewards) {
(lusdAmount, ethAmount) = _sendRewards(_recipient, initialLUSDAmount, initialETHAmount);
}
}
/// @inheritdoc IUserProxy
function unstake(uint256 _amount, bool _doSendRewards, address _recipient)
public
onlyStakingV2
returns (uint256 lusdAmount, uint256 ethAmount)
{
uint256 initialLQTYAmount = lqty.balanceOf(address(this));
uint256 initialLUSDAmount = lusd.balanceOf(address(this));
uint256 initialETHAmount = address(this).balance;
stakingV1.unstake(_amount);
uint256 lqtyAmount = lqty.balanceOf(address(this));
if (lqtyAmount > 0) lqty.transfer(_recipient, lqtyAmount);
emit Unstake(_recipient, lqtyAmount - initialLQTYAmount, lqtyAmount);
if (_doSendRewards) {
(lusdAmount, ethAmount) = _sendRewards(_recipient, initialLUSDAmount, initialETHAmount);
}
}
function _sendRewards(address _recipient, uint256 _initialLUSDAmount, uint256 _initialETHAmount)
internal
returns (uint256 lusdAmount, uint256 ethAmount)
{
lusdAmount = lusd.balanceOf(address(this));
if (lusdAmount > 0) lusd.transfer(_recipient, lusdAmount);
ethAmount = address(this).balance;
if (ethAmount > 0) {
(bool success,) = payable(_recipient).call{value: ethAmount}("");
require(success, "UserProxy: eth-fail");
}
emit SendRewards(
_recipient, lusdAmount - _initialLUSDAmount, lusdAmount, ethAmount - _initialETHAmount, ethAmount
);
}
/// @inheritdoc IUserProxy
function staked() external view returns (uint88) {
return uint88(stakingV1.stakes(address(this)));
}
receive() external payable {}
}
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
import {Clones} from "openzeppelin/contracts/proxy/Clones.sol";
import {IUserProxyFactory} from "./interfaces/IUserProxyFactory.sol";
import {UserProxy} from "./UserProxy.sol";
contract UserProxyFactory is IUserProxyFactory {
/// @inheritdoc IUserProxyFactory
address public immutable userProxyImplementation;
constructor(address _lqty, address _lusd, address _stakingV1) {
userProxyImplementation = address(new UserProxy(_lqty, _lusd, _stakingV1));
}
/// @inheritdoc IUserProxyFactory
function deriveUserProxyAddress(address _user) public view returns (address) {
return Clones.predictDeterministicAddress(userProxyImplementation, bytes32(uint256(uint160(_user))));
}
/// @inheritdoc IUserProxyFactory
function deployUserProxy() public returns (address) {
// reverts if the user already has a proxy
address userProxy = Clones.cloneDeterministic(userProxyImplementation, bytes32(uint256(uint160(msg.sender))));
emit DeployUserProxy(msg.sender, userProxy);
return userProxy;
}
}
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
function add(uint88 a, int88 b) pure returns (uint88) {
if (b < 0) {
return a - abs(b);
}
return a + abs(b);
}
function max(uint256 a, uint256 b) pure returns (uint256) {
return a > b ? a : b;
}
function abs(int88 a) pure returns (uint88) {
return a < 0 ? uint88(uint256(-int256(a))) : uint88(a);
}
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
/// @dev Checks that there's no duplicate addresses
/// @param arr - List to check for dups
function _requireNoDuplicates(address[] calldata arr) pure {
uint256 arrLength = arr.length;
if (arrLength == 0) return;
// only up to len - 1 (no j to check if i == len - 1)
for (uint i; i < arrLength - 1;) {
for (uint j = i + 1; j < arrLength;) {
require(arr[i] != arr[j], "dup");
unchecked {
++j;
}
}
unchecked {
++i;
}
}
}
function _requireNoNegatives(int88[] memory vals) pure {
uint256 arrLength = vals.length;
for (uint i; i < arrLength; i++) {
require(vals[i] >= 0, "Cannot be negative");
}
}
// SPDX-License-Identifier: GPL-2.0-or-later
pragma solidity ^0.8.24;
import {IMulticall} from "../interfaces/IMulticall.sol";
/// Copied from: https://github.com/Uniswap/v3-periphery/blob/main/contracts/base/Multicall.sol
/// @title Multicall
/// @notice Enables calling multiple methods in a single call to the contract
abstract contract Multicall is IMulticall {
/// @inheritdoc IMulticall
function multicall(bytes[] calldata data) public payable override returns (bytes[] memory results) {
results = new bytes[](data.length);
for (uint256 i = 0; i < data.length; i++) {
(bool success, bytes memory result) = address(this).delegatecall(data[i]);
if (!success) {
// Next 5 lines from https://ethereum.stackexchange.com/a/83577
if (result.length < 68) revert();
assembly {
result := add(result, 0x04)
}
revert(abi.decode(result, (string)));
}
results[i] = result;
}
}
}
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
struct PermitParams {
address owner;
address spender;
uint256 value;
uint256 deadline;
uint8 v;
bytes32 r;
bytes32 s;
}
uint256 constant WAD = 1e18;
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
/// @notice Given the gas requirement, ensures that the current context has sufficient gas to perform a call + a fixed buffer
/// @dev Credits: https://github.com/ethereum-optimism/optimism/blob/develop/packages/contracts-bedrock/src/libraries/SafeCall.sol#L100-L107
function hasMinGas(uint256 _minGas, uint256 _reservedGas) view returns (bool) {
bool _hasMinGas;
assembly {
// Equation: gas × 63 ≥ minGas × 64 + 63(40_000 + reservedGas)
_hasMinGas := iszero(lt(mul(gas(), 63), add(mul(_minGas, 64), mul(add(40000, _reservedGas), 63))))
}
return _hasMinGas;
}
/// @dev Performs a call ignoring the recipient existing or not, passing the exact gas value, ignoring any return value
function safeCallWithMinGas(address _target, uint256 _gas, uint256 _value, bytes memory _calldata)
returns (bool success)
{
/// @audit This is not necessary
/// But this is basically a worst case estimate of mem exp cost + operations before the call
require(hasMinGas(_gas, 1_000), "Must have minGas");
// dispatch message to recipient
// by assembly calling "handle" function
// we call via assembly to avoid memcopying a very large returndata
// returned by a malicious contract
assembly {
success :=
call(
_gas, // gas
_target, // recipient
_value, // ether value
add(_calldata, 0x20), // inloc
mload(_calldata), // inlen
0, // outloc
0 // outlen
)
// Ignore all return values
}
return (success);
}
// SPDX-License-Identifier: MIT
pragma solidity 0.8.24;
/**
* Based on OpenZeppelin's Ownable contract:
* https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/access/Ownable.sol
*
* @dev Contract module which provides a basic access control mechanism, where
* there is an account (an owner) that can be granted exclusive access to
* specific functions.
*
* This module is used through inheritance. It will make available the modifier
* `onlyOwner`, which can be applied to your functions to restrict their use to
* the owner.
*/
contract Ownable {
address private _owner;
event OwnershipTransferred(address indexed previousOwner, address indexed newOwner);
/**
* @dev Initializes the contract setting `initialOwner` as the initial owner.
*/
constructor(address initialOwner) {
_owner = initialOwner;
emit OwnershipTransferred(address(0), initialOwner);
}
/**
* @dev Returns the address of the current owner.
*/
function owner() public view returns (address) {
return _owner;
}
/**
* @dev Throws if called by any account other than the owner.
*/
modifier onlyOwner() {
require(isOwner(), "Ownable: caller is not the owner");
_;
}
/**
* @dev Returns true if the caller is the current owner.
*/
function isOwner() public view returns (bool) {
return msg.sender == _owner;
}
/**
* @dev Leaves the contract without owner. It will not be possible to call
* `onlyOwner` functions anymore.
*
* NOTE: Renouncing ownership will leave the contract without an owner,
* thereby removing any functionality that is only available to the owner.
*
* NOTE: This function is not safe, as it doesn’t check owner is calling it.
* Make sure you check it before calling it.
*/
function _renounceOwnership() internal {
emit OwnershipTransferred(_owner, address(0));
_owner = address(0);
}
}
// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v5.0.0) (token/ERC20/IERC20.sol)
pragma solidity ^0.8.20;
/**
* @dev Interface of the ERC20 standard as defined in the EIP.
*/
interface IERC20 {
/**
* @dev Emitted when `value` tokens are moved from one account (`from`) to
* another (`to`).
*
* Note that `value` may be zero.
*/
event Transfer(address indexed from, address indexed to, uint256 value);
/**
* @dev Emitted when the allowance of a `spender` for an `owner` is set by
* a call to {approve}. `value` is the new allowance.
*/
event Approval(address indexed owner, address indexed spender, uint256 value);
/**
* @dev Returns the value of tokens in existence.
*/
function totalSupply() external view returns (uint256);
/**
* @dev Returns the value of tokens owned by `account`.
*/
function balanceOf(address account) external view returns (uint256);
/**
* @dev Moves a `value` amount of tokens from the caller's account to `to`.
*
* Returns a boolean value indicating whether the operation succeeded.
*
* Emits a {Transfer} event.
*/
function transfer(address to, uint256 value) external returns (bool);
/**
* @dev Returns the remaining number of tokens that `spender` will be
* allowed to spend on behalf of `owner` through {transferFrom}. This is
* zero by default.
*
* This value changes when {approve} or {transferFrom} are called.
*/
function allowance(address owner, address spender) external view returns (uint256);
/**
* @dev Sets a `value` amount of tokens as the allowance of `spender` over the
* caller's tokens.
*
* Returns a boolean value indicating whether the operation succeeded.
*
* IMPORTANT: Beware that changing an allowance with this method brings the risk
* that someone may use both the old and the new allowance by unfortunate
* transaction ordering. One possible solution to mitigate this race
* condition is to first reduce the spender's allowance to 0 and set the
* desired value afterwards:
* https://github.com/ethereum/EIPs/issues/20#issuecomment-263524729
*
* Emits an {Approval} event.
*/
function approve(address spender, uint256 value) external returns (bool);
/**
* @dev Moves a `value` amount of tokens from `from` to `to` using the
* allowance mechanism. `value` is then deducted from the caller's
* allowance.
*
* Returns a boolean value indicating whether the operation succeeded.
*
* Emits a {Transfer} event.
*/
function transferFrom(address from, address to, uint256 value) external returns (bool);
}
// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v5.0.0) (token/ERC20/extensions/IERC20Permit.sol)
pragma solidity ^0.8.20;
/**
* @dev Interface of the ERC20 Permit extension allowing approvals to be made via signatures, as defined in
* https://eips.ethereum.org/EIPS/eip-2612[EIP-2612].
*
* Adds the {permit} method, which can be used to change an account's ERC20 allowance (see {IERC20-allowance}) by
* presenting a message signed by the account. By not relying on {IERC20-approve}, the token holder account doesn't
* need to send a transaction, and thus is not required to hold Ether at all.
*
* ==== Security Considerations
*
* There are two important considerations concerning the use of `permit`. The first is that a valid permit signature
* expresses an allowance, and it should not be assumed to convey additional meaning. In particular, it should not be
* considered as an intention to spend the allowance in any specific way. The second is that because permits have
* built-in replay protection and can be submitted by anyone, they can be frontrun. A protocol that uses permits should
* take this into consideration and allow a `permit` call to fail. Combining these two aspects, a pattern that may be
* generally recommended is:
*
* ```solidity
* function doThingWithPermit(..., uint256 value, uint256 deadline, uint8 v, bytes32 r, bytes32 s) public {
* try token.permit(msg.sender, address(this), value, deadline, v, r, s) {} catch {}
* doThing(..., value);
* }
*
* function doThing(..., uint256 value) public {
* token.safeTransferFrom(msg.sender, address(this), value);
* ...
* }
* ```
*
* Observe that: 1) `msg.sender` is used as the owner, leaving no ambiguity as to the signer intent, and 2) the use of
* `try/catch` allows the permit to fail and makes the code tolerant to frontrunning. (See also
* {SafeERC20-safeTransferFrom}).
*
* Additionally, note that smart contract wallets (such as Argent or Safe) are not able to produce permit signatures, so
* contracts should have entry points that don't rely on permit.
*/
interface IERC20Permit {
/**
* @dev Sets `value` as the allowance of `spender` over ``owner``'s tokens,
* given ``owner``'s signed approval.
*
* IMPORTANT: The same issues {IERC20-approve} has related to transaction
* ordering also apply here.
*
* Emits an {Approval} event.
*
* Requirements:
*
* - `spender` cannot be the zero address.
* - `deadline` must be a timestamp in the future.
* - `v`, `r` and `s` must be a valid `secp256k1` signature from `owner`
* over the EIP712-formatted function arguments.
* - the signature must use ``owner``'s current nonce (see {nonces}).
*
* For more information on the signature format, see the
* https://eips.ethereum.org/EIPS/eip-2612#specification[relevant EIP
* section].
*
* CAUTION: See Security Considerations above.
*/
function permit(
address owner,
address spender,
uint256 value,
uint256 deadline,
uint8 v,
bytes32 r,
bytes32 s
) external;
/**
* @dev Returns the current nonce for `owner`. This value must be
* included whenever a signature is generated for {permit}.
*
* Every successful call to {permit} increases ``owner``'s nonce by one. This
* prevents a signature from being used multiple times.
*/
function nonces(address owner) external view returns (uint256);
/**
* @dev Returns the domain separator used in the encoding of the signature for {permit}, as defined by {EIP712}.
*/
// solhint-disable-next-line func-name-mixedcase
function DOMAIN_SEPARATOR() external view returns (bytes32);
}
// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v5.0.0) (utils/Address.sol)
pragma solidity ^0.8.20;
/**
* @dev Collection of functions related to the address type
*/
library Address {
/**
* @dev The ETH balance of the account is not enough to perform the operation.
*/
error AddressInsufficientBalance(address account);
/**
* @dev There's no code at `target` (it is not a contract).
*/
error AddressEmptyCode(address target);
/**
* @dev A call to an address target failed. The target may have reverted.
*/
error FailedInnerCall();
/**
* @dev Replacement for Solidity's `transfer`: sends `amount` wei to
* `recipient`, forwarding all available gas and reverting on errors.
*
* https://eips.ethereum.org/EIPS/eip-1884[EIP1884] increases the gas cost
* of certain opcodes, possibly making contracts go over the 2300 gas limit
* imposed by `transfer`, making them unable to receive funds via
* `transfer`. {sendValue} removes this limitation.
*
* https://consensys.net/diligence/blog/2019/09/stop-using-soliditys-transfer-now/[Learn more].
*
* IMPORTANT: because control is transferred to `recipient`, care must be
* taken to not create reentrancy vulnerabilities. Consider using
* {ReentrancyGuard} or the
* https://solidity.readthedocs.io/en/v0.8.20/security-considerations.html#use-the-checks-effects-interactions-pattern[checks-effects-interactions pattern].
*/
function sendValue(address payable recipient, uint256 amount) internal {
if (address(this).balance < amount) {
revert AddressInsufficientBalance(address(this));
}
(bool success, ) = recipient.call{value: amount}("");
if (!success) {
revert FailedInnerCall();
}
}
/**
* @dev Performs a Solidity function call using a low level `call`. A
* plain `call` is an unsafe replacement for a function call: use this
* function instead.
*
* If `target` reverts with a revert reason or custom error, it is bubbled
* up by this function (like regular Solidity function calls). However, if
* the call reverted with no returned reason, this function reverts with a
* {FailedInnerCall} error.
*
* Returns the raw returned data. To convert to the expected return value,
* use https://solidity.readthedocs.io/en/latest/units-and-global-variables.html?highlight=abi.decode#abi-encoding-and-decoding-functions[`abi.decode`].
*
* Requirements:
*
* - `target` must be a contract.
* - calling `target` with `data` must not revert.
*/
function functionCall(address target, bytes memory data) internal returns (bytes memory) {
return functionCallWithValue(target, data, 0);
}
/**
* @dev Same as {xref-Address-functionCall-address-bytes-}[`functionCall`],
* but also transferring `value` wei to `target`.
*
* Requirements:
*
* - the calling contract must have an ETH balance of at least `value`.
* - the called Solidity function must be `payable`.
*/
function functionCallWithValue(address target, bytes memory data, uint256 value) internal returns (bytes memory) {
if (address(this).balance < value) {
revert AddressInsufficientBalance(address(this));
}
(bool success, bytes memory returndata) = target.call{value: value}(data);
return verifyCallResultFromTarget(target, success, returndata);
}
/**
* @dev Same as {xref-Address-functionCall-address-bytes-}[`functionCall`],
* but performing a static call.
*/
function functionStaticCall(address target, bytes memory data) internal view returns (bytes memory) {
(bool success, bytes memory returndata) = target.staticcall(data);
return verifyCallResultFromTarget(target, success, returndata);
}
/**
* @dev Same as {xref-Address-functionCall-address-bytes-}[`functionCall`],
* but performing a delegate call.
*/
function functionDelegateCall(address target, bytes memory data) internal returns (bytes memory) {
(bool success, bytes memory returndata) = target.delegatecall(data);
return verifyCallResultFromTarget(target, success, returndata);
}
/**
* @dev Tool to verify that a low level call to smart-contract was successful, and reverts if the target
* was not a contract or bubbling up the revert reason (falling back to {FailedInnerCall}) in case of an
* unsuccessful call.
*/
function verifyCallResultFromTarget(
address target,
bool success,
bytes memory returndata
) internal view returns (bytes memory) {
if (!success) {
_revert(returndata);
} else {
// only check if target is a contract if the call was successful and the return data is empty
// otherwise we already know that it was a contract
if (returndata.length == 0 && target.code.length == 0) {
revert AddressEmptyCode(target);
}
return returndata;
}
}
/**
* @dev Tool to verify that a low level call was successful, and reverts if it wasn't, either by bubbling the
* revert reason or with a default {FailedInnerCall} error.
*/
function verifyCallResult(bool success, bytes memory returndata) internal pure returns (bytes memory) {
if (!success) {
_revert(returndata);
} else {
return returndata;
}
}
/**
* @dev Reverts with returndata if present. Otherwise reverts with {FailedInnerCall}.
*/
function _revert(bytes memory returndata) private pure {
// Look for revert reason and bubble it up if present
if (returndata.length > 0) {
// The easiest way to bubble the revert reason is using memory via assembly
/// @solidity memory-safe-assembly
assembly {
let returndata_size := mload(returndata)
revert(add(32, returndata), returndata_size)
}
} else {
revert FailedInnerCall();
}
}
}
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
import {IERC20} from "openzeppelin/contracts/interfaces/IERC20.sol";
import {ILQTYStaking} from "../interfaces/ILQTYStaking.sol";
import {PermitParams} from "../utils/Types.sol";
interface IUserProxy {
event Stake(uint256 amount, address lqtyFrom);
event Unstake(address indexed lqtyRecipient, uint256 lqtyReceived, uint256 lqtySent);
event SendRewards(
address indexed recipient,
uint256 lusdAmountReceived,
uint256 lusdAmountSent,
uint256 ethAmountReceived,
uint256 ethAmountSent
);
/// @notice Address of the LQTY token
/// @return lqty Address of the LQTY token
function lqty() external view returns (IERC20 lqty);
/// @notice Address of the LUSD token
/// @return lusd Address of the LUSD token
function lusd() external view returns (IERC20 lusd);
/// @notice Address of the V1 LQTY staking contract
/// @return stakingV1 Address of the V1 LQTY staking contract
function stakingV1() external view returns (ILQTYStaking stakingV1);
/// @notice Address of the V2 LQTY staking contract
/// @return stakingV2 Address of the V2 LQTY staking contract
function stakingV2() external view returns (address stakingV2);
/// @notice Stakes a given amount of LQTY tokens in the V1 staking contract
/// @dev The LQTY tokens must be approved for transfer by the user
/// @param _amount Amount of LQTY tokens to stake
/// @param _lqtyFrom Address from which to transfer the LQTY tokens
/// @param _doSendRewards If true, send rewards claimed from LQTY staking
/// @param _recipient Address to which the tokens should be sent
/// @return lusdAmount Amount of LUSD tokens claimed
/// @return ethAmount Amount of ETH claimed
function stake(uint256 _amount, address _lqtyFrom, bool _doSendRewards, address _recipient)
external
returns (uint256 lusdAmount, uint256 ethAmount);
/// @notice Stakes a given amount of LQTY tokens in the V1 staking contract using a permit
/// @param _amount Amount of LQTY tokens to stake
/// @param _lqtyFrom Address from which to transfer the LQTY tokens
/// @param _permitParams Parameters for the permit data
/// @param _doSendRewards If true, send rewards claimed from LQTY staking
/// @param _recipient Address to which the tokens should be sent
/// @return lusdAmount Amount of LUSD tokens claimed
/// @return ethAmount Amount of ETH claimed
function stakeViaPermit(
uint256 _amount,
address _lqtyFrom,
PermitParams calldata _permitParams,
bool _doSendRewards,
address _recipient
) external returns (uint256 lusdAmount, uint256 ethAmount);
/// @notice Unstakes a given amount of LQTY tokens from the V1 staking contract and claims the accrued rewards
/// @param _amount Amount of LQTY tokens to unstake
/// @param _doSendRewards If true, send rewards claimed from LQTY staking
/// @param _recipient Address to which the tokens should be sent
/// @return lusdAmount Amount of LUSD tokens claimed
/// @return ethAmount Amount of ETH claimed
function unstake(uint256 _amount, bool _doSendRewards, address _recipient)
external
returns (uint256 lusdAmount, uint256 ethAmount);
/// @notice Returns the current amount LQTY staked by a user in the V1 staking contract
/// @return staked Amount of LQTY tokens staked
function staked() external view returns (uint88);
}
// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v5.0.0) (proxy/Clones.sol)
pragma solidity ^0.8.20;
/**
* @dev https://eips.ethereum.org/EIPS/eip-1167[EIP 1167] is a standard for
* deploying minimal proxy contracts, also known as "clones".
*
* > To simply and cheaply clone contract functionality in an immutable way, this standard specifies
* > a minimal bytecode implementation that delegates all calls to a known, fixed address.
*
* The library includes functions to deploy a proxy using either `create` (traditional deployment) or `create2`
* (salted deterministic deployment). It also includes functions to predict the addresses of clones deployed using the
* deterministic method.
*/
library Clones {
/**
* @dev A clone instance deployment failed.
*/
error ERC1167FailedCreateClone();
/**
* @dev Deploys and returns the address of a clone that mimics the behaviour of `implementation`.
*
* This function uses the create opcode, which should never revert.
*/
function clone(address implementation) internal returns (address instance) {
/// @solidity memory-safe-assembly
assembly {
// Cleans the upper 96 bits of the `implementation` word, then packs the first 3 bytes
// of the `implementation` address with the bytecode before the address.
mstore(0x00, or(shr(0xe8, shl(0x60, implementation)), 0x3d602d80600a3d3981f3363d3d373d3d3d363d73000000))
// Packs the remaining 17 bytes of `implementation` with the bytecode after the address.
mstore(0x20, or(shl(0x78, implementation), 0x5af43d82803e903d91602b57fd5bf3))
instance := create(0, 0x09, 0x37)
}
if (instance == address(0)) {
revert ERC1167FailedCreateClone();
}
}
/**
* @dev Deploys and returns the address of a clone that mimics the behaviour of `implementation`.
*
* This function uses the create2 opcode and a `salt` to deterministically deploy
* the clone. Using the same `implementation` and `salt` multiple time will revert, since
* the clones cannot be deployed twice at the same address.
*/
function cloneDeterministic(address implementation, bytes32 salt) internal returns (address instance) {
/// @solidity memory-safe-assembly
assembly {
// Cleans the upper 96 bits of the `implementation` word, then packs the first 3 bytes
// of the `implementation` address with the bytecode before the address.
mstore(0x00, or(shr(0xe8, shl(0x60, implementation)), 0x3d602d80600a3d3981f3363d3d373d3d3d363d73000000))
// Packs the remaining 17 bytes of `implementation` with the bytecode after the address.
mstore(0x20, or(shl(0x78, implementation), 0x5af43d82803e903d91602b57fd5bf3))
instance := create2(0, 0x09, 0x37, salt)
}
if (instance == address(0)) {
revert ERC1167FailedCreateClone();
}
}
/**
* @dev Computes the address of a clone deployed using {Clones-cloneDeterministic}.
*/
function predictDeterministicAddress(
address implementation,
bytes32 salt,
address deployer
) internal pure returns (address predicted) {
/// @solidity memory-safe-assembly
assembly {
let ptr := mload(0x40)
mstore(add(ptr, 0x38), deployer)
mstore(add(ptr, 0x24), 0x5af43d82803e903d91602b57fd5bf3ff)
mstore(add(ptr, 0x14), implementation)
mstore(ptr, 0x3d602d80600a3d3981f3363d3d373d3d3d363d73)
mstore(add(ptr, 0x58), salt)
mstore(add(ptr, 0x78), keccak256(add(ptr, 0x0c), 0x37))
predicted := keccak256(add(ptr, 0x43), 0x55)
}
}
/**
* @dev Computes the address of a clone deployed using {Clones-cloneDeterministic}.
*/
function predictDeterministicAddress(
address implementation,
bytes32 salt
) internal view returns (address predicted) {
return predictDeterministicAddress(implementation, salt, address(this));
}
}
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
interface IUserProxyFactory {
event DeployUserProxy(address indexed user, address indexed userProxy);
/// @notice Address of the UserProxy implementation contract
/// @return implementation Address of the UserProxy implementation contract
function userProxyImplementation() external view returns (address implementation);
/// @notice Derive the address of a user's proxy contract
/// @param _user Address of the user
/// @return userProxyAddress Address of the user's proxy contract
function deriveUserProxyAddress(address _user) external view returns (address userProxyAddress);
/// @notice Deploy a new UserProxy contract for the sender
/// @return userProxyAddress Address of the deployed UserProxy contract
function deployUserProxy() external returns (address userProxyAddress);
}
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
/// Copied from: https://github.com/Uniswap/v3-periphery/blob/main/contracts/interfaces/IMulticall.sol
/// @title Multicall interface
/// @notice Enables calling multiple methods in a single call to the contract
interface IMulticall {
/// @notice Call multiple functions in the current contract and return the data from all of them if they all succeed
/// @dev The `msg.value` should not be trusted for any method callable from multicall.
/// @param data The encoded function data for each of the calls to make to this contract
/// @return results The results from each of the calls passed in via data
function multicall(bytes[] calldata data) external payable returns (bytes[] memory results);
}