Description:
Multi-signature wallet contract requiring multiple confirmations for transaction execution.
Blockchain: Ethereum
Source Code: View Code On The Blockchain
Solidity Source Code:
// SPDX-License-Identifier: MIT
pragma solidity =0.8.24;
contract MainnetLiquityV2Addresses {
address internal constant BOLD_ADDR = 0x6440f144b7e50D6a8439336510312d2F54beB01D;
address internal constant MULTI_TROVE_GETTER_ADDR = 0xFA61dB085510C64B83056Db3A7Acf3b6f631D235;
address internal constant WETH_MARKET_ADDR = 0x20F7C9ad66983F6523a0881d0f82406541417526;
address internal constant WSTETH_MARKET_ADDR = 0x8d733F7ea7c23Cbea7C613B6eBd845d46d3aAc54;
address internal constant RETH_MARKET_ADDR = 0x6106046F031a22713697e04C08B330dDaf3e8789;
}
contract DSMath {
function add(uint256 x, uint256 y) internal pure returns (uint256 z) {
z = x + y;
}
function sub(uint256 x, uint256 y) internal pure returns (uint256 z) {
z = x - y;
}
function mul(uint256 x, uint256 y) internal pure returns (uint256 z) {
z = x * y;
}
function div(uint256 x, uint256 y) internal pure returns (uint256 z) {
return x / y;
}
function min(uint256 x, uint256 y) internal pure returns (uint256 z) {
return x <= y ? x : y;
}
function max(uint256 x, uint256 y) internal pure returns (uint256 z) {
return x >= y ? x : y;
}
function imin(int256 x, int256 y) internal pure returns (int256 z) {
return x <= y ? x : y;
}
function imax(int256 x, int256 y) internal pure returns (int256 z) {
return x >= y ? x : y;
}
uint256 constant WAD = 10**18;
uint256 constant RAY = 10**27;
function wmul(uint256 x, uint256 y) internal pure returns (uint256 z) {
z = add(mul(x, y), WAD / 2) / WAD;
}
function rmul(uint256 x, uint256 y) internal pure returns (uint256 z) {
z = add(mul(x, y), RAY / 2) / RAY;
}
function wdiv(uint256 x, uint256 y) internal pure returns (uint256 z) {
z = add(mul(x, WAD), y / 2) / y;
}
function rdiv(uint256 x, uint256 y) internal pure returns (uint256 z) {
z = add(mul(x, RAY), y / 2) / y;
}
// This famous algorithm is called "exponentiation by squaring"
// and calculates x^n with x as fixed-point and n as regular unsigned.
//
// It's O(log n), instead of O(n) for naive repeated multiplication.
//
// These facts are why it works:
//
// If n is even, then x^n = (x^2)^(n/2).
// If n is odd, then x^n = x * x^(n-1),
// and applying the equation for even x gives
// x^n = x * (x^2)^((n-1) / 2).
//
// Also, EVM division is flooring and
// floor[(n-1) / 2] = floor[n / 2].
//
function rpow(uint256 x, uint256 n) internal pure returns (uint256 z) {
z = n % 2 != 0 ? x : RAY;
for (n /= 2; n != 0; n /= 2) {
x = rmul(x, x);
if (n % 2 != 0) {
z = rmul(z, x);
}
}
}
}
interface IAddressesRegistry {
function CCR() external view returns (uint256);
function SCR() external view returns (uint256);
function MCR() external view returns (uint256);
function BCR() external view returns (uint256);
function LIQUIDATION_PENALTY_SP() external view returns (uint256);
function LIQUIDATION_PENALTY_REDISTRIBUTION() external view returns (uint256);
function WETH() external view returns (address);
function troveNFT() external view returns (address);
function collToken() external view returns (address);
function boldToken() external view returns (address);
function borrowerOperations() external view returns (address);
function troveManager() external view returns (address);
function stabilityPool() external view returns (address);
function activePool() external view returns (address);
function defaultPool() external view returns (address);
function sortedTroves() external view returns (address);
function collSurplusPool() external view returns (address);
function hintHelpers() external view returns (address);
function priceFeed() external view returns (address);
function gasPoolAddress() external view returns (address);
}
interface IBorrowerOperations {
function CCR() external view returns (uint256);
function MCR() external view returns (uint256);
function SCR() external view returns (uint256);
function openTrove(
address _owner,
uint256 _ownerIndex,
uint256 _collAmount,
uint256 _boldAmount,
uint256 _upperHint,
uint256 _lowerHint,
uint256 _annualInterestRate,
uint256 _maxUpfrontFee,
address _addManager,
address _removeManager,
address _receiver
) external returns (uint256);
struct OpenTroveAndJoinInterestBatchManagerParams {
address owner;
uint256 ownerIndex;
uint256 collAmount;
uint256 boldAmount;
uint256 upperHint;
uint256 lowerHint;
address interestBatchManager;
uint256 maxUpfrontFee;
address addManager;
address removeManager;
address receiver;
}
function openTroveAndJoinInterestBatchManager(OpenTroveAndJoinInterestBatchManagerParams calldata _params)
external
returns (uint256);
function addColl(uint256 _troveId, uint256 _ETHAmount) external;
function withdrawColl(uint256 _troveId, uint256 _amount) external;
function withdrawBold(uint256 _troveId, uint256 _amount, uint256 _maxUpfrontFee) external;
function repayBold(uint256 _troveId, uint256 _amount) external;
function closeTrove(uint256 _troveId) external;
function adjustTrove(
uint256 _troveId,
uint256 _collChange,
bool _isCollIncrease,
uint256 _debtChange,
bool isDebtIncrease,
uint256 _maxUpfrontFee
) external;
function adjustZombieTrove(
uint256 _troveId,
uint256 _collChange,
bool _isCollIncrease,
uint256 _boldChange,
bool _isDebtIncrease,
uint256 _upperHint,
uint256 _lowerHint,
uint256 _maxUpfrontFee
) external;
function adjustTroveInterestRate(
uint256 _troveId,
uint256 _newAnnualInterestRate,
uint256 _upperHint,
uint256 _lowerHint,
uint256 _maxUpfrontFee
) external;
function applyPendingDebt(uint256 _troveId, uint256 _lowerHint, uint256 _upperHint) external;
function onLiquidateTrove(uint256 _troveId) external;
function claimCollateral() external;
function hasBeenShutDown() external view returns (bool);
function shutdown() external;
function shutdownFromOracleFailure(address _failedOracleAddr) external;
function checkBatchManagerExists(address _batchMananger) external view returns (bool);
// -- individual delegation --
struct InterestIndividualDelegate {
address account;
uint128 minInterestRate;
uint128 maxInterestRate;
}
function getInterestIndividualDelegateOf(uint256 _troveId)
external
view
returns (InterestIndividualDelegate memory);
function setInterestIndividualDelegate(
uint256 _troveId,
address _delegate,
uint128 _minInterestRate,
uint128 _maxInterestRate,
// only needed if trove was previously in a batch:
uint256 _newAnnualInterestRate,
uint256 _upperHint,
uint256 _lowerHint,
uint256 _maxUpfrontFee
) external;
function removeInterestIndividualDelegate(uint256 _troveId) external;
// -- batches --
struct InterestBatchManager {
uint128 minInterestRate;
uint128 maxInterestRate;
uint256 minInterestRateChangePeriod;
}
function registerBatchManager(
uint128 minInterestRate,
uint128 maxInterestRate,
uint128 currentInterestRate,
uint128 fee,
uint128 minInterestRateChangePeriod
) external;
function lowerBatchManagementFee(uint256 _newAnnualFee) external;
function setBatchManagerAnnualInterestRate(
uint128 _newAnnualInterestRate,
uint256 _upperHint,
uint256 _lowerHint,
uint256 _maxUpfrontFee
) external;
function interestBatchManagerOf(uint256 _troveId) external view returns (address);
function getInterestBatchManager(address _account) external view returns (InterestBatchManager memory);
function setInterestBatchManager(
uint256 _troveId,
address _newBatchManager,
uint256 _upperHint,
uint256 _lowerHint,
uint256 _maxUpfrontFee
) external;
function removeFromBatch(
uint256 _troveId,
uint256 _newAnnualInterestRate,
uint256 _upperHint,
uint256 _lowerHint,
uint256 _maxUpfrontFee
) external;
function switchBatchManager(
uint256 _troveId,
uint256 _removeUpperHint,
uint256 _removeLowerHint,
address _newBatchManager,
uint256 _addUpperHint,
uint256 _addLowerHint,
uint256 _maxUpfrontFee
) external;
function getEntireBranchColl() external view returns (uint);
function getEntireBranchDebt() external view returns (uint);
}
interface ISortedTroves {
// -- Mutating functions (permissioned) --
function insert(uint256 _id, uint256 _annualInterestRate, uint256 _prevId, uint256 _nextId) external;
function insertIntoBatch(
uint256 _troveId,
address _batchId,
uint256 _annualInterestRate,
uint256 _prevId,
uint256 _nextId
) external;
function remove(uint256 _id) external;
function removeFromBatch(uint256 _id) external;
function reInsert(uint256 _id, uint256 _newAnnualInterestRate, uint256 _prevId, uint256 _nextId) external;
function reInsertBatch(address _id, uint256 _newAnnualInterestRate, uint256 _prevId, uint256 _nextId) external;
// -- View functions --
function contains(uint256 _id) external view returns (bool);
function isBatchedNode(uint256 _id) external view returns (bool);
function isEmptyBatch(address _id) external view returns (bool);
function isEmpty() external view returns (bool);
function getSize() external view returns (uint256);
function getFirst() external view returns (uint256);
function getLast() external view returns (uint256);
function getNext(uint256 _id) external view returns (uint256);
function getPrev(uint256 _id) external view returns (uint256);
function validInsertPosition(uint256 _annualInterestRate, uint256 _prevId, uint256 _nextId)
external
view
returns (bool);
function findInsertPosition(uint256 _annualInterestRate, uint256 _prevId, uint256 _nextId)
external
view
returns (uint256, uint256);
// Public state variable getters
function size() external view returns (uint256);
function nodes(uint256 _id) external view returns (uint256 nextId, uint256 prevId, address batchId, bool exists);
function batches(address _id) external view returns (uint256 head, uint256 tail);
}
interface IStabilityPool {
/* provideToSP():
* - Calculates depositor's Coll gain
* - Calculates the compounded deposit
* - Increases deposit, and takes new snapshots of accumulators P and S
* - Sends depositor's accumulated Coll gains to depositor
*/
function provideToSP(uint256 _amount, bool _doClaim) external;
/* withdrawFromSP():
* - Calculates depositor's Coll gain
* - Calculates the compounded deposit
* - Sends the requested BOLD withdrawal to depositor
* - (If _amount > userDeposit, the user withdraws all of their compounded deposit)
* - Decreases deposit by withdrawn amount and takes new snapshots of accumulators P and S
*/
function withdrawFromSP(uint256 _amount, bool doClaim) external;
function claimAllCollGains() external;
/*
* Initial checks:
* - Caller is TroveManager
* ---
* Cancels out the specified debt against the Bold contained in the Stability Pool (as far as possible)
* and transfers the Trove's collateral from ActivePool to StabilityPool.
* Only called by liquidation functions in the TroveManager.
*/
function offset(uint256 _debt, uint256 _coll) external;
function deposits(address _depositor) external view returns (uint256 initialValue);
function stashedColl(address _depositor) external view returns (uint256);
/*
* Returns the total amount of Coll held by the pool, accounted in an internal variable instead of `balance`,
* to exclude edge cases like Coll received from a self-destruct.
*/
function getCollBalance() external view returns (uint256);
/*
* Returns Bold held in the pool. Changes when users deposit/withdraw, and when Trove debt is offset.
*/
function getTotalBoldDeposits() external view returns (uint256);
function getYieldGainsOwed() external view returns (uint256);
function getYieldGainsPending() external view returns (uint256);
/*
* Calculates the Coll gain earned by the deposit since its last snapshots were taken.
*/
function getDepositorCollGain(address _depositor) external view returns (uint256);
/*
* Calculates the BOLD yield gain earned by the deposit since its last snapshots were taken.
*/
function getDepositorYieldGain(address _depositor) external view returns (uint256);
/*
* Calculates what `getDepositorYieldGain` will be if interest is minted now.
*/
function getDepositorYieldGainWithPending(address _depositor) external view returns (uint256);
/*
* Return the user's compounded deposit.
*/
function getCompoundedBoldDeposit(address _depositor) external view returns (uint256);
function epochToScaleToS(uint128 _epoch, uint128 _scale) external view returns (uint256);
function epochToScaleToB(uint128 _epoch, uint128 _scale) external view returns (uint256);
function P() external view returns (uint256);
function currentScale() external view returns (uint128);
function currentEpoch() external view returns (uint128);
}
interface ITroveManager {
enum Status {
nonExistent,
active,
closedByOwner,
closedByLiquidation,
zombie
}
struct LatestTroveData {
uint256 entireDebt;
uint256 entireColl;
uint256 redistBoldDebtGain;
uint256 redistCollGain;
uint256 accruedInterest;
uint256 recordedDebt;
uint256 annualInterestRate;
uint256 weightedRecordedDebt;
uint256 accruedBatchManagementFee;
uint256 lastInterestRateAdjTime;
}
struct LatestBatchData {
uint256 entireDebtWithoutRedistribution;
uint256 entireCollWithoutRedistribution;
uint256 accruedInterest;
uint256 recordedDebt;
uint256 annualInterestRate;
uint256 weightedRecordedDebt;
uint256 annualManagementFee;
uint256 accruedManagementFee;
uint256 weightedRecordedBatchManagementFee;
uint256 lastDebtUpdateTime;
uint256 lastInterestRateAdjTime;
}
function Troves(uint256 _id)
external
view
returns (
uint256 debt,
uint256 coll,
uint256 stake,
Status status,
uint64 arrayIndex,
uint64 lastDebtUpdateTime,
uint64 lastInterestRateAdjTime,
uint256 annualInterestRate,
address interestBatchManager,
uint256 batchDebtShares
);
function shutdownTime() external view returns (uint256);
function troveNFT() external view returns (address);
function getLatestTroveData(uint256 _troveId) external view returns (LatestTroveData memory);
function getCurrentICR(uint256 _troveId, uint256 _price) external view returns (uint256);
function getTroveStatus(uint256 _troveId) external view returns (Status);
function getTroveAnnualInterestRate(uint256 _troveId) external view returns (uint256);
function getLatestBatchData(address _batchAddress) external view returns (LatestBatchData memory);
}
contract LiquityV2Helper is MainnetLiquityV2Addresses, DSMath {
/// @notice Cooldown period for interest rate adjustments (7 days)
uint256 constant INTEREST_RATE_ADJ_COOLDOWN = 7 days;
/// @notice Maximum number of iterations to get the debt in front for a current trove branch
uint256 internal constant MAX_ITERATIONS = 1000;
// Amount of ETH to be locked in gas pool on opening troves
uint256 constant ETH_GAS_COMPENSATION = 0.0375 ether;
// Minimum amount of net Bold debt a trove must have
uint256 constant MIN_DEBT = 2000e18;
// collateral indexes for different branches (markets)
uint256 constant WETH_COLL_INDEX = 0;
uint256 constant WSTETH_COLL_INDEX = 1;
uint256 constant RETH_COLL_INDEX = 2;
/// @notice Error thrown when an invalid market address is provided
error InvalidMarketAddress();
/// @notice Helper struct containing the total debt and unbacked debt of a single market
/// @dev totalDebt is the total bold debt of the market
/// @dev unbackedDebt is the unbacked bold debt of the market. Diff between total debt and stability pool bold deposits
struct Market {
uint256 totalDebt;
uint256 unbackedDebt;
}
/// @notice Helper struct containing the current market and other markets data.
/// @notice Used for estimating the redemption amounts per market
/// @dev current is the current market we are calculating the debt in front for
/// @dev otherMarkets are the 2 other markets
struct Markets {
Market current;
Market[] otherMarkets;
}
/// @notice Gets the debt in front for a given market and trove
/// @param _market address of the market (a.k.a. branch)
/// @param _trove id of the trove
/// @return debtInFront debt in front of the trove
/// @dev This function estimates the total real debt in front of a given trove.
/// Because redemptions are routed through every branch, the total debt in front
/// is usually higher than the debt of the troves preceding the current trove in its given branch.
/// General equation:
/// X * branchRedeemPercentage = branchDebtInFront
/// X * (totalDebtOrUnbackedDebtOnBranch / totalDebtOrUnbackedDebt) = branchDebtInFront
/// X = branchDebtInFront * totalDebtOrUnbackedDebt / totalDebtOrUnbackedDebtOnBranch
/// Where X is the estimated redemption amount for which all debt in front of the trove in its branch will be redeemed.
function getDebtInFront(
address _market,
uint256 _trove
) public view returns (uint256) {
(uint256 ethTotalDebt, uint256 ethUnbackedDebt) = _getTotalAndUnbackedDebt(WETH_MARKET_ADDR);
(uint256 wstEthTotalDebt, uint256 wstEthUnbackedDebt) = _getTotalAndUnbackedDebt(WSTETH_MARKET_ADDR);
(uint256 rEthTotalDebt, uint256 rEthUnbackedDebt) = _getTotalAndUnbackedDebt(RETH_MARKET_ADDR);
uint256 totalUnbackedDebt = ethUnbackedDebt + wstEthUnbackedDebt + rEthUnbackedDebt;
uint256 totalDebt = ethTotalDebt + wstEthTotalDebt + rEthTotalDebt;
uint256 branchDebtInFront = _getTroveDebtInFrontForCurrentBranch(_market, _trove);
Markets memory markets = _getMarketsData(
_market,
ethTotalDebt,
ethUnbackedDebt,
wstEthTotalDebt,
wstEthUnbackedDebt,
rEthTotalDebt,
rEthUnbackedDebt
);
// Sanity check to avoid division by 0. Highly unlikely to ever happen.
if (markets.current.totalDebt == 0) return 0;
// CASE 1: Current branch has 0 unbacked debt
// When totalUnbackedDebt is 0, redemptions will be proportional with the branch size and not to unbacked debt.
// When unbacked debt is 0 for some branch, next redemption call won't touch that branch, so in order to estimate total debt in front we will:
// - First add up all the unbacked debt from other branches, as that will be the only debt that will be redeemed on the fist redemption call
// - Perform split the same way as we do when totalUnbackedDebt == 0, this would represent the second call to the redemption function
if (markets.current.unbackedDebt == 0) {
// If the branch debt in front is 0, it means that all debt in front is unbacked debt from other branches.
if (branchDebtInFront == 0) {
return markets.otherMarkets[0].unbackedDebt + markets.otherMarkets[1].unbackedDebt;
}
// 1. First redemption call:
// - add up all the unbacked debt from other branches
// - remove the unbacked debt from total debt as it will be redeemed on the first call
// - update the total debt of other branches
uint256 redeemAmountFromFirstCall = markets.otherMarkets[0].unbackedDebt + markets.otherMarkets[1].unbackedDebt;
totalDebt -= redeemAmountFromFirstCall;
markets.otherMarkets[0].totalDebt -= markets.otherMarkets[0].unbackedDebt;
markets.otherMarkets[1].totalDebt -= markets.otherMarkets[1].unbackedDebt;
// 2. Second redemption call:
// Perform the split by total debt because there is no more unbacked debt to redeem
uint256 estimatedRedemptionAmount = branchDebtInFront * totalDebt / markets.current.totalDebt;
uint256[] memory redemptionAmounts = _calculateRedemptionAmounts(
estimatedRedemptionAmount,
totalDebt,
markets,
false // isTotalUnbacked = false. Proportional to total debt
);
uint256 redeemAmountFromSecondCall = branchDebtInFront + redemptionAmounts[0] + redemptionAmounts[1];
return redeemAmountFromFirstCall + redeemAmountFromSecondCall;
}
// CASE 2: Current branch has unbacked debt
uint256 estimatedRedemptionAmount = branchDebtInFront * totalUnbackedDebt / markets.current.unbackedDebt;
uint256[] memory redemptionAmounts = _calculateRedemptionAmounts(
estimatedRedemptionAmount,
totalUnbackedDebt,
markets,
true // isTotalUnbacked = true. Proportional to total unbacked debt
);
return branchDebtInFront + redemptionAmounts[0] + redemptionAmounts[1];
}
/*//////////////////////////////////////////////////////////////
INTERNAL FUNCTIONS
//////////////////////////////////////////////////////////////*/
function _calculateRedemptionAmounts(
uint256 _estimatedRedemptionAmount,
uint256 _total,
Markets memory _markets,
bool _isTotalUnbacked
) internal pure returns (uint256[] memory redemptionAmounts) {
redemptionAmounts = new uint256[](2);
for (uint256 i = 0; i < 2; ++i) {
uint256 branchProportion = _isTotalUnbacked
? _markets.otherMarkets[i].unbackedDebt
: _markets.otherMarkets[i].totalDebt;
redemptionAmounts[i] = min(
branchProportion * _estimatedRedemptionAmount / _total,
_markets.otherMarkets[i].totalDebt
);
}
}
function _getTotalAndUnbackedDebt(
address _market
) internal view returns (uint256 branchDebt, uint256 unbackedBold) {
IAddressesRegistry registry = IAddressesRegistry(_market);
branchDebt = IBorrowerOperations(registry.borrowerOperations()).getEntireBranchDebt();
uint256 boldDeposits = IStabilityPool(registry.stabilityPool()).getTotalBoldDeposits();
unbackedBold = branchDebt > boldDeposits ? branchDebt - boldDeposits : 0;
}
function _getTroveDebtInFrontForCurrentBranch(
address _market,
uint256 _troveId
) public view returns (uint256 debt) {
ITroveManager troveManager = ITroveManager(IAddressesRegistry(_market).troveManager());
ISortedTroves sortedTroves = ISortedTroves(IAddressesRegistry(_market).sortedTroves());
uint256 next = _troveId;
for (uint256 i = 0; i < MAX_ITERATIONS; ++i) {
next = sortedTroves.getNext(next);
if (next == 0) return debt;
debt += _getTroveTotalDebt(troveManager, next);
}
}
function _getTroveTotalDebt(
ITroveManager _troveManager,
uint256 _troveId
) internal view returns (uint256 debt) {
ITroveManager.LatestTroveData memory latestTroveData = _troveManager.getLatestTroveData(_troveId);
debt = latestTroveData.entireDebt;
}
function _getMarketsData(
address _currentMarket,
uint256 _ethTotalDebt,
uint256 _ethUnbackedDebt,
uint256 _wstEthTotalDebt,
uint256 _wstEthUnbackedDebt,
uint256 _rEthTotalDebt,
uint256 _rEthUnbackedDebt
) internal pure returns (Markets memory retVal) {
if (_currentMarket == WETH_MARKET_ADDR) {
retVal.current = Market(_ethTotalDebt, _ethUnbackedDebt);
retVal.otherMarkets = new Market[](2);
retVal.otherMarkets[0] = Market(_wstEthTotalDebt, _wstEthUnbackedDebt);
retVal.otherMarkets[1] = Market(_rEthTotalDebt, _rEthUnbackedDebt);
} else if (_currentMarket == WSTETH_MARKET_ADDR) {
retVal.current = Market(_wstEthTotalDebt, _wstEthUnbackedDebt);
retVal.otherMarkets = new Market[](2);
retVal.otherMarkets[0] = Market(_ethTotalDebt, _ethUnbackedDebt);
retVal.otherMarkets[1] = Market(_rEthTotalDebt, _rEthUnbackedDebt);
} else if (_currentMarket == RETH_MARKET_ADDR) {
retVal.current = Market(_rEthTotalDebt, _rEthUnbackedDebt);
retVal.otherMarkets = new Market[](2);
retVal.otherMarkets[0] = Market(_ethTotalDebt, _ethUnbackedDebt);
retVal.otherMarkets[1] = Market(_wstEthTotalDebt, _wstEthUnbackedDebt);
} else {
revert InvalidMarketAddress();
}
}
}
contract MainnetAuthAddresses {
address internal constant ADMIN_VAULT_ADDR = 0xCCf3d848e08b94478Ed8f46fFead3008faF581fD;
address internal constant DSGUARD_FACTORY_ADDRESS = 0x5a15566417e6C1c9546523066500bDDBc53F88C7;
address internal constant ADMIN_ADDR = 0x25eFA336886C74eA8E282ac466BdCd0199f85BB9; // USED IN ADMIN VAULT CONSTRUCTOR
address internal constant PROXY_AUTH_ADDRESS = 0x149667b6FAe2c63D1B4317C716b0D0e4d3E2bD70;
address internal constant MODULE_AUTH_ADDRESS = 0x7407974DDBF539e552F1d051e44573090912CC3D;
}
contract AuthHelper is MainnetAuthAddresses {
}
contract AdminVault is AuthHelper {
address public owner;
address public admin;
error SenderNotAdmin();
constructor() {
owner = msg.sender;
admin = ADMIN_ADDR;
}
/// @notice Admin is able to change owner
/// @param _owner Address of new owner
function changeOwner(address _owner) public {
if (admin != msg.sender){
revert SenderNotAdmin();
}
owner = _owner;
}
/// @notice Admin is able to set new admin
/// @param _admin Address of multisig that becomes new admin
function changeAdmin(address _admin) public {
if (admin != msg.sender){
revert SenderNotAdmin();
}
admin = _admin;
}
}
interface IERC20 {
function name() external view returns (string memory);
function symbol() external view returns (string memory);
function decimals() external view returns (uint256 digits);
function totalSupply() external view returns (uint256 supply);
function balanceOf(address _owner) external view returns (uint256 balance);
function transfer(address _to, uint256 _value) external returns (bool success);
function transferFrom(
address _from,
address _to,
uint256 _value
) external returns (bool success);
function approve(address _spender, uint256 _value) external returns (bool success);
function allowance(address _owner, address _spender) external view returns (uint256 remaining);
event Approval(address indexed _owner, address indexed _spender, uint256 _value);
}
library Address {
//insufficient balance
error InsufficientBalance(uint256 available, uint256 required);
//unable to send value, recipient may have reverted
error SendingValueFail();
//insufficient balance for call
error InsufficientBalanceForCall(uint256 available, uint256 required);
//call to non-contract
error NonContractCall();
function isContract(address account) internal view returns (bool) {
// According to EIP-1052, 0x0 is the value returned for not-yet created accounts
// and 0xc5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470 is returned
// for accounts without code, i.e. `keccak256('')`
bytes32 codehash;
bytes32 accountHash = 0xc5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470;
// solhint-disable-next-line no-inline-assembly
assembly {
codehash := extcodehash(account)
}
return (codehash != accountHash && codehash != 0x0);
}
function sendValue(address payable recipient, uint256 amount) internal {
uint256 balance = address(this).balance;
if (balance < amount){
revert InsufficientBalance(balance, amount);
}
// solhint-disable-next-line avoid-low-level-calls, avoid-call-value
(bool success, ) = recipient.call{value: amount}("");
if (!(success)){
revert SendingValueFail();
}
}
function functionCall(address target, bytes memory data) internal returns (bytes memory) {
return functionCall(target, data, "Address: low-level call failed");
}
function functionCall(
address target,
bytes memory data,
string memory errorMessage
) internal returns (bytes memory) {
return _functionCallWithValue(target, data, 0, errorMessage);
}
function functionCallWithValue(
address target,
bytes memory data,
uint256 value
) internal returns (bytes memory) {
return
functionCallWithValue(target, data, value, "Address: low-level call with value failed");
}
function functionCallWithValue(
address target,
bytes memory data,
uint256 value,
string memory errorMessage
) internal returns (bytes memory) {
uint256 balance = address(this).balance;
if (balance < value){
revert InsufficientBalanceForCall(balance, value);
}
return _functionCallWithValue(target, data, value, errorMessage);
}
function _functionCallWithValue(
address target,
bytes memory data,
uint256 weiValue,
string memory errorMessage
) private returns (bytes memory) {
if (!(isContract(target))){
revert NonContractCall();
}
// solhint-disable-next-line avoid-low-level-calls
(bool success, bytes memory returndata) = target.call{value: weiValue}(data);
if (success) {
return returndata;
} else {
// 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
// solhint-disable-next-line no-inline-assembly
assembly {
let returndata_size := mload(returndata)
revert(add(32, returndata), returndata_size)
}
} else {
revert(errorMessage);
}
}
}
}
library SafeERC20 {
using Address for address;
/**
* @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.encodeWithSelector(token.transfer.selector, 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.encodeWithSelector(token.transferFrom.selector, from, to, value));
}
/**
* @dev Set the calling contract's allowance toward `spender` to `value`. If `token` returns no value,
* non-reverting calls are assumed to be successful. Compatible with tokens that require the approval to be set to
* 0 before setting it to a non-zero value.
*/
function safeApprove(IERC20 token, address spender, uint256 value) internal {
bytes memory approvalCall = abi.encodeWithSelector(token.approve.selector, spender, value);
if (!_callOptionalReturnBool(token, approvalCall)) {
_callOptionalReturn(token, abi.encodeWithSelector(token.approve.selector, 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, "SafeERC20: low-level call failed");
require(returndata.length == 0 || abi.decode(returndata, (bool)), "SafeERC20: ERC20 operation did not succeed");
}
/**
* @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;
}
}
contract AdminAuth is AuthHelper {
using SafeERC20 for IERC20;
AdminVault public constant adminVault = AdminVault(ADMIN_VAULT_ADDR);
error SenderNotOwner();
error SenderNotAdmin();
modifier onlyOwner() {
if (adminVault.owner() != msg.sender){
revert SenderNotOwner();
}
_;
}
modifier onlyAdmin() {
if (adminVault.admin() != msg.sender){
revert SenderNotAdmin();
}
_;
}
/// @notice withdraw stuck funds
function withdrawStuckFunds(address _token, address _receiver, uint256 _amount) public onlyOwner {
if (_token == 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE) {
payable(_receiver).transfer(_amount);
} else {
IERC20(_token).safeTransfer(_receiver, _amount);
}
}
/// @notice Destroy the contract
/// @dev Deprecated method, selfdestruct will soon just send eth
function kill() public onlyAdmin {
selfdestruct(payable(msg.sender));
}
}
contract MainnetTriggerAddresses {
address public constant UNISWAP_V3_NONFUNGIBLE_POSITION_MANAGER = 0xC36442b4a4522E871399CD717aBDD847Ab11FE88;
address public constant UNISWAP_V3_FACTORY = 0x1F98431c8aD98523631AE4a59f267346ea31F984;
address public constant MCD_PRICE_VERIFIER = 0xeAa474cbFFA87Ae0F1a6f68a3aBA6C77C656F72c;
address public constant TRANSIENT_STORAGE = 0x2F7Ef2ea5E8c97B8687CA703A0e50Aa5a49B7eb2;
address public constant TRANSIENT_STORAGE_CANCUN = 0x0304E27cccE28bAB4d78C6cb7AfD4cd01c87c1e4;
}
contract TriggerHelper is MainnetTriggerAddresses {
}
abstract contract ITrigger {
function isTriggered(bytes memory, bytes memory) public virtual returns (bool);
function isChangeable() public virtual returns (bool);
function changedSubData(bytes memory) public virtual returns (bytes memory);
}
contract TransientStorageCancun {
/// @notice Stores a bytes32 value under a given string key
/// @dev Uses keccak256(_key) as the transient storage slot
function setBytes32(string memory _key, bytes32 _value) public {
bytes32 slot = keccak256(abi.encode(_key));
assembly {
tstore(slot, _value)
}
}
/// @notice Reads a bytes32 value previously stored with the given key
/// @dev Only valid in the same tx that `setBytes32` was called
function getBytes32(string memory _key) public view returns (bytes32 value) {
bytes32 slot = keccak256(abi.encode(_key));
assembly {
value := tload(slot)
}
}
}
contract LiquityV2AdjustRateDebtInFrontTrigger is
ITrigger,
AdminAuth,
TriggerHelper,
LiquityV2Helper
{
/// @notice Transient storage contract for storing temporary data during execution
TransientStorageCancun public constant tempStorage = TransientStorageCancun(TRANSIENT_STORAGE_CANCUN);
/// @notice Parameters for the LiquityV2 interest rate adjustment trigger
/// @param market Address of the LiquityV2 market (branch) to monitor
/// @param troveId ID of the trove to monitor for debt in front
/// @param criticalDebtInFrontLimit Critical threshold - strategy executes when debt in front is below this limit
/// @param nonCriticalDebtInFrontLimit Non-critical threshold - strategy executes when debt in front is below this limit AND adjustment fee is zero
struct SubParams {
address market;
uint256 troveId;
uint256 criticalDebtInFrontLimit;
uint256 nonCriticalDebtInFrontLimit;
}
/// @notice Checks if the debt in front of the trove is below the specified thresholds
/// @dev This function determines whether to trigger the interest rate adjustment strategy based on:
/// 1. Whether the trove is eligible for interest rate adjustment (active, no batch manager, cooldown passed)
/// 2. Whether the debt in front is below the critical or non-critical thresholds
/// 3. Whether adjustment fees are zero (affects which threshold to use)
/// @param _subData Encoded SubParams struct containing market, troveId, and threshold limits
/// @return bool True if the strategy should be triggered, false otherwise
function isTriggered(bytes memory, bytes memory _subData) public override returns (bool) {
SubParams memory triggerSubData = parseSubInputs(_subData);
(bool isAdjustmentFeeZero, uint256 interestRate, bool shouldExecuteStrategy) = getAdjustmentFeeAndInterestRate(
triggerSubData.market,
triggerSubData.troveId
);
if (!shouldExecuteStrategy) {
return false;
}
uint256 debtInFront = getDebtInFront(triggerSubData.market, triggerSubData.troveId);
tempStorage.setBytes32("LIQUITY_V2_INTEREST_RATE", bytes32(interestRate));
if (isAdjustmentFeeZero) {
return debtInFront < triggerSubData.nonCriticalDebtInFrontLimit;
} else {
return debtInFront < triggerSubData.criticalDebtInFrontLimit;
}
}
/// @notice Checks if the trove is eligible for interest rate adjustment and gets current interest rate
/// @dev Validates that the trove is active, has no batch manager, and checks if cooldown period has passed
/// @param _market Address of the LiquityV2 market
/// @param _troveId ID of the trove to check
/// @return adjustmentFeeZero True if adjustment fee is zero (cooldown period has passed)
/// @return interestRate Current annual interest rate of the trove
/// @return shouldExecuteStrategy True if the trove is eligible for interest rate adjustment
function getAdjustmentFeeAndInterestRate(
address _market,
uint256 _troveId
) internal view returns (bool adjustmentFeeZero, uint256 interestRate, bool shouldExecuteStrategy) {
IAddressesRegistry market = IAddressesRegistry(_market);
ITroveManager troveManager = ITroveManager(market.troveManager());
IBorrowerOperations borrowerOperations = IBorrowerOperations(market.borrowerOperations());
// return false if trove has an interest batch manager
if (borrowerOperations.interestBatchManagerOf(_troveId) != address(0)) {
return (false, 0, false);
}
// return false if trove is not active
if (troveManager.getTroveStatus(_troveId) != ITroveManager.Status.active) {
return (false, 0, false);
}
ITroveManager.LatestTroveData memory troveData = troveManager.getLatestTroveData(_troveId);
adjustmentFeeZero = block.timestamp >= troveData.lastInterestRateAdjTime + INTEREST_RATE_ADJ_COOLDOWN;
interestRate = troveData.annualInterestRate;
shouldExecuteStrategy = true;
}
function parseSubInputs(bytes memory _subData) public pure returns (SubParams memory params) {
params = abi.decode(_subData, (SubParams));
}
function changedSubData(bytes memory _subData) public pure override returns (bytes memory) {}
function isChangeable() public pure override returns (bool) {
return false;
}
}
Submitted on: 2025-09-17 14:47:32
Comments
Log in to comment.
No comments yet.