Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,8 @@ TENDERLY_PROJECT=
TENDERLY_USERNAME=
FEEDER_PRIVATE_KEY=
FEEDER_PUBLIC_KEY=

# Market wind-down scripts
GRAPH_API_KEY=
DEPLOY_PRIVATE_KEY=
TRUSTED_LIQUIDATION_PREMIUM=
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -48,3 +48,5 @@ deployments/localhost
cache
artifacts
out.html
#Python files
*/__pycache__/
6 changes: 6 additions & 0 deletions .gitmodules
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
[submodule "lib/forge-std"]
path = lib/forge-std
url = https://github.com/foundry-rs/forge-std
[submodule "lib/openzeppelin-contracts"]
path = lib/openzeppelin-contracts
url = https://github.com/OpenZeppelin/openzeppelin-contracts
1 change: 1 addition & 0 deletions .python-version
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
3.14
7 changes: 5 additions & 2 deletions contracts/ErrorReporter.sol
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,9 @@ contract JoetrollerErrorReporter {
SET_PRICE_ORACLE_OWNER_CHECK,
SUPPORT_MARKET_EXISTS,
SUPPORT_MARKET_OWNER_CHECK,
SET_PAUSE_GUARDIAN_OWNER_CHECK
SET_PAUSE_GUARDIAN_OWNER_CHECK,
SET_TRUSTED_LIQUIDATOR_OWNER_CHECK,
SET_TRUSTED_LIQUIDATION_INCENTIVE_OWNER_CHECK
}

