Description:
Multi-signature wallet contract requiring multiple confirmations for transaction execution.
Blockchain: Ethereum
Source Code: View Code On The Blockchain
Solidity Source Code:
{{
"language": "Solidity",
"sources": {
"src/Lens.sol": {
"content": "// SPDX-License-Identifier: UNLICENSED
pragma solidity 0.8.13;
import "./Lender.sol";
contract Lens {
using FixedPointMathLib for uint256;
function getCollateralOf(Lender _lender, address borrower) public view returns (uint256) {
uint borrowerDebtShares = _lender.freeDebtShares(borrower);
// If borrower has no debt shares skip calculation
if (borrowerDebtShares == 0) return 0;
uint _borrowerEpoch = _lender.borrowerEpoch(borrower);
uint bal = _lender._cachedCollateralBalances(borrower);
uint lastIndex = _lender.borrowerLastRedeemedIndex(borrower);
// Loop through all missed epochs
for (uint i = 0; i < 5 && _borrowerEpoch < _lender.epoch() && borrowerDebtShares > 0; ++i) {
// Apply redemption for the borrower's current epoch
uint indexDelta = _lender.epochRedeemedCollateral(_borrowerEpoch) - lastIndex;
uint redeemedCollateral = indexDelta.mulDivUp(borrowerDebtShares, 1e36);
bal = bal < redeemedCollateral ? 0 : bal - redeemedCollateral;
// Move to next epoch, reduce shares
_borrowerEpoch += 1;
borrowerDebtShares = borrowerDebtShares.divWadUp(1e36) == 1 ? 0 : borrowerDebtShares.divWadUp(1e36); // If shares is 1 round down to 0
lastIndex = 0; // For new epoch, last redeemed index is 0
}
// Apply any remaining redemption for the current epoch
if (borrowerDebtShares > 0) {
uint indexDelta = _lender.epochRedeemedCollateral(_borrowerEpoch) - lastIndex;
uint redeemedCollateral = indexDelta.mulDivUp(borrowerDebtShares, 1e36);
bal = bal < redeemedCollateral ? 0 : bal - redeemedCollateral;
}
return bal;
}
}"
},
"src/Lender.sol": {
"content": "// SPDX-License-Identifier: UNLICENSED
pragma solidity 0.8.13;
import "lib/solmate/src/tokens/ERC20.sol";
import "lib/solmate/src/utils/SafeTransferLib.sol";
import "lib/solmate/src/utils/FixedPointMathLib.sol";
import "./Coin.sol";
import "./Vault.sol";
import "./InterestModel.sol";
interface IChainlinkFeed {
function decimals() external view returns (uint8);
function latestRoundData() external view returns (
uint80 roundId,
int256 answer,
uint256 startedAt,
uint256 updatedAt,
uint80 answeredInRound
);
}
interface IFactory {
function getFeeOf(address _lender) external view returns (uint256);
}
contract Lender {
using SafeTransferLib for ERC20;
using FixedPointMathLib for uint256;
// single 256-bit slot
uint16 public targetFreeDebtRatioStartBps = 2000; // max uint16 is 65535 bps which is outside of the range [0, 10000]
uint16 public targetFreeDebtRatioEndBps = 4000; // max uint16 is 65535 bps which is outside of the range [0, 10000]
uint16 public redeemFeeBps = 30; // max uint16 is 65535 bps fee which is outside of the range [0, 10000]
uint64 public expRate = uint64(uint(wadLn(2*1e18)) / 7 days); // max result is 693147180559945309 which is within uint64 range
uint40 public lastAccrue; // max uint40 is year 36812
uint88 public lastBorrowRateMantissa = uint88(5e15); // max uint88 is equivalent to 309485000% APR
uint16 public feeBps; // max uint16 is 65535 bps which is outside of the range [0, 10000]
// single 256-bit slot
uint16 public cachedGlobalFeeBps;
uint120 public accruedLocalReserves;
uint120 public accruedGlobalReserves;
// Other state variables
address public operator;
address public pendingOperator;
uint public immutabilityDeadline; // may only be reduced by operator
uint public totalFreeDebt;
uint public totalFreeDebtShares;
uint public totalPaidDebt;
uint public totalPaidDebtShares;
uint public epoch;
// Constants and immutables
Coin public immutable coin;
ERC20 public immutable collateral;
IChainlinkFeed public immutable feed;
Vault public immutable vault;
InterestModel public immutable interestModel;
IFactory public immutable factory;
uint public immutable collateralFactor;
uint public immutable minDebt;
uint public constant STALENESS_THRESHOLD = 25 hours; // standard 24 hours staleness + 1 hour buffer
uint public constant STALENESS_UNWIND_DURATION = 24 hours;
uint public constant MIN_LIQUIDATION_DEBT = 10_000e18; // 10,000 Coin
// Mappings
mapping(address => uint) public _cachedCollateralBalances; // should not be read externally in most cases
mapping(address => uint) public freeDebtShares;
mapping(address => uint) public paidDebtShares;
mapping(address => bool) public isRedeemable;
mapping(address => mapping(address => bool)) public delegations;
mapping(address => uint) public borrowerLastRedeemedIndex;
mapping(address => uint) public borrowerEpoch;
mapping(uint => uint) public epochRedeemedCollateral;
uint256 public nonRedeemableCollateral;
constructor(
ERC20 _collateral,
IChainlinkFeed _feed,
Coin _coin,
Vault _vault,
InterestModel _interestModel,
IFactory _factory,
address _operator,
uint _collateralFactor,
uint _minDebt,
uint _timeUntilImmutability
) {
require(_collateralFactor <= 10000, "Invalid collateral factor");
require(_timeUntilImmutability < 1460 days, "Max immutability deadline is in 4 years");
collateral = _collateral;
feed = _feed;
coin = _coin;
vault = _vault;
interestModel = _interestModel;
factory = _factory;
operator = _operator;
collateralFactor = _collateralFactor;
minDebt = _minDebt;
immutabilityDeadline = block.timestamp + _timeUntilImmutability;
lastAccrue = uint40(block.timestamp);
cachedGlobalFeeBps = uint16(factory.getFeeOf(address(this)));
}
// Modifiers
modifier onlyOperator() {
require(msg.sender == operator, "Unauthorized");
_;
}
modifier beforeDeadline() {
require(block.timestamp < immutabilityDeadline, "Deadline passed");
_;
}
// Public functions
function accrueInterest() public {
uint timeElapsed = block.timestamp - lastAccrue;
if(timeElapsed == 0) return;
try interestModel.calculateInterest(
totalPaidDebt,
lastBorrowRateMantissa,
timeElapsed,
expRate,
getFreeDebtRatio(),
targetFreeDebtRatioStartBps,
targetFreeDebtRatioEndBps
) returns (uint currBorrowRate, uint interest) {
uint120 localReserveFee = uint120(interest * feeBps / 10000);
uint120 globalReserveFee = uint120(interest * cachedGlobalFeeBps / 10000);
accruedLocalReserves += localReserveFee;
accruedGlobalReserves += globalReserveFee;
// we remove reserve fees from interest before calculating how much to give to stakers
uint interestAfterFees = interest - localReserveFee - globalReserveFee;
uint totalStaked = vault.totalAssets();
if(totalStaked < totalPaidDebt) { // this also implies totalPaidDebt > 0 and guards the division below
// if total staked is less than paid debt, giving all interest to stakers would
// result in higher supply rate than borrow rate which is undesirable.
// we cap the supply rate at the borrow rate and give the rest to local reserves.
uint stakedInterest = interestAfterFees * totalStaked / totalPaidDebt;
coin.mint(address(vault), stakedInterest);
uint remainingInterest = interestAfterFees - stakedInterest;
accruedLocalReserves += uint120(remainingInterest);
} else {
// if total staked is greater than paid debt, we give all interest to stakers
coin.mint(address(vault), interestAfterFees);
}
totalPaidDebt += interest; // we add all interest to paid debt (NOT interestAfterFees)
lastAccrue = uint40(block.timestamp);
lastBorrowRateMantissa = uint88(currBorrowRate);
cachedGlobalFeeBps = uint16(factory.getFeeOf(address(this)));
} catch {
// If the call reverts, do nothing.
}
}
function adjust(address account, int collateralDelta, int debtDelta) public {
accrueInterest();
updateBorrower(account);
// Handle collateral changes
if (collateralDelta > 0) {
if(!isRedeemable[account]) nonRedeemableCollateral += uint(collateralDelta);
// Deposit collateral
_cachedCollateralBalances[account] += uint(collateralDelta);
collateral.safeTransferFrom(msg.sender, address(this), uint(collateralDelta));
} else if (collateralDelta < 0) {
// Ensure sufficient collateral for non-redeemable accounts
if (isRedeemable[account]) {
require(
collateral.balanceOf(address(this)) - uint256(-collateralDelta) >= nonRedeemableCollateral,
"Insufficient redeemable collateral"
);
} else {
nonRedeemableCollateral -= uint256(-collateralDelta);
}
// Withdraw collateral
_cachedCollateralBalances[account] -= uint(-collateralDelta);
collateral.safeTransfer(msg.sender, uint(-collateralDelta));
}
// Handle debt changes
if (debtDelta > 0) {
// Borrow
uint amount = uint256(debtDelta);
increaseDebt(account, amount);
coin.mint(msg.sender, amount);
} else if (debtDelta < 0) {
// Repay
uint amount = uint256(-debtDelta);
uint debt = getDebtOf(account);
if(debt <= amount) {
amount = debt;
decreaseDebt(account, type(uint).max);
} else {
decreaseDebt(account, amount);
}
coin.transferFrom(msg.sender, address(this), amount);
coin.burn(amount);
}
// if debtDelta is non-zero, require debt balance to either be 0 or >= minDebt
uint debtBalance = getDebtOf(account);
if(debtDelta != 0) require(debtBalance == 0 || debtBalance >= minDebt, "Debt below minimum and larger than 0");
// Emit event before the first early return
emit PositionAdjusted(account, collateralDelta, debtDelta);
// Skip remaining invariants if caller does not reduce collateral AND does not increase debt
if(collateralDelta >= 0 && debtDelta <= 0) return;
// The caller is removing collateral and/or increasing debt. Enforce ownership beyond this point
require(msg.sender == account || delegations[account][msg.sender], "Unauthorized");
// Skip solvency checks if debt is zero
if(debtBalance == 0) return;
// Check solvency
(uint price, bool reduceOnly, ) = getCollateralPrice();
require(!reduceOnly, "Reduce only");
uint borrowingPower = price * _cachedCollateralBalances[account] * collateralFactor / 1e18 / 10000;
require(debtBalance <= borrowingPower, "Solvency check failed");
}
function adjust(address account, int collateralDelta, int debtDelta, bool chooseRedeemable) external {
setRedemptionStatus(account, chooseRedeemable);
adjust(account, collateralDelta, debtDelta);
}
/// @notice Allows an account to delegate control of their position to another address (adjustPosition, optInRedemptions, optOutRedemptions functions)
/// @param delegatee The address to delegate to
/// @param isDelegatee True to enable delegation, false to revoke
function delegate(address delegatee, bool isDelegatee) external {
delegations[msg.sender][delegatee] = isDelegatee;
emit DelegationUpdated(msg.sender, delegatee, isDelegatee);
}
function setRedemptionStatus(address account, bool chooseRedeemable) public {
accrueInterest();
updateBorrower(account);
require(msg.sender == account || delegations[account][msg.sender], "Unauthorized");
if(chooseRedeemable == isRedeemable[account]) return; // no change
if(chooseRedeemable){
borrowerEpoch[account] = epoch;
borrowerLastRedeemedIndex[account] = epochRedeemedCollateral[epoch];
nonRedeemableCollateral -= _cachedCollateralBalances[account];
} else {
nonRedeemableCollateral += _cachedCollateralBalances[account];
}
uint prevDebt = getDebtOf(account);
if(prevDebt > 0) {
decreaseDebt(account, type(uint).max);
isRedeemable[account] = chooseRedeemable;
increaseDebt(account, prevDebt);
uint currDebt = getDebtOf(account);
require(currDebt >= prevDebt, "Debt decreased unexpectedly");
} else {
isRedeemable[account] = chooseRedeemable;
}
emit RedemptionStatusUpdated(account, chooseRedeemable);
}
/// @notice Liquidates an unsafe position
/// @param borrower The account to be liquidated
/// @param repayAmount The amount of debt to repay
/// @param minCollateralOut The minimum amount of collateral to receive
/// @return The amount of collateral received
function liquidate(address borrower, uint repayAmount, uint minCollateralOut) external returns(uint) {
accrueInterest();
updateBorrower(borrower);
require(repayAmount > 0, "Repay amount must be greater than 0");
(uint price,, bool allowLiquidations) = getCollateralPrice();
require(allowLiquidations, "liquidations disabled");
uint debt = getDebtOf(borrower);
uint collateralBalance = _cachedCollateralBalances[borrower];
// check liquidation condition
uint liquidatableDebt = getLiquidatableDebt(collateralBalance, price, debt);
require(liquidatableDebt > 0, "insufficient liquidatable debt");
if(repayAmount > liquidatableDebt) {
repayAmount = liquidatableDebt;
}
// apply repayment
decreaseDebt(borrower, repayAmount);
// calculate collateral reward
uint liqIncentiveBps = getLiquidationIncentiveBps(collateralBalance, price, debt);
uint collateralRewardValue = repayAmount * (10000 + liqIncentiveBps) / 10000;
uint collateralReward = collateralRewardValue * 1e18 / price;
collateralReward = collateralReward > collateralBalance ? collateralBalance : collateralReward;
require(collateralReward >= minCollateralOut, "insufficient collateral out");
if(collateralReward > 0) {
collateral.safeTransfer(msg.sender, collateralReward);
_cachedCollateralBalances[borrower] = collateralBalance - collateralReward;
if(!isRedeemable[borrower]) nonRedeemableCollateral -= collateralReward;
}
coin.transferFrom(msg.sender, address(this), repayAmount);
coin.burn(repayAmount);
emit Liquidated(borrower, msg.sender, repayAmount, collateralReward);
// try to write off remaining debt. Call externally and catch error to prevent liquidation failure
try this.writeOff(borrower, msg.sender) {} catch {}
return collateralReward;
}
/// @notice Redistributes excess debt of undercollateralized accounts among other borrowers
/// @param borrower The account in potentiallyundercollateralized state
/// @return writtenOff True if the borrower was written off, false otherwise
/// @param to The address to send the collateral to
/// @dev This function is called by liquidate() when a borrower's position is undercollateralized. It should never revert to avoid liquidation failure.
function writeOff(address borrower, address to) external returns (bool writtenOff) {
accrueInterest();
updateBorrower(borrower);
// check for write off
uint debt = getDebtOf(borrower);
if(debt > 0) {
uint collateralBalance = _cachedCollateralBalances[borrower];
(uint price,, bool allowLiquidations) = getCollateralPrice();
require(allowLiquidations, "liquidations disabled");
uint collateralValue = price * collateralBalance / 1e18;
// if debt is more than 100 times the collateral value, write off
if(debt > collateralValue * 100) {
// 1. delete all of the borrower's debt
decreaseDebt(borrower, type(uint).max);
// 2. redistribute excess debt among remaining borrowers
uint256 totalDebt = totalFreeDebt + totalPaidDebt;
if (totalDebt > 0) {
uint256 freeDebtIncrease = debt * totalFreeDebt / totalDebt;
uint256 paidDebtIncrease = debt - freeDebtIncrease;
totalFreeDebt += freeDebtIncrease;
totalPaidDebt += paidDebtIncrease;
}
// 3. send collateral to caller
collateral.safeTransfer(to, collateralBalance);
if(!isRedeemable[borrower]) nonRedeemableCollateral -= collateralBalance;
_cachedCollateralBalances[borrower] = 0;
emit WrittenOff(borrower, to, debt, collateralBalance);
writtenOff = true;
}
}
}
/// @notice Redeems Coin for collateral at current market price minus a fee
/// @param amountIn The amount of Coin to redeem
/// @param minAmountOut The minimum amount of collateral to receive
/// @return amountOut The amount of collateral received
/// @dev Redemptions requires sufficient redeemable collateral to seize and free debt to repay
function redeem(uint amountIn, uint minAmountOut) external returns (uint amountOut) {
accrueInterest();
// calculate amountOut
amountOut = getRedeemAmountOut(amountIn);
require(amountOut >= minAmountOut, "insufficient amount out");
require(collateral.balanceOf(address(this)) - amountOut >= nonRedeemableCollateral, "Insufficient redeemable collateral");
// repay on behalf of free debtors
totalFreeDebt -= amountIn;
coin.transferFrom(msg.sender, address(this), amountIn);
coin.burn(amountIn);
// distribute collateral redemption per free debt share
epochRedeemedCollateral[epoch] += amountOut.mulDivUp(1e36, totalFreeDebtShares);
collateral.safeTransfer(msg.sender, amountOut);
// Intentional division by zero and revert if totalFreeDebt is 0
if( totalFreeDebtShares / totalFreeDebt > 1e9) {
epoch++;
totalFreeDebtShares = totalFreeDebtShares.mulDivUp(1e18,1e36);
emit NewEpoch(epoch);
}
emit Redeemed(msg.sender, amountIn, amountOut);
return amountOut;
}
// Internal functions
function updateBorrower(address borrower) internal {
uint borrowerDebtShares = freeDebtShares[borrower];
if (borrowerDebtShares > 0) {
uint _borrowerEpoch = borrowerEpoch[borrower];
uint bal = _cachedCollateralBalances[borrower];
uint lastIndex = borrowerLastRedeemedIndex[borrower];
// Loop through missed epochs (max 5 iterations considering max uint256 is 2^256 - 1 would go to zero in 5 iterations)
for (uint i = 0; i < 5 && _borrowerEpoch < epoch && borrowerDebtShares > 0; ++i) {
// Apply redemption for the borrower's current epoch
uint indexDelta = epochRedeemedCollateral[_borrowerEpoch] - lastIndex;
uint redeemedCollateral = indexDelta.mulDivUp(borrowerDebtShares, 1e36);
bal = bal < redeemedCollateral ? 0 : bal - redeemedCollateral;
// Move to next epoch, reduce shares
_borrowerEpoch += 1;
borrowerDebtShares = borrowerDebtShares.divWadUp(1e36) == 1 ? 0 : borrowerDebtShares.divWadUp(1e36); // If shares is 1 round down to 0
lastIndex = 0; // For new epoch, last redeemed index is 0
}
// Apply any remaining redemption for the current epoch
if (borrowerDebtShares > 0) {
uint indexDelta = epochRedeemedCollateral[_borrowerEpoch] - lastIndex;
uint redeemedCollateral = indexDelta.mulDivUp(borrowerDebtShares, 1e36);
bal = bal < redeemedCollateral ? 0 : bal - redeemedCollateral;
}
// Update state
freeDebtShares[borrower] = borrowerDebtShares;
_cachedCollateralBalances[borrower] = bal;
}
if(isRedeemable[borrower]){
borrowerEpoch[borrower] = epoch;
borrowerLastRedeemedIndex[borrower] = epochRedeemedCollateral[epoch];
}
}
function increaseDebt(address account, uint256 amount) internal {
if (isRedeemable[account]) {
// Handle free debt
uint shares = totalFreeDebtShares == 0 ?
amount :
amount.mulDivUp(totalFreeDebtShares, totalFreeDebt);
totalFreeDebt += amount;
totalFreeDebtShares += shares;
freeDebtShares[account] += shares;
} else {
// Handle paid debt
uint256 shares = totalPaidDebtShares == 0 ?
amount :
amount.mulDivUp(totalPaidDebtShares, totalPaidDebt);
totalPaidDebt += amount;
totalPaidDebtShares += shares;
paidDebtShares[account] += shares;
}
}
function decreaseDebt(address account, uint256 amount) internal {
if (isRedeemable[account]) {
// Handle free debt
uint256 shares;
if(amount == type(uint).max) {
shares = freeDebtShares[account];
amount = getDebtOf(account);
} else {
shares = amount.mulDivDown(totalFreeDebtShares, totalFreeDebt);
}
freeDebtShares[account] -= shares;
totalFreeDebtShares = totalFreeDebtShares <= shares ? 0 : totalFreeDebtShares - shares; // prevent underflow
totalFreeDebt = totalFreeDebt <= amount ? 0 : totalFreeDebt - amount; // prevent underflow
} else {
// Handle paid debt
uint256 shares;
if(amount == type(uint).max) {
shares = paidDebtShares[account];
amount = getDebtOf(account);
} else {
shares = amount.mulDivDown(totalPaidDebtShares, totalPaidDebt);
}
paidDebtShares[account] -= shares;
totalPaidDebtShares -= shares;
totalPaidDebt -= amount;
}
}
function getLiquidatableDebt(uint collateralBalance, uint price, uint debt) internal view returns(uint liquidatableDebt){
uint borrowingPower = price * collateralBalance * collateralFactor / 1e18 / 10000;
if(borrowingPower > debt) return 0;
// liquidate 25% of the total debt
liquidatableDebt = debt / 4; // 25% of the debt
// liquidate at least MIN_LIQUIDATION_DEBT (or the entire debt if it's less than MIN_LIQUIDATION_DEBT)
if(liquidatableDebt < MIN_LIQUIDATION_DEBT) liquidatableDebt = debt < MIN_LIQUIDATION_DEBT ? debt : MIN_LIQUIDATION_DEBT;
}
function getLiquidationIncentiveBps(uint collateralBalance, uint price, uint debt) internal view returns(uint) {
uint collateralValue = collateralBalance * price / 1e18;
if (collateralValue == 0) return 100; // avoid division by zero, return 1% incentive
uint ltvBps = debt * 10000 / collateralValue;
uint _collateralFactor = collateralFactor; // gas optimization
uint maxLtvBps = _collateralFactor + 500; // range is [_collateralFactor, _collateralFactor + 5%]
if (ltvBps <= _collateralFactor) {
return 100; // 1% incentive
} else if (ltvBps >= maxLtvBps) {
return 1000; // 10% incentive
} else {
// linear interpolation between 1% and 10% incentive
return 100 + (ltvBps - _collateralFactor) * 900 / (maxLtvBps - _collateralFactor);
}
}
// Getters
function getFreeDebtRatio() public view returns (uint) {
return totalFreeDebt == 0 ? 0 : totalFreeDebt * 10000 / (totalPaidDebt + totalFreeDebt);
}
function getDebtOf(address account) public view returns (uint) {
if(isRedeemable[account]) {
return totalFreeDebtShares == 0 ? 0 : freeDebtShares[account].mulDivUp(totalFreeDebt, totalFreeDebtShares);
} else {
return totalPaidDebtShares == 0 ? 0 : paidDebtShares[account].mulDivUp(totalPaidDebt, totalPaidDebtShares);
}
}
/// @notice Gets the current price of the collateral asset
/// @return price The price in USD normalized to (36 - collateral decimals) decimals for consistent calculations
/// @return reduceOnly A boolean indicating if reduce only mode is enabled
/// @return allowLiquidations A boolean indicating if liquidations and write-offs are enabled
function getCollateralPrice() public view returns (uint price, bool reduceOnly, bool allowLiquidations) {
uint updatedAt;
allowLiquidations = true; // Default to allowing liquidations
// call our own getFeedPrice() externally to catch all feed reverts e.g. due to inexistent feed contract, function, etc.
try this.getFeedPrice() returns (uint _price, uint _updatedAt) {
price = _price;
updatedAt = _updatedAt;
if(price == 0) {
reduceOnly = true;
allowLiquidations = false; // Disable liquidations if price is invalid
}
} catch {
reduceOnly = true;
allowLiquidations = false; // Disable liquidations only if the oracle feed is reverting
}
uint currentTime = block.timestamp;
uint timeElapsed = currentTime >= updatedAt ? currentTime - updatedAt : 0;
if (timeElapsed > STALENESS_THRESHOLD) {
reduceOnly = true;
uint stalenessDuration = timeElapsed - STALENESS_THRESHOLD;
if (stalenessDuration < STALENESS_UNWIND_DURATION) {
price = price * (STALENESS_UNWIND_DURATION - stalenessDuration) / STALENESS_UNWIND_DURATION;
} else {
price = 0;
}
}
price = price == 0 ? 1 : price; // avoid division by zero in consumer functions
}
function getFeedPrice() external view returns (uint price, uint updatedAt) {
(,int256 feedPrice,,uint256 feedUpdatedAt,) = feed.latestRoundData();
uint8 feedDecimals = feed.decimals();
uint8 tokenDecimals = collateral.decimals();
if(feedDecimals + tokenDecimals <= 36) {
uint8 decimals = 36 - tokenDecimals - feedDecimals;
price = feedPrice > 0 ? uint(feedPrice) * (10**decimals) : 0; // convert negative price to uint 0 to signal invalid price
} else {
uint8 decimals = feedDecimals + tokenDecimals - 36;
price = feedPrice > 0 ? uint(feedPrice) / (10**decimals) : 0; // convert negative price to uint 0 to signal invalid price
}
updatedAt = feedUpdatedAt;
}
/// @notice Calculates the amount of collateral received for redeeming Coin
/// @param amountIn The amount of Coin to redeem
/// @return amountOut The amount of collateral to receive
function getRedeemAmountOut(uint amountIn) public view returns (uint amountOut) {
if(amountIn > totalFreeDebt) return 0; // can't redeem more than free debt
(uint price,, bool allowLiquidations) = getCollateralPrice();
if(!allowLiquidations) return 0;
// multiply amountIn by price then apply redeem fee to amountIn
amountOut = amountIn * 1e18 * (10000 - redeemFeeBps) / price / 10000;
}
// Setters
function setHalfLife(uint64 halfLife) external onlyOperator beforeDeadline {
accrueInterest();
require(halfLife >= 12 hours && halfLife <= 30 days, "Invalid half life");
expRate = uint64(uint(wadLn(2*1e18)) / halfLife);
emit HalfLifeUpdated(halfLife);
}
function setTargetFreeDebtRatio(uint16 startBps, uint16 endBps) external onlyOperator beforeDeadline {
accrueInterest();
require(startBps >= 500 && startBps <= endBps, "Invalid start bps");
require(endBps >= startBps && endBps <= 9500, "Invalid end bps");
targetFreeDebtRatioStartBps = uint16(startBps);
targetFreeDebtRatioEndBps = uint16(endBps);
emit TargetFreeDebtRatioUpdated(startBps, endBps);
}
function setRedeemFeeBps(uint16 _redeemFeeBps) external onlyOperator beforeDeadline {
accrueInterest();
require(_redeemFeeBps <= 300, "Invalid redeem fee bps");
redeemFeeBps = uint16(_redeemFeeBps);
emit RedeemFeeBpsUpdated(_redeemFeeBps);
}
function setLocalReserveFeeBps(uint _feeBps) external onlyOperator {
accrueInterest();
require(_feeBps <= 1000, "Invalid fee");
feeBps = uint16(_feeBps);
emit LocalReserveFeeUpdated(_feeBps);
}
function setPendingOperator(address _pendingOperator) external onlyOperator {
pendingOperator = _pendingOperator;
emit PendingOperatorUpdated(_pendingOperator);
}
function acceptOperator() external {
require(msg.sender == pendingOperator, "Unauthorized");
operator = pendingOperator;
emit OperatorAccepted(pendingOperator);
}
function enableImmutabilityNow() external onlyOperator beforeDeadline {
immutabilityDeadline = block.timestamp;
}
function pullLocalReserves() external onlyOperator {
accrueInterest();
coin.mint(msg.sender, accruedLocalReserves);
accruedLocalReserves = 0;
}
function pullGlobalReserves(address _to) external {
require(msg.sender == address(factory), "Unauthorized");
accrueInterest();
coin.mint(_to, accruedGlobalReserves);
accruedGlobalReserves = 0;
}
// Events
event PositionAdjusted(address indexed account, int collateralDelta, int debtDelta);
event HalfLifeUpdated(uint64 halfLife);
event TargetFreeDebtRatioUpdated(uint16 startBps, uint16 endBps);
event RedeemFeeBpsUpdated(uint16 redeemFeeBps);
event DelegationUpdated(address indexed delegator, address indexed delegatee, bool isDelegatee);
event PendingOperatorUpdated(address indexed pendingOperator);
event OperatorAccepted(address indexed operator);
event LocalReserveFeeUpdated(uint256 feeBps);
event RedemptionStatusUpdated(address indexed account, bool isRedeemable);
event Liquidated(address indexed borrower, address indexed liquidator, uint repayAmount, uint collateralOut);
event WrittenOff(address indexed borrower, address indexed to, uint debt, uint collateral);
event NewEpoch(uint epoch);
event Redeemed(address indexed account, uint amountIn, uint amountOut);
}
"
},
"lib/solmate/src/tokens/ERC20.sol": {
"content": "// SPDX-License-Identifier: AGPL-3.0-only
pragma solidity >=0.8.0;
/// @notice Modern and gas efficient ERC20 + EIP-2612 implementation.
/// @author Solmate (https://github.com/transmissions11/solmate/blob/main/src/tokens/ERC20.sol)
/// @author Modified from Uniswap (https://github.com/Uniswap/uniswap-v2-core/blob/master/contracts/UniswapV2ERC20.sol)
/// @dev Do not manually set balances without updating totalSupply, as the sum of all user balances must not exceed it.
abstract contract ERC20 {
/*//////////////////////////////////////////////////////////////
EVENTS
//////////////////////////////////////////////////////////////*/
event Transfer(address indexed from, address indexed to, uint256 amount);
event Approval(address indexed owner, address indexed spender, uint256 amount);
/*//////////////////////////////////////////////////////////////
METADATA STORAGE
//////////////////////////////////////////////////////////////*/
string public name;
string public symbol;
uint8 public immutable decimals;
/*//////////////////////////////////////////////////////////////
ERC20 STORAGE
//////////////////////////////////////////////////////////////*/
uint256 public totalSupply;
mapping(address => uint256) public balanceOf;
mapping(address => mapping(address => uint256)) public allowance;
/*//////////////////////////////////////////////////////////////
EIP-2612 STORAGE
//////////////////////////////////////////////////////////////*/
uint256 internal immutable INITIAL_CHAIN_ID;
bytes32 internal immutable INITIAL_DOMAIN_SEPARATOR;
mapping(address => uint256) public nonces;
/*//////////////////////////////////////////////////////////////
CONSTRUCTOR
//////////////////////////////////////////////////////////////*/
constructor(
string memory _name,
string memory _symbol,
uint8 _decimals
) {
name = _name;
symbol = _symbol;
decimals = _decimals;
INITIAL_CHAIN_ID = block.chainid;
INITIAL_DOMAIN_SEPARATOR = computeDomainSeparator();
}
/*//////////////////////////////////////////////////////////////
ERC20 LOGIC
//////////////////////////////////////////////////////////////*/
function approve(address spender, uint256 amount) public virtual returns (bool) {
allowance[msg.sender][spender] = amount;
emit Approval(msg.sender, spender, amount);
return true;
}
function transfer(address to, uint256 amount) public virtual returns (bool) {
balanceOf[msg.sender] -= amount;
// Cannot overflow because the sum of all user
// balances can't exceed the max uint256 value.
unchecked {
balanceOf[to] += amount;
}
emit Transfer(msg.sender, to, amount);
return true;
}
function transferFrom(
address from,
address to,
uint256 amount
) public virtual returns (bool) {
uint256 allowed = allowance[from][msg.sender]; // Saves gas for limited approvals.
if (allowed != type(uint256).max) allowance[from][msg.sender] = allowed - amount;
balanceOf[from] -= amount;
// Cannot overflow because the sum of all user
// balances can't exceed the max uint256 value.
unchecked {
balanceOf[to] += amount;
}
emit Transfer(from, to, amount);
return true;
}
/*//////////////////////////////////////////////////////////////
EIP-2612 LOGIC
//////////////////////////////////////////////////////////////*/
function permit(
address owner,
address spender,
uint256 value,
uint256 deadline,
uint8 v,
bytes32 r,
bytes32 s
) public virtual {
require(deadline >= block.timestamp, "PERMIT_DEADLINE_EXPIRED");
// Unchecked because the only math done is incrementing
// the owner's nonce which cannot realistically overflow.
unchecked {
address recoveredAddress = ecrecover(
keccak256(
abi.encodePacked(
"\x19\x01",
DOMAIN_SEPARATOR(),
keccak256(
abi.encode(
keccak256(
"Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)"
),
owner,
spender,
value,
nonces[owner]++,
deadline
)
)
)
),
v,
r,
s
);
require(recoveredAddress != address(0) && recoveredAddress == owner, "INVALID_SIGNER");
allowance[recoveredAddress][spender] = value;
}
emit Approval(owner, spender, value);
}
function DOMAIN_SEPARATOR() public view virtual returns (bytes32) {
return block.chainid == INITIAL_CHAIN_ID ? INITIAL_DOMAIN_SEPARATOR : computeDomainSeparator();
}
function computeDomainSeparator() internal view virtual returns (bytes32) {
return
keccak256(
abi.encode(
keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"),
keccak256(bytes(name)),
keccak256("1"),
block.chainid,
address(this)
)
);
}
/*//////////////////////////////////////////////////////////////
INTERNAL MINT/BURN LOGIC
//////////////////////////////////////////////////////////////*/
function _mint(address to, uint256 amount) internal virtual {
totalSupply += amount;
// Cannot overflow because the sum of all user
// balances can't exceed the max uint256 value.
unchecked {
balanceOf[to] += amount;
}
emit Transfer(address(0), to, amount);
}
function _burn(address from, uint256 amount) internal virtual {
balanceOf[from] -= amount;
// Cannot underflow because a user's balance
// will never be larger than the total supply.
unchecked {
totalSupply -= amount;
}
emit Transfer(from, address(0), amount);
}
}
"
},
"lib/solmate/src/utils/SafeTransferLib.sol": {
"content": "// SPDX-License-Identifier: AGPL-3.0-only
pragma solidity >=0.8.0;
import {ERC20} from "../tokens/ERC20.sol";
/// @notice Safe ETH and ERC20 transfer library that gracefully handles missing return values.
/// @author Solmate (https://github.com/transmissions11/solmate/blob/main/src/utils/SafeTransferLib.sol)
/// @dev Use with caution! Some functions in this library knowingly create dirty bits at the destination of the free memory pointer.
/// @dev Note that none of the functions in this library check that a token has code at all! That responsibility is delegated to the caller.
library SafeTransferLib {
/*//////////////////////////////////////////////////////////////
ETH OPERATIONS
//////////////////////////////////////////////////////////////*/
function safeTransferETH(address to, uint256 amount) internal {
bool success;
/// @solidity memory-safe-assembly
assembly {
// Transfer the ETH and store if it succeeded or not.
success := call(gas(), to, amount, 0, 0, 0, 0)
}
require(success, "ETH_TRANSFER_FAILED");
}
/*//////////////////////////////////////////////////////////////
ERC20 OPERATIONS
//////////////////////////////////////////////////////////////*/
function safeTransferFrom(
ERC20 token,
address from,
address to,
uint256 amount
) internal {
bool success;
/// @solidity memory-safe-assembly
assembly {
// Get a pointer to some free memory.
let freeMemoryPointer := mload(0x40)
// Write the abi-encoded calldata into memory, beginning with the function selector.
mstore(freeMemoryPointer, 0x23b872dd00000000000000000000000000000000000000000000000000000000)
mstore(add(freeMemoryPointer, 4), and(from, 0xffffffffffffffffffffffffffffffffffffffff)) // Append and mask the "from" argument.
mstore(add(freeMemoryPointer, 36), and(to, 0xffffffffffffffffffffffffffffffffffffffff)) // Append and mask the "to" argument.
mstore(add(freeMemoryPointer, 68), amount) // Append the "amount" argument. Masking not required as it's a full 32 byte type.
success := and(
// Set success to whether the call reverted, if not we check it either
// returned exactly 1 (can't just be non-zero data), or had no return data.
or(and(eq(mload(0), 1), gt(returndatasize(), 31)), iszero(returndatasize())),
// We use 100 because the length of our calldata totals up like so: 4 + 32 * 3.
// We use 0 and 32 to copy up to 32 bytes of return data into the scratch space.
// Counterintuitively, this call must be positioned second to the or() call in the
// surrounding and() call or else returndatasize() will be zero during the computation.
call(gas(), token, 0, freeMemoryPointer, 100, 0, 32)
)
}
require(success, "TRANSFER_FROM_FAILED");
}
function safeTransfer(
ERC20 token,
address to,
uint256 amount
) internal {
bool success;
/// @solidity memory-safe-assembly
assembly {
// Get a pointer to some free memory.
let freeMemoryPointer := mload(0x40)
// Write the abi-encoded calldata into memory, beginning with the function selector.
mstore(freeMemoryPointer, 0xa9059cbb00000000000000000000000000000000000000000000000000000000)
mstore(add(freeMemoryPointer, 4), and(to, 0xffffffffffffffffffffffffffffffffffffffff)) // Append and mask the "to" argument.
mstore(add(freeMemoryPointer, 36), amount) // Append the "amount" argument. Masking not required as it's a full 32 byte type.
success := and(
// Set success to whether the call reverted, if not we check it either
// returned exactly 1 (can't just be non-zero data), or had no return data.
or(and(eq(mload(0), 1), gt(returndatasize(), 31)), iszero(returndatasize())),
// We use 68 because the length of our calldata totals up like so: 4 + 32 * 2.
// We use 0 and 32 to copy up to 32 bytes of return data into the scratch space.
// Counterintuitively, this call must be positioned second to the or() call in the
// surrounding and() call or else returndatasize() will be zero during the computation.
call(gas(), token, 0, freeMemoryPointer, 68, 0, 32)
)
}
require(success, "TRANSFER_FAILED");
}
function safeApprove(
ERC20 token,
address to,
uint256 amount
) internal {
bool success;
/// @solidity memory-safe-assembly
assembly {
// Get a pointer to some free memory.
let freeMemoryPointer := mload(0x40)
// Write the abi-encoded calldata into memory, beginning with the function selector.
mstore(freeMemoryPointer, 0x095ea7b300000000000000000000000000000000000000000000000000000000)
mstore(add(freeMemoryPointer, 4), and(to, 0xffffffffffffffffffffffffffffffffffffffff)) // Append and mask the "to" argument.
mstore(add(freeMemoryPointer, 36), amount) // Append the "amount" argument. Masking not required as it's a full 32 byte type.
success := and(
// Set success to whether the call reverted, if not we check it either
// returned exactly 1 (can't just be non-zero data), or had no return data.
or(and(eq(mload(0), 1), gt(returndatasize(), 31)), iszero(returndatasize())),
// We use 68 because the length of our calldata totals up like so: 4 + 32 * 2.
// We use 0 and 32 to copy up to 32 bytes of return data into the scratch space.
// Counterintuitively, this call must be positioned second to the or() call in the
// surrounding and() call or else returndatasize() will be zero during the computation.
call(gas(), token, 0, freeMemoryPointer, 68, 0, 32)
)
}
require(success, "APPROVE_FAILED");
}
}
"
},
"lib/solmate/src/utils/FixedPointMathLib.sol": {
"content": "// SPDX-License-Identifier: AGPL-3.0-only
pragma solidity >=0.8.0;
/// @notice Arithmetic library with operations for fixed-point numbers.
/// @author Solmate (https://github.com/transmissions11/solmate/blob/main/src/utils/FixedPointMathLib.sol)
/// @author Inspired by USM (https://github.com/usmfum/USM/blob/master/contracts/WadMath.sol)
library FixedPointMathLib {
/*//////////////////////////////////////////////////////////////
SIMPLIFIED FIXED POINT OPERATIONS
//////////////////////////////////////////////////////////////*/
uint256 internal constant MAX_UINT256 = 2**256 - 1;
uint256 internal constant WAD = 1e18; // The scalar of ETH and most ERC20s.
function mulWadDown(uint256 x, uint256 y) internal pure returns (uint256) {
return mulDivDown(x, y, WAD); // Equivalent to (x * y) / WAD rounded down.
}
function mulWadUp(uint256 x, uint256 y) internal pure returns (uint256) {
return mulDivUp(x, y, WAD); // Equivalent to (x * y) / WAD rounded up.
}
function divWadDown(uint256 x, uint256 y) internal pure returns (uint256) {
return mulDivDown(x, WAD, y); // Equivalent to (x * WAD) / y rounded down.
}
function divWadUp(uint256 x, uint256 y) internal pure returns (uint256) {
return mulDivUp(x, WAD, y); // Equivalent to (x * WAD) / y rounded up.
}
/*//////////////////////////////////////////////////////////////
LOW LEVEL FIXED POINT OPERATIONS
//////////////////////////////////////////////////////////////*/
function mulDivDown(
uint256 x,
uint256 y,
uint256 denominator
) internal pure returns (uint256 z) {
/// @solidity memory-safe-assembly
assembly {
// Equivalent to require(denominator != 0 && (y == 0 || x <= type(uint256).max / y))
if iszero(mul(denominator, iszero(mul(y, gt(x, div(MAX_UINT256, y)))))) {
revert(0, 0)
}
// Divide x * y by the denominator.
z := div(mul(x, y), denominator)
}
}
function mulDivUp(
uint256 x,
uint256 y,
uint256 denominator
) internal pure returns (uint256 z) {
/// @solidity memory-safe-assembly
assembly {
// Equivalent to require(denominator != 0 && (y == 0 || x <= type(uint256).max / y))
if iszero(mul(denominator, iszero(mul(y, gt(x, div(MAX_UINT256, y)))))) {
revert(0, 0)
}
// If x * y modulo the denominator is strictly greater than 0,
// 1 is added to round up the division of x * y by the denominator.
z := add(gt(mod(mul(x, y), denominator), 0), div(mul(x, y), denominator))
}
}
function rpow(
uint256 x,
uint256 n,
uint256 scalar
) internal pure returns (uint256 z) {
/// @solidity memory-safe-assembly
assembly {
switch x
case 0 {
switch n
case 0 {
// 0 ** 0 = 1
z := scalar
}
default {
// 0 ** n = 0
z := 0
}
}
default {
switch mod(n, 2)
case 0 {
// If n is even, store scalar in z for now.
z := scalar
}
default {
// If n is odd, store x in z for now.
z := x
}
// Shifting right by 1 is like dividing by 2.
let half := shr(1, scalar)
for {
// Shift n right by 1 before looping to halve it.
n := shr(1, n)
} n {
// Shift n right by 1 each iteration to halve it.
n := shr(1, n)
} {
// Revert immediately if x ** 2 would overflow.
// Equivalent to iszero(eq(div(xx, x), x)) here.
if shr(128, x) {
revert(0, 0)
}
// Store x squared.
let xx := mul(x, x)
// Round to the nearest number.
let xxRound := add(xx, half)
// Revert if xx + half overflowed.
if lt(xxRound, xx) {
revert(0, 0)
}
// Set x to scaled xxRound.
x := div(xxRound, scalar)
// If n is even:
if mod(n, 2) {
// Compute z * x.
let zx := mul(z, x)
// If z * x overflowed:
if iszero(eq(div(zx, x), z)) {
// Revert if x is non-zero.
if iszero(iszero(x)) {
revert(0, 0)
}
}
// Round to the nearest number.
let zxRound := add(zx, half)
// Revert if zx + half overflowed.
if lt(zxRound, zx) {
revert(0, 0)
}
// Return properly scaled zxRound.
z := div(zxRound, scalar)
}
}
}
}
}
/*//////////////////////////////////////////////////////////////
GENERAL NUMBER UTILITIES
//////////////////////////////////////////////////////////////*/
function sqrt(uint256 x) internal pure returns (uint256 z) {
/// @solidity memory-safe-assembly
assembly {
let y := x // We start y at x, which will help us make our initial estimate.
z := 181 // The "correct" value is 1, but this saves a multiplication later.
// This segment is to get a reasonable initial estimate for the Babylonian method. With a bad
// start, the correct # of bits increases ~linearly each iteration instead of ~quadratically.
// We check y >= 2^(k + 8) but shift right by k bits
// each branch to ensure that if x >= 256, then y >= 256.
if iszero(lt(y, 0x10000000000000000000000000000000000)) {
y := shr(128, y)
z := shl(64, z)
}
if iszero(lt(y, 0x1000000000000000000)) {
y := shr(64, y)
z := shl(32, z)
}
if iszero(lt(y, 0x10000000000)) {
y := shr(32, y)
z := shl(16, z)
}
if iszero(lt(y, 0x1000000)) {
y := shr(16, y)
z := shl(8, z)
}
// Goal was to get z*z*y within a small factor of x. More iterations could
// get y in a tighter range. Currently, we will have y in [256, 256*2^16).
// We ensured y >= 256 so that the relative difference between y and y+1 is small.
// That's not possible if x < 256 but we can just verify those cases exhaustively.
// Now, z*z*y <= x < z*z*(y+1), and y <= 2^(16+8), and either y >= 256, or x < 256.
// Correctness can be checked exhaustively for x < 256, so we assume y >= 256.
// Then z*sqrt(y) is within sqrt(257)/sqrt(256) of sqrt(x), or about 20bps.
// For s in the range [1/256, 256], the estimate f(s) = (181/1024) * (s+1) is in the range
// (1/2.84 * sqrt(s), 2.84 * sqrt(s)), with largest error when s = 1 and when s = 256 or 1/256.
// Since y is in [256, 256*2^16), let a = y/65536, so that a is in [1/256, 256). Then we can estimate
// sqrt(y) using sqrt(65536) * 181/1024 * (a + 1) = 181/4 * (y + 65536)/65536 = 181 * (y + 65536)/2^18.
// There is no overflow risk here since y < 2^136 after the first branch above.
z := shr(18, mul(z, add(y, 65536))) // A mul() is saved from starting z at 181.
// Given the worst case multiplicative error of 2.84 above, 7 iterations should be enough.
z := shr(1, add(z, div(x, z)))
z := shr(1, add(z, div(x, z)))
z := shr(1, add(z, div(x, z)))
z := shr(1, add(z, div(x, z)))
z := shr(1, add(z, div(x, z)))
z := shr(1, add(z, div(x, z)))
z := shr(1, add(z, div(x, z)))
// If x+1 is a perfect square, the Babylonian method cycles between
// floor(sqrt(x)) and ceil(sqrt(x)). This statement ensures we return floor.
// See: https://en.wikipedia.org/wiki/Integer_square_root#Using_only_integer_division
// Since the ceil is rare, we save gas on the assignment and repeat division in the rare case.
// If you don't care whether the floor or ceil square root is returned, you can remove this statement.
z := sub(z, lt(div(x, z), z))
}
}
function unsafeMod(uint256 x, uint256 y) internal pure returns (uint256 z) {
/// @solidity memory-safe-assembly
assembly {
// Mod x by y. Note this will return
// 0 instead of reverting if y is zero.
z := mod(x, y)
}
}
function unsafeDiv(uint256 x, uint256 y) internal pure returns (uint256 r) {
/// @solidity memory-safe-assembly
assembly {
// Divide x by y. Note this will return
// 0 instead of reverting if y is zero.
r := div(x, y)
}
}
function unsafeDivUp(uint256 x, uint256 y) internal pure returns (uint256 z) {
/// @solidity memory-safe-assembly
assembly {
// Add 1 to x * y if x % y > 0. Note this will
// return 0 instead of reverting if y is zero.
z := add(gt(mod(x, y), 0), div(x, y))
}
}
}
"
},
"src/Coin.sol": {
"content": "// SPDX-License-Identifier: UNLICENSED
pragma solidity 0.8.13;
import "lib/solmate/src/tokens/ERC20.sol";
contract Coin is ERC20 {
address public immutable minter;
constructor(address _minter, string memory name, string memory symbol) ERC20(name, symbol, 18) {
minter = _minter;
}
function mint(address to, uint256 amount) external {
require(msg.sender == minter, "Only minter can mint");
_mint(to, amount);
}
function burn(uint256 amount) external {
_burn(msg.sender, amount);
}
}"
},
"src/Vault.sol": {
"content": "// SPDX-License-Identifier: UNLICENSED
pragma solidity 0.8.13;
import "lib/solmate/src/tokens/ERC4626.sol";
interface ILender {
function accrueInterest() external;
function coin() external view returns (ERC20);
}
contract Vault is ERC4626 {
ILender public immutable lender;
uint256 constant MIN_SHARES = 1e16; // 1 cent;
/// @param _name Name of the token. Prepended with "Staked "
/// @param _symbol Symbol of the token. Prepended with "s"
/// @param _lender Address of the Lender token
constructor(
string memory _name,
string memory _symbol,
address _lender
) ERC4626(
ILender(_lender).coin(),
string.concat("Staked ", _name),
string.concat("s", _symbol)
) {
lender = ILender(_lender);
}
modifier accrueInterest() {
lender.accrueInterest();
_;
}
function totalAssets() public view override returns (uint256) {
return asset.balanceOf(address(this));
}
/// @notice Deposits assets into the vault
/// @param assets Amount of assets to deposit
/// @param receiver Address to receive the shares
/// @return shares Amount of shares minted
function deposit(uint256 assets, address receiver) public accrueInterest override returns (uint256 shares) {
bool isFirstDeposit = totalSupply == 0;
shares = super.deposit(assets, receiver);
if(isFirstDeposit) {
// if this underflows, the first deposit is less than MIN_SHARES which is not allowed
balanceOf[receiver] -= MIN_SHARES;
balanceOf[address(0)] += MIN_SHARES;
emit Transfer(receiver, address(0), MIN_SHARES);
shares -= MIN_SHARES;
}
}
/// @notice Mints shares of the vault
/// @param shares Amount of shares to mint
/// @param receiver Address to receive the shares
/// @return assets Amount of assets deposited
function mint(uint256 shares, address receiver) public accrueInterest override returns (uint256 assets) {
bool isFirstDeposit = totalSupply == 0;
assets = super.mint(shares, receiver);
if(isFirstDeposit) {
// if this underflows, the first deposit is less than MIN_SHARES which is not allowed
balanceOf[receiver] -= MIN_SHARES;
balanceOf[address(0)] += MIN_SHARES;
emit Transfer(receiver, address(0), MIN_SHARES);
assets -= MIN_SHARES; // shares and assets are 1:1 when the first deposit is made
}
}
/// @notice Withdraws assets from the vault
/// @param assets Amount of assets to withdraw
/// @param receiver Address to receive the assets
/// @param owner Owner of the shares
/// @return shares Amount of shares burned
function withdraw(
uint256 assets,
address receiver,
address owner
) public accrueInterest override returns (uint256 shares) {
shares = super.withdraw(assets, receiver, owner);
}
/// @notice Redeems shares for assets
/// @param shares Amount of shares to redeem
/// @param receiver Address to receive the assets
/// @param owner Owner of the shares
/// @return assets Amount of assets withdrawn
function redeem(
uint256 shares,
address receiver,
address owner
) public accrueInterest override returns (uint256 assets) {
assets = super.redeem(shares, receiver, owner);
}
}"
},
"src/InterestModel.sol": {
"content": "// SPDX-License-Identifier: UNLICENSED
pragma solidity 0.8.13;
import "lib/solmate/src/utils/SignedWadMath.sol";
// Only one instance of this contract is deployed for use by all Lender contracts
contract InterestModel {
uint internal constant MIN_RATE = 5e15; // 0.5%
// The reason why this pure math is called externally by Lender contracts even though
// it is non-upgradeable is to allow Lender contracts try/catch the call for safety.
// In case of unexpected under/overflow here, Lender contracts would skip accruing interest
// while allowing borrowers to exit. Otherwise failure would freeze their funds.
// We could have also added this as an external function to Lender and called
// self.calculateInterest() externally, but since it's the same cost to call it here,
// we opt for this approach in order to reduce contract bytecode size of Lender.
// We also reduce Factory deployment costs by only using one instance of
// this contract for all Lender contracts.
function calculateInterest(
uint _totalPaidDebt,
uint _lastRate,
uint _timeElapsed,
uint _expRate,
uint _lastFreeDebtRatioBps,
uint _targetFreeDebtRatioStartBps,
uint _targetFreeDebtRatioEndBps
) external pure returns (uint currBorrowRate, uint interest) {
// check _expRate * _timeElapsed overflow
if(uint(type(int256).max) / _expRate < _timeElapsed) _timeElapsed = uint(type(int256).max) / _expRate;
// we use a negative exponent in order to prevent growthDecay overflow due to large timeElapsed
// Results of positive exponents can exceed max uint256, negative exponents only return a value between [0, 1e18]
uint growthDecay = uint(wadExp(-int(_expRate * _timeElapsed)));
if (_lastFreeDebtRatioBps < _targetFreeDebtRatioStartBps) {
currBorrowRate = _lastRate * 1e18 / growthDecay;
interest = _totalPaidDebt * (currBorrowRate - _lastRate) / _expRate / 365 days;
} else if (_lastFreeDebtRatioBps > _targetFreeDebtRatioEndBps) {
currBorrowRate = _lastRate * growthDecay / 1e18;
if (currBorrowRate < MIN_RATE) {
currBorrowRate = MIN_RATE;
// calculate integral
if (_lastRate <= MIN_RATE) {
// Already at min rate, just use flat rate for entire period
interest = _totalPaidDebt * MIN_RATE * _timeElapsed / 365 days / 1e18;
} else {
uint timeToMin = uint(-wadLn(int(MIN_RATE * 1e18 / _lastRate))) / _expRate;
// Decaying integral up to min rate, then add flat rate portion
interest = _totalPaidDebt * ((_lastRate - MIN_RATE) / _expRate +
MIN_RATE * (_timeElapsed - timeToMin)) / 365 days / 1e18;
}
} else {
interest = _totalPaidDebt * (_lastRate - currBorrowRate) / _expRate / 365 days;
}
} else {
currBorrowRate = _lastRate;
interest = _totalPaidDebt * _lastRate * _timeElapsed / 365 days / 1e18;
}
}
}"
},
"lib/solmate/src/tokens/ERC4626.sol": {
"content": "// SPDX-License-Identifier: AGPL-3.0-only
pragma solidity >=0.8.0;
import {ERC20} from "../tokens/ERC20.sol";
import {SafeTransferLib} from "../utils/SafeTransferLib.sol";
import {FixedPointMathLib} from "../utils/FixedPointMathLib.sol";
/// @notice Minimal ERC4626 tokenized Vault implementation.
/// @author Solmate (https://github.com/transmissions11/solmate/blob/main/src/tokens/ERC4626.sol)
abstract contract ERC4626 is ERC20 {
using SafeTransferLib for ERC20;
using FixedPointMathLib for uint256;
/*//////////////////////////////////////////////////////////////
EVENTS
//////////////////////////////////////////////////////////////*/
event Deposit(address indexed caller, address indexed owner, uint256 assets, uint256 shares);
event Withdraw(
address indexed caller,
address indexed receiver,
address indexed owner,
uint256 assets,
uint256 shares
);
/*//////////////////////////////////////////////////////////////
IMMUTABLES
//////////////////////////////////////////////////////////////*/
ERC20 public immutable asset;
constructor(
ERC20 _asset,
string memory _name,
string memory _symbol
) ERC20(_name, _symbol, _asset.decimals()) {
asset = _asset;
}
/*//////////////////////////////////////////////////////////////
DEPOSIT/WITHDRAWAL LOGIC
//////////////////////////
Submitted on: 2025-09-19 13:36:07
Comments
Log in to comment.
No comments yet.