Skip to content

Commit

Permalink
Merge branch 'jun/PAG-C-01' into jun/PAG-M-04
Browse files Browse the repository at this point in the history
  • Loading branch information
junkim012 committed May 16, 2024
2 parents c21600c + 7802ed9 commit 27e8d3b
Show file tree
Hide file tree
Showing 10 changed files with 899 additions and 69 deletions.
5 changes: 4 additions & 1 deletion src/IonPool.sol
Original file line number Diff line number Diff line change
Expand Up @@ -478,7 +478,10 @@ contract IonPool is PausableUpgradeable, RewardToken {
Ilk storage ilk = $.ilks[ilkIndex];

uint256 _totalNormalizedDebt = ilk.totalNormalizedDebt;
if (_totalNormalizedDebt == 0 || block.timestamp == ilk.lastRateUpdate) {
// Because all interest that would have accrued during a pause is
// cancelled upon `unpause`, we return zero interest while markets are
// paused.
if (_totalNormalizedDebt == 0 || block.timestamp == ilk.lastRateUpdate || paused()) {
// Unsafe cast OK
// block.timestamp - ilk.lastRateUpdate will almost always be 0
// here. The exception is on first borrow.
Expand Down
1 change: 1 addition & 0 deletions src/interfaces/IIonPool.sol
Original file line number Diff line number Diff line change
Expand Up @@ -234,4 +234,5 @@ interface IIonPool {
function getTotalUnderlyingClaims() external view returns (uint256);
function getUnderlyingClaimOf(address user) external view returns (uint256);
function extsload(bytes32 slot) external view returns (bytes32);
function balanceOfUnaccrued(address user) external view returns (uint256);
}
12 changes: 10 additions & 2 deletions src/token/RewardToken.sol
Original file line number Diff line number Diff line change
Expand Up @@ -459,6 +459,14 @@ abstract contract RewardToken is
return $._normalizedBalances[user].rayMulDown($.supplyFactor + totalSupplyFactorIncrease);
}

/**
* @dev Current claim of the underlying token without accounting for interest to be accrued.
*/
function balanceOfUnaccrued(address user) public view returns (uint256) {
RewardTokenStorage storage $ = _getRewardTokenStorage();
return $._normalizedBalances[user].rayMulDown($.supplyFactor);
}

/**
* @dev Accounting is done in normalized balances
* @param user to get normalized balance of
Expand Down Expand Up @@ -519,9 +527,9 @@ abstract contract RewardToken is
return 0;
}

(uint256 totalSupplyFactorIncrease,,,,) = calculateRewardAndDebtDistribution();
(uint256 totalSupplyFactorIncrease, uint256 totalTreasuryMintAmount,,,) = calculateRewardAndDebtDistribution();

return _normalizedTotalSupply.rayMulDown($.supplyFactor + totalSupplyFactorIncrease);
return _normalizedTotalSupply.rayMulDown($.supplyFactor + totalSupplyFactorIncrease) + totalTreasuryMintAmount;
}

function normalizedTotalSupplyUnaccrued() public view returns (uint256) {
Expand Down
95 changes: 73 additions & 22 deletions src/vault/Vault.sol
Original file line number Diff line number Diff line change
Expand Up @@ -86,14 +86,22 @@ contract Vault is ERC4626, Multicall, AccessControlDefaultAdminRules, Reentrancy
int256 assets;
}

struct MarketsArgs {
IIonPool[] marketsToAdd;
uint256[] allocationCaps;
IIonPool[] newSupplyQueue;
IIonPool[] newWithdrawQueue;
}

constructor(
IERC20 _baseAsset,
address _feeRecipient,
uint256 _feePercentage,
string memory _name,
string memory _symbol,
uint48 initialDelay,
address initialDefaultAdmin
address initialDefaultAdmin,
MarketsArgs memory marketsArgs
)
ERC4626(_baseAsset)
ERC20(_name, _symbol)
Expand All @@ -105,6 +113,13 @@ contract Vault is ERC4626, Multicall, AccessControlDefaultAdminRules, Reentrancy
feeRecipient = _feeRecipient;

DECIMALS_OFFSET = uint8(_zeroFloorSub(uint256(18), IERC20Metadata(address(_baseAsset)).decimals()));

_addSupportedMarkets(
marketsArgs.marketsToAdd,
marketsArgs.allocationCaps,
marketsArgs.newSupplyQueue,
marketsArgs.newWithdrawQueue
);
}

/**
Expand All @@ -115,6 +130,7 @@ contract Vault is ERC4626, Multicall, AccessControlDefaultAdminRules, Reentrancy
*/
function updateFeePercentage(uint256 _feePercentage) external onlyRole(OWNER_ROLE) {
if (_feePercentage > RAY) revert InvalidFeePercentage();
_accrueFee();
feePercentage = _feePercentage;
}

Expand All @@ -138,13 +154,24 @@ contract Vault is ERC4626, Multicall, AccessControlDefaultAdminRules, Reentrancy
* @param newWithdrawQueue Desired withdraw queue of IonPools for all resulting supported markets.
*/
function addSupportedMarkets(
IIonPool[] calldata marketsToAdd,
uint256[] calldata allocationCaps,
IIonPool[] calldata newSupplyQueue,
IIonPool[] calldata newWithdrawQueue
IIonPool[] memory marketsToAdd,
uint256[] memory allocationCaps,
IIonPool[] memory newSupplyQueue,
IIonPool[] memory newWithdrawQueue
)
public
external
onlyRole(OWNER_ROLE)
{
_addSupportedMarkets(marketsToAdd, allocationCaps, newSupplyQueue, newWithdrawQueue);
}

function _addSupportedMarkets(
IIonPool[] memory marketsToAdd,
uint256[] memory allocationCaps,
IIonPool[] memory newSupplyQueue,
IIonPool[] memory newWithdrawQueue
)
internal
{
if (marketsToAdd.length != allocationCaps.length) revert MarketsAndAllocationCapLengthMustBeEqual();

Expand All @@ -167,8 +194,8 @@ contract Vault is ERC4626, Multicall, AccessControlDefaultAdminRules, Reentrancy
}
}

updateSupplyQueue(newSupplyQueue);
updateWithdrawQueue(newWithdrawQueue);
_updateSupplyQueue(newSupplyQueue);
_updateWithdrawQueue(newWithdrawQueue);
}