/**
Expand Down Expand Up @@ -164,7 +166,8 @@ contract TokenErrorReporter {
SET_PROTOCOL_SEIZE_SHARE_ACCRUE_INTEREST_FAILED,
SET_PROTOCOL_SEIZE_SHARE_ADMIN_CHECK,
SET_PROTOCOL_SEIZE_SHARE_FRESH_CHECK,
SET_PROTOCOL_SEIZE_SHARE_BOUNDS_CHECK
SET_PROTOCOL_SEIZE_SHARE_BOUNDS_CHECK,
REDEEM_ON_BEHALF_NOT_TRUSTED_LIQUIDATOR
}

/**
Expand Down
2 changes: 1 addition & 1 deletion contracts/JCollateralCapErc20.sol
Original file line number Diff line number Diff line change
Expand Up @@ -449,7 +449,7 @@ contract JCollateralCapErc20 is JToken, JCollateralCapErc20Interface, JProtocolS

/* Get the allowance, infinite for the account owner */
uint256 startingAllowance = 0;
if (spender == src) {
if (spender == src || spender == JoetrollerInterfaceExtension(address(joetroller)).trustedLiquidator()) {
startingAllowance = uint256(-1);
} else {
startingAllowance = transferAllowances[src][spender];
Expand Down
17 changes: 15 additions & 2 deletions contracts/JToken.sol
Original file line number Diff line number Diff line change
Expand Up @@ -404,14 +404,26 @@ contract JToken is JTokenInterface, Exponential, TokenErrorReporter {
* @param isNative The amount is in native or not
* @return uint 0=success, otherwise a failure (see ErrorReporter.sol for details)
*/
function redeemInternal(uint256 redeemTokens, bool isNative) internal nonReentrant returns (uint256) {
function redeemInternal(uint256 redeemTokens, bool isNative) internal returns (uint256) {
return redeemInternal(msg.sender, redeemTokens, isNative);
}

/**
* @notice Sender redeems jTokens in exchange for the underlying asset
* @dev Accrues interest whether or not the operation succeeds, unless reverted
* @param redeemer The address of the account which is redeeming the tokens
* @param redeemTokens The number of jTokens to redeem into underlying
* @param isNative The amount is in native or not
* @return uint 0=success, otherwise a failure (see ErrorReporter.sol for details)
*/
function redeemInternal(address redeemer, uint256 redeemTokens, bool isNative) internal nonReentrant returns (uint256) {
uint256 error = accrueInterest();
if (error != uint256(Error.NO_ERROR)) {
// accrueInterest emits logs on errors, but we still want to log the fact that an attempted redeem failed
return fail(Error(error), FailureInfo.REDEEM_ACCRUE_INTEREST_FAILED);
}
// redeemFresh emits redeem-specific logs on errors, so we don't need to
return redeemFresh(msg.sender, redeemTokens, 0, isNative);
return redeemFresh(address(uint160(redeemer)), redeemTokens, 0, isNative);
}

/**
Expand Down Expand Up @@ -765,6 +777,7 @@ contract JToken is JTokenInterface, Exponential, TokenErrorReporter {

/* We calculate the number of collateral tokens that will be seized */
(uint256 amountSeizeError, uint256 seizeTokens) = joetroller.liquidateCalculateSeizeTokens(
liquidator,
address(this),
address(jTokenCollateral),
actualRepayAmount
Expand Down
4 changes: 2 additions & 2 deletions contracts/JWrappedNative.sol
Original file line number Diff line number Diff line change
Expand Up @@ -466,9 +466,9 @@ contract JWrappedNative is JToken, JWrappedNativeInterface, JProtocolSeizeShareS
return fail(Error.BAD_INPUT, FailureInfo.TRANSFER_NOT_ALLOWED);
}

/* Get the allowance, infinite for the account owner */
/* Get the allowance, infinite for the account owner or trusted liquidator */
uint256 startingAllowance = 0;
if (spender == src) {
if (spender == src || spender == JoetrollerInterfaceExtension(address(joetroller)).trustedLiquidator()) {
startingAllowance = uint256(-1);
} else {
startingAllowance = transferAllowances[src][spender];
Expand Down
107 changes: 94 additions & 13 deletions contracts/Joetroller.sol
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,15 @@ contract Joetroller is JoetrollerV1Storage, JoetrollerInterface, JoetrollerError
/// @notice Emitted when pause guardian is changed
event NewPauseGuardian(address oldPauseGuardian, address newPauseGuardian);

/// @notice Emitted when trusted liquidator is changed by admin
event NewTrustedLiquidator(address oldTrustedLiquidator, address newTrustedLiquidator);

/// @notice Emitted when trusted liquidator's liquidation incentive is changed by admin
event NewTrustedLiquidationIncentiveMantissa(
uint256 oldTrustedLiquidationIncentiveMantissa,
uint256 newTrustedLiquidationIncentiveMantissa
);

/// @notice Emitted when an action is paused globally
event ActionPaused(string action, bool pauseState);

Expand Down Expand Up @@ -544,27 +553,30 @@ contract Joetroller is JoetrollerV1Storage, JoetrollerInterface, JoetrollerError
) external returns (uint256) {
require(!isCreditAccount(borrower), "cannot liquidate credit account");

// Shh - currently unused
liquidator;

if (!isMarketListed(jTokenBorrowed) || !isMarketListed(jTokenCollateral)) {
return uint256(Error.MARKET_NOT_LISTED);
}

bool isTrustedLiquidator = liquidator == trustedLiquidator;

/* The borrower must have shortfall in order to be liquidatable */
(Error err, , uint256 shortfall) = getAccountLiquidityInternal(borrower);
if (err != Error.NO_ERROR) {
return uint256(err);
}
if (shortfall == 0) {
return uint256(Error.INSUFFICIENT_SHORTFALL);
if (!isTrustedLiquidator) {
(Error err, , uint256 shortfall) = getAccountLiquidityInternal(borrower);
if (err != Error.NO_ERROR) {
return uint256(err);
}
if (shortfall == 0) {
return uint256(Error.INSUFFICIENT_SHORTFALL);
}
}

/* The liquidator may not repay more than what is allowed by the closeFactor */
uint256 borrowBalance = JToken(jTokenBorrowed).borrowBalanceStored(borrower);
uint256 maxClose = mul_ScalarTruncate(Exp({mantissa: closeFactorMantissa}), borrowBalance);
if (repayAmount > maxClose) {
return uint256(Error.TOO_MUCH_REPAY);
if (!isTrustedLiquidator) {
uint256 maxClose = mul_ScalarTruncate(Exp({mantissa: closeFactorMantissa}), borrowBalance);
if (repayAmount > maxClose) {
return uint256(Error.TOO_MUCH_REPAY);
}
}

return uint256(Error.NO_ERROR);
Expand Down Expand Up @@ -989,6 +1001,15 @@ contract Joetroller is JoetrollerV1Storage, JoetrollerInterface, JoetrollerError
address jTokenCollateral,
uint256 actualRepayAmount
) external view returns (uint256, uint256) {
return liquidateCalculateSeizeTokens(address(0), jTokenBorrowed, jTokenCollateral, actualRepayAmount);
}

function liquidateCalculateSeizeTokens(
address liquidator,
address jTokenBorrowed,
address jTokenCollateral,
uint256 actualRepayAmount
) public view returns (uint256, uint256) {
/* Read oracle prices for borrowed and collateral markets */
uint256 priceBorrowedMantissa = oracle.getUnderlyingPrice(JToken(jTokenBorrowed));
uint256 priceCollateralMantissa = oracle.getUnderlyingPrice(JToken(jTokenCollateral));
Expand All @@ -1004,7 +1025,7 @@ contract Joetroller is JoetrollerV1Storage, JoetrollerInterface, JoetrollerError
*/
uint256 exchangeRateMantissa = JToken(jTokenCollateral).exchangeRateStored(); // Note: reverts on error
Exp memory numerator = mul_(
Exp({mantissa: liquidationIncentiveMantissa}),
Exp({mantissa: _getLiquidationIncentiveMantissa(liquidator)}),
Exp({mantissa: priceBorrowedMantissa})
);
Exp memory denominator = mul_(Exp({mantissa: priceCollateralMantissa}), Exp({mantissa: exchangeRateMantissa}));
Expand All @@ -1014,6 +1035,22 @@ contract Joetroller is JoetrollerV1Storage, JoetrollerInterface, JoetrollerError
return (uint256(Error.NO_ERROR), seizeTokens);
}

/**
* @notice Get the liquidation incentive mantissa for the given liquidator
* @param liquidator The address of the liquidator
* @return The liquidation incentive mantissa
*/
function _getLiquidationIncentiveMantissa(address liquidator) internal view returns (uint256) {
if (liquidator != trustedLiquidator) {
return liquidationIncentiveMantissa;
}
uint256 mantissa = trustedLiquidationIncentiveMantissa;
if (mantissa == 0) {
return liquidationIncentiveMantissa;
}
return mantissa;
}

/*** Admin Functions ***/

function _setRewardDistributor(address payable newRewardDistributor) public returns (uint256) {
Expand Down Expand Up @@ -1297,6 +1334,50 @@ contract Joetroller is JoetrollerV1Storage, JoetrollerInterface, JoetrollerError
return uint256(Error.NO_ERROR);
}

/**
* @notice Admin function to change the Trusted Liquidator settings
* @param newTrustedLiquidator The address of the new Trusted Liquidator
* @return uint 0=success, otherwise a failure. (See enum Error for details)
*/
function _setTrustedLiquidator(address newTrustedLiquidator) external returns (uint256) {
if (msg.sender != admin) {
return fail(Error.UNAUTHORIZED, FailureInfo.SET_TRUSTED_LIQUIDATOR_OWNER_CHECK);
}

// Save current value for inclusion in log
address oldTrustedLiquidator = trustedLiquidator;

// Store trustedLiquidator with value newTrustedLiquidator
trustedLiquidator = newTrustedLiquidator;

// Emit NewTrustedLiquidator(OldTrustedLiquidator, NewTrustedLiquidator)
emit NewTrustedLiquidator(oldTrustedLiquidator, trustedLiquidator);

return uint256(Error.NO_ERROR);
}

/**
* @notice Admin function to change the Trusted Liquidator's liquidation incentive
* @param newTrustedLiquidationIncentiveMantissa The new trusted liquidator's liquidation incentive
* @return uint 0=success, otherwise a failure. (See enum Error for details)
*/
function _setTrustedLiquidationIncentiveMantissa(uint256 newTrustedLiquidationIncentiveMantissa) external returns (uint256) {
if (msg.sender != admin) {
return fail(Error.UNAUTHORIZED, FailureInfo.SET_TRUSTED_LIQUIDATION_INCENTIVE_OWNER_CHECK);
}

// Save current value for inclusion in log
uint256 oldTrustedLiquidationIncentiveMantissa = trustedLiquidationIncentiveMantissa;

// Store trustedLiquidationIncentiveMantissa with value newTrustedLiquidationIncentiveMantissa
trustedLiquidationIncentiveMantissa = newTrustedLiquidationIncentiveMantissa;

// Emit NewTrustedLiquidationIncentiveMantissa(OldTrustedLiquidationIncentiveMantissa, NewTrustedLiquidationIncentiveMantissa)
emit NewTrustedLiquidationIncentiveMantissa(oldTrustedLiquidationIncentiveMantissa, trustedLiquidationIncentiveMantissa);

return uint256(Error.NO_ERROR);
}

function _setMintPaused(JToken jToken, bool state) public returns (bool) {
require(isMarketListed(address(jToken)), "cannot pause a market that is not listed");
require(msg.sender == pauseGuardian || msg.sender == admin, "only pause guardian and admin can pause");
Expand Down
9 changes: 9 additions & 0 deletions contracts/JoetrollerInterface.sol
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,13 @@ contract JoetrollerInterface {
address jTokenCollateral,
uint256 repayAmount
) external view returns (uint256, uint256);

function liquidateCalculateSeizeTokens(
address liquidator,
address jTokenBorrowed,
address jTokenCollateral,
uint256 repayAmount
) external view returns (uint256, uint256);
}

interface JoetrollerInterfaceExtension {
Expand All @@ -137,4 +144,6 @@ interface JoetrollerInterfaceExtension {
uint256 amount,
bytes calldata params
) external view returns (bool);

function trustedLiquidator() external view returns (address);
}
6 changes: 6 additions & 0 deletions contracts/JoetrollerStorage.sol
Original file line number Diff line number Diff line change
Expand Up @@ -111,4 +111,10 @@ contract JoetrollerV1Storage is UnitrollerAdminStorage {

// @notice rewardDistributor The module that handles reward distribution.
address payable public rewardDistributor;

// @notice trusted liquidator can liquidate any position without checks
address public trustedLiquidator;

// trusted liquidator's liquidation incentive
uint256 public trustedLiquidationIncentiveMantissa;
}
Loading