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
88 changes: 68 additions & 20 deletions src/periphery/crowdsource/PWNCrowdsourceLenderVault.sol
Original file line number Diff line number Diff line change
Expand Up @@ -158,16 +158,14 @@ contract PWNCrowdsourceLenderVault is ERC4626, IPWNLenderCreateHook, IPWNLenderR
/** @inheritdoc ERC4626*/
function totalAssets() public view override returns (uint256) {
uint256 additionalAssets;
// Note: assuming aToken:token ratio is always 1:1
// aToken balance is always part of total assets (deposits in POOLING, repayments in RUNNING/ENDING)
if (aAsset != address(0)) {
additionalAssets = IERC20(aAsset).balanceOf(address(this));
}
Stage _stage = stage();
if (_stage == Stage.POOLING) {
if (aAsset != address(0)) {
// Note: assuming aToken:token ratio is always 1:1
additionalAssets = IERC20(aAsset).balanceOf(address(this));
}
} else if (_stage == Stage.RUNNING) {
if (loanContract.getLOANStatus(loanId) == LOANStatus.RUNNING) {
additionalAssets = loanContract.getLOANDebt(loanId);
}
if (_stage == Stage.RUNNING && loanContract.getLOANStatus(loanId) == LOANStatus.RUNNING) {
additionalAssets += loanContract.getLOANDebt(loanId);
}
return _availableLiquidity() + additionalAssets;
}
Expand All @@ -193,15 +191,17 @@ contract PWNCrowdsourceLenderVault is ERC4626, IPWNLenderCreateHook, IPWNLenderR

max = _convertToAssets(balanceOf(owner), Math.Rounding.Down);
if (_stage == Stage.RUNNING) {
max = Math.min(max, _availableLiquidity());
// Limit withdrawal to proportional share of available liquidity based on share ownership
max = Math.min(max, _proportionalAvailableLiquidity(owner));
}
}

/** @inheritdoc ERC4626*/
function maxRedeem(address owner) public view override returns (uint256 max) {
max = balanceOf(owner);
if (stage() == Stage.RUNNING) {
max = Math.min(max, _convertToShares(_availableLiquidity(), Math.Rounding.Down));
// Limit redemption to proportional share of available liquidity based on share ownership
max = Math.min(max, _convertToShares(_proportionalAvailableLiquidity(owner), Math.Rounding.Down));
}
}

Expand Down Expand Up @@ -278,6 +278,31 @@ contract PWNCrowdsourceLenderVault is ERC4626, IPWNLenderCreateHook, IPWNLenderR
return IERC20(asset()).balanceOf(address(this));
}

function _totalAvailableLiquidity() internal view returns (uint256) {
uint256 liquidity = _availableLiquidity();
if (aAsset != address(0)) {
liquidity += IERC20(aAsset).balanceOf(address(this));
}
return liquidity;
}

/**
* @notice Calculates the proportional share of available liquidity for an owner based on their share ownership.
* @dev During RUNNING stage, each lender can only claim their proportional share of partial repayments.
* @param owner The address of the share owner.
* @return The proportional amount of available liquidity the owner can claim.
*/
function _proportionalAvailableLiquidity(address owner) internal view returns (uint256) {
uint256 _totalSupply = totalSupply();
if (_totalSupply == 0) return 0;

uint256 ownerShares = balanceOf(owner);
uint256 totalLiquidity = _totalAvailableLiquidity();

// Calculate proportional share: totalLiquidity * ownerShares / totalSupply
return totalLiquidity.mulDiv(ownerShares, _totalSupply, Math.Rounding.Down);
}

function _deposit(address caller, address receiver, uint256 assets, uint256 shares) internal override {
super._deposit(caller, receiver, assets, shares);
if (aAsset != address(0)) {
Expand All @@ -286,8 +311,20 @@ contract PWNCrowdsourceLenderVault is ERC4626, IPWNLenderCreateHook, IPWNLenderR
}

function _withdraw(address caller, address receiver, address owner, uint256 assets, uint256 shares) internal override {
if (aAsset != address(0) && stage() == Stage.POOLING) {
aave.withdraw(asset(), assets, address(this));
if (aAsset != address(0)) {
Stage _stage = stage();
if (_stage == Stage.POOLING) {
// During POOLING, all assets are in Aave
aave.withdraw(asset(), assets, address(this));
} else if (_stage == Stage.RUNNING || _stage == Stage.ENDING) {
// During RUNNING/ENDING, repayments and unutilized capital are in Aave.
// Withdraw from Aave if we don't have enough cash.
// Use try/catch so withdrawals of available cash still work if Aave is down.
uint256 availableCash = _availableLiquidity();
if (availableCash < assets) {
try aave.withdraw(asset(), assets - availableCash, address(this)) {} catch {}
}
}
}
super._withdraw(caller, receiver, owner, assets, shares);
}
Expand Down Expand Up @@ -366,7 +403,7 @@ contract PWNCrowdsourceLenderVault is ERC4626, IPWNLenderCreateHook, IPWNLenderR
uint256 loanId_,
address lender,
address creditAddress,
uint256 /* principal */,
uint256 principal,
bytes calldata lenderData
) external returns (bytes32) {
require(msg.sender == address(loanContract));
Expand All @@ -378,7 +415,11 @@ contract PWNCrowdsourceLenderVault is ERC4626, IPWNLenderCreateHook, IPWNLenderR

loanId = loanId_;
if (aAsset != address(0)) {
aave.withdraw(asset(), type(uint256).max, address(this));
// Withdraw only the principal needed for the loan, keep the rest earning yield in Aave
uint256 availableCash = _availableLiquidity();
if (availableCash < principal) {
aave.withdraw(asset(), principal - availableCash, address(this));
}
}

return LENDER_CREATE_HOOK_RETURN_VALUE;
Expand All @@ -387,12 +428,19 @@ contract PWNCrowdsourceLenderVault is ERC4626, IPWNLenderCreateHook, IPWNLenderR
/** @inheritdoc IPWNLenderRepaymentHook*/
function onLoanRepaid(
address /* lender */,
address /* creditAddress */,
uint256 /* repayment */,
address creditAddress,
uint256 repayment,
bytes calldata /* lenderData */
) external pure returns (bytes32) {
// Note: no need to validate anything, the hook only accepts repayments
// This guarantees that the loan unclaimed amount is always zero
) external returns (bytes32) {
require(msg.sender == address(loanContract));

// Deposit repayment to Aave to earn yield while waiting for withdrawals/redemptions.
// Use try/catch to avoid triggering PWNLoan's unclaimedRepayment fallback if Aave is down.
// If supply fails, the repayment stays as cash in the vault.
if (aAsset != address(0)) {
try aave.supply(creditAddress, repayment, address(this), 0) {} catch {}
}

return LENDER_REPAYMENT_HOOK_RETURN_VALUE;
}

Expand Down
Loading
Loading