/**
Expand Down Expand Up @@ -211,16 +238,20 @@ contract Vault is ERC4626, Multicall, AccessControlDefaultAdminRules, Reentrancy
++i;
}
}
updateSupplyQueue(newSupplyQueue);
updateWithdrawQueue(newWithdrawQueue);
_updateSupplyQueue(newSupplyQueue);
_updateWithdrawQueue(newWithdrawQueue);
}

/**
* @notice Update the order of the markets in which user deposits are supplied.
* @dev Each IonPool in the queue must be part of the `supportedMarkets` set.
* @param newSupplyQueue The new supply queue ordering.
*/
function updateSupplyQueue(IIonPool[] calldata newSupplyQueue) public onlyRole(ALLOCATOR_ROLE) {
function updateSupplyQueue(IIonPool[] memory newSupplyQueue) external onlyRole(ALLOCATOR_ROLE) {
_updateSupplyQueue(newSupplyQueue);
}

function _updateSupplyQueue(IIonPool[] memory newSupplyQueue) internal {
_validateQueueInput(newSupplyQueue);

supplyQueue = newSupplyQueue;
Expand All @@ -233,7 +264,11 @@ contract Vault is ERC4626, Multicall, AccessControlDefaultAdminRules, Reentrancy
* @dev The IonPool in the queue must be part of the `supportedMarkets` set.
* @param newWithdrawQueue The new withdraw queue ordering.
*/
function updateWithdrawQueue(IIonPool[] calldata newWithdrawQueue) public onlyRole(ALLOCATOR_ROLE) {
function updateWithdrawQueue(IIonPool[] memory newWithdrawQueue) external onlyRole(ALLOCATOR_ROLE) {
_updateWithdrawQueue(newWithdrawQueue);
}

function _updateWithdrawQueue(IIonPool[] memory newWithdrawQueue) internal {
_validateQueueInput(newWithdrawQueue);

withdrawQueue = newWithdrawQueue;
Expand All @@ -249,7 +284,7 @@ contract Vault is ERC4626, Multicall, AccessControlDefaultAdminRules, Reentrancy
* The above rule enforces that the queue must have all and only the elements in the `supportedMarkets` set.
* @param queue The queue being validated.
*/
function _validateQueueInput(IIonPool[] calldata queue) internal view {
function _validateQueueInput(IIonPool[] memory queue) internal view {
uint256 _supportedMarketsLength = supportedMarkets.length();
uint256 queueLength = queue.length;

Expand Down Expand Up @@ -343,6 +378,8 @@ contract Vault is ERC4626, Multicall, AccessControlDefaultAdminRules, Reentrancy
// to the user from the previous function scope.
if (pool != IDLE) {
pool.withdraw(address(this), transferAmt);
} else {
currentIdleDeposits -= transferAmt;
}

totalWithdrawn += transferAmt;
Expand Down Expand Up @@ -372,6 +409,8 @@ contract Vault is ERC4626, Multicall, AccessControlDefaultAdminRules, Reentrancy
// contract.
if (pool != IDLE) {
pool.supply(address(this), transferAmt, new bytes32[](0));
} else {
currentIdleDeposits += transferAmt;
}

totalSupplied += transferAmt;
Expand Down Expand Up @@ -421,12 +460,16 @@ contract Vault is ERC4626, Multicall, AccessControlDefaultAdminRules, Reentrancy

// For the IDLE pool, decrement the accumulator at the end of this
// loop, but no external interactions need to be made as the assets
// are already on this contract' balance.
// are already on this contract' balance. If the pool supply
// reverts, simply skip to the next iteration.
if (pool != IDLE) {
pool.supply(address(this), toSupply, new bytes32[](0));
try pool.supply(address(this), toSupply, new bytes32[](0)) {
assets -= toSupply;
} catch { }
} else {
assets -= toSupply;
}

assets -= toSupply;
if (assets == 0) return;
}

Expand Down Expand Up @@ -458,12 +501,17 @@ contract Vault is ERC4626, Multicall, AccessControlDefaultAdminRules, Reentrancy
uint256 toWithdraw = Math.min(withdrawable, assets);

// For the `IDLE` pool, they are already on this contract's
// balance. Update `assets` accumulator but don't actually transfer.
// balance. Update `assets` accumulator but don't actually
// transfer. If the pool withdraw reverts, simply skip to the
// next iteration.
if (pool != IDLE) {
pool.withdraw(address(this), toWithdraw);
try pool.withdraw(address(this), toWithdraw) {
assets -= toWithdraw;
} catch { }
} else {
assets -= toWithdraw;
}

assets -= toWithdraw;
if (assets == 0) return;
}

Expand Down Expand Up @@ -705,9 +753,7 @@ contract Vault is ERC4626, Multicall, AccessControlDefaultAdminRules, Reentrancy

function _deposit(address caller, address receiver, uint256 assets, uint256 shares) internal override {
super._deposit(caller, receiver, assets, shares);

_supplyToIonPool(assets);

_updateLastTotalAssets(lastTotalAssets + assets);
}

Expand Down Expand Up @@ -750,7 +796,12 @@ contract Vault is ERC4626, Multicall, AccessControlDefaultAdminRules, Reentrancy
(feeShares, newTotalAssets) = _accruedFeeShares();
newTotalSupply = totalSupply() + feeShares;

assets = _convertToAssetsWithTotals(balanceOf(owner), newTotalSupply, newTotalAssets, Math.Rounding.Floor);
uint256 shareBalances = balanceOf(owner);
if (owner == feeRecipient) {
shareBalances += feeShares;
}

assets = _convertToAssetsWithTotals(shareBalances, newTotalSupply, newTotalAssets, Math.Rounding.Floor);

assets -= _simulateWithdrawIon(assets);
}
Expand Down
28 changes: 24 additions & 4 deletions src/vault/VaultFactory.sol
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,16 @@ pragma solidity 0.8.21;

import { Vault } from "./Vault.sol";
import { IERC20 } from "openzeppelin-contracts/contracts/interfaces/IERC20.sol";
import { SafeERC20 } from "openzeppelin-contracts/contracts/token/ERC20/utils/SafeERC20.sol";

/**
* @title Ion Lending Vault Factory
* @author Molecular Labs
* @notice Factory contract for deploying Ion Lending Vaults.
*/
contract VaultFactory {
using SafeERC20 for IERC20;

// --- Events ---

event CreateVault(
Expand All @@ -25,7 +28,13 @@ contract VaultFactory {
// --- External ---

/**
* @notice Deploys a new Ion Lending Vault.
* @notice Deploys a new Ion Lending Vault. Transfers the `initialDeposit`
* amount of the base asset from the caller initiate the first deposit to
* the vault. The minimum `initialDeposit` is 1e3. If less, this call would
* underflow as it will always burn 1e3 shares of the total shares minted to
* defend against inflation attacks.
* @dev The 1e3 initial deposit amount was chosen to defend against
* inflation attacks, referencing the UniV2 LP token implementation.
* @param baseAsset The asset that is being lent out to IonPools.
* @param feeRecipient Address that receives the accrued manager fees.
* @param feePercentage Fee percentage to be set.
Expand All @@ -34,6 +43,8 @@ contract VaultFactory {
* @param initialDelay The initial delay for default admin transfers.
* @param initialDefaultAdmin The initial default admin for the vault.
* @param salt The salt used for CREATE2 deployment.
* @param marketsArgs Arguments for the markets to be added to the vault.
* @param initialDeposit The initial deposit to be made to the vault.
*/
function createVault(
IERC20 baseAsset,
Expand All @@ -43,16 +54,25 @@ contract VaultFactory {
string memory symbol,
uint48 initialDelay,
address initialDefaultAdmin,
bytes32 salt
bytes32 salt,
Vault.MarketsArgs memory marketsArgs,
uint256 initialDeposit
)
external
returns (Vault vault)
{
// TODO use named args syntax
vault = new Vault{ salt: salt }(
baseAsset, feeRecipient, feePercentage, name, symbol, initialDelay, initialDefaultAdmin
baseAsset, feeRecipient, feePercentage, name, symbol, initialDelay, initialDefaultAdmin, marketsArgs
);

baseAsset.safeTransferFrom(msg.sender, address(this), initialDeposit);
baseAsset.approve(address(vault), initialDeposit);
uint256 sharesMinted = vault.deposit(initialDeposit, address(this));

// The factory keeps 1e3 shares to reduce inflation attack vector.
// Effectively burns this amount of shares by locking it in the factory.
vault.transfer(msg.sender, sharesMinted - 1e3);

emit CreateVault(address(vault), baseAsset, feeRecipient, feePercentage, name, symbol, initialDefaultAdmin);
}
}
Loading

0 comments on commit 27e8d3b

Please sign in to comment.