mintPY
mintPY function tokenizes yield-bearing assets (SY) into Principal Tokens (PT) and Yield Tokens (YT).The user first deposits SY tokens into the YT contract, then calls
mintPY to mint an equivalent amount of PT and YT, based on the underlying asset value represented by the deposited SY. Amount of PT and YT equals the amount of amount of underlying asset represented by SY._pyIndexCurrentqueries exchange rate of SY and underlying asset from SY contract.
- YT contract invokes PT contract to mint PT.
- User can only mint PT and YT when it is not expired.
/// --- pendle-core-v2-public/contracts/core/YieldContracts/PendleYieldToken.sol --- address public immutable SY; address public immutable PT; modifier notExpired() { if (isExpired()) revert Errors.YCExpired(); _; } modifier updateData() { if (isExpired()) _setPostExpiryData(); _; _updateSyReserve(); } /** * @notice Tokenize SY into PT + YT of equal qty. Every unit of asset of SY will create 1 PT + 1 YT * @dev SY must be transferred to this contract prior to calling */ function mintPY( address receiverPT, address receiverYT ) external nonReentrant notExpired updateData returns (uint256 amountPYOut) { address[] memory receiverPTs = new address[](1); address[] memory receiverYTs = new address[](1); uint256[] memory amountSyToMints = new uint256[](1); (receiverPTs[0], receiverYTs[0], amountSyToMints[0]) = (receiverPT, receiverYT, _getFloatingSyAmount()); uint256[] memory amountPYOuts = _mintPY(receiverPTs, receiverYTs, amountSyToMints); amountPYOut = amountPYOuts[0]; } function _getFloatingSyAmount() internal view returns (uint256 amount) { amount = _selfBalance(SY) - syReserve; if (amount == 0) revert Errors.YCNoFloatingSy(); } function _mintPY( address[] memory receiverPTs, address[] memory receiverYTs, uint256[] memory amountSyToMints ) internal returns (uint256[] memory amountPYOuts) { amountPYOuts = new uint256[](amountSyToMints.length); uint256 index = _pyIndexCurrent(); for (uint256 i = 0; i < amountSyToMints.length; i++) { amountPYOuts[i] = _calcPYToMint(amountSyToMints[i], index); _mint(receiverYTs[i], amountPYOuts[i]); IPPrincipalToken(PT).mintByYT(receiverPTs[i], amountPYOuts[i]); emit Mint(msg.sender, receiverPTs[i], receiverYTs[i], amountSyToMints[i], amountPYOuts[i]); } } function _pyIndexCurrent() internal returns (uint256 currentIndex) { if (doCacheIndexSameBlock && pyIndexLastUpdatedBlock == block.number) return _pyIndexStored; uint128 index128 = PMath.max(IStandardizedYield(SY).exchangeRate(), _pyIndexStored).Uint128(); currentIndex = index128; _pyIndexStored = index128; pyIndexLastUpdatedBlock = uint128(block.number); emit NewInterestIndex(currentIndex); } /// --- pendle-core-v2-public/contracts/core/libraries/TokenHelper.sol --- function _selfBalance(address token) internal view returns (uint256) { return (token == NATIVE) ? address(this).balance : IERC20(token).balanceOf(address(this)); }
_calcPYToMint
/// --- pendle-core-v2-public/contracts/core/StandardizedYield/SYUtils.sol --- uint256 internal constant ONE = 1e18; function _calcPYToMint(uint256 amountSy, uint256 indexCurrent) internal pure returns (uint256 amountPY) { // doesn't matter before or after expiry, since mintPY is only allowed before expiry return SYUtils.syToAsset(indexCurrent, amountSy); } function syToAsset(uint256 exchangeRate, uint256 syAmount) internal pure returns (uint256) { return (syAmount * exchangeRate) / ONE; }
updateData
updateData modifier checks whether YT is expired, if it is, it invokes _setPostExpiryData function to set post-expiry data.After
mintPY is executed, it updates syReserve to reflect current SY balance./// --- pendle-core-v2-public/contracts/core/YieldContracts/PendleYieldToken.sol --- modifier updateData() { if (isExpired()) _setPostExpiryData(); _; _updateSyReserve(); } function isExpired() public view returns (bool) { return MiniHelpers.isCurrentlyExpired(expiry); } function _updateSyReserve() internal virtual { syReserve = _selfBalance(SY); }
_setPostExpiryData
_setPostExpiryData claims external rewards of yield-bearing token, and records reward status.- It can only be executed once after expiry
- It calls
SY.claimRewardsto claim rewards
- It queries reward tokens and reward indexes from SY contract, and records it.
- Future reward on yield-bearing token belongs to treasury.
/// --- pendle-core-v2-public/contracts/core/YieldContracts/PendleYieldToken.sol --- struct PostExpiryData { uint128 firstPYIndex; uint128 totalSyInterestForTreasury; mapping(address => uint256) firstRewardIndex; mapping(address => uint256) userRewardOwed; } function _setPostExpiryData() internal { PostExpiryData storage local = postExpiry; if (local.firstPYIndex != 0) return; // already set _redeemExternalReward(); // do a final redeem. All the future reward income will belong to the treasury local.firstPYIndex = _pyIndexCurrent().Uint128(); address[] memory rewardTokens = IStandardizedYield(SY).getRewardTokens(); uint256[] memory rewardIndexes = IStandardizedYield(SY).rewardIndexesCurrent(); for (uint256 i = 0; i < rewardTokens.length; i++) { local.firstRewardIndex[rewardTokens[i]] = rewardIndexes[i]; local.userRewardOwed[rewardTokens[i]] = _selfBalance(rewardTokens[i]); } } function _redeemExternalReward() internal virtual override { IStandardizedYield(SY).claimRewards(address(this)); }
_mint
_mint function in PendleERC20 executes _beforeTokenTransfer before mint operation. _beforeTokenTransfer updates reward and interest related information of from and to to ensure correctness of reward and interest calculation./// --- pendle-core-v2-public/contracts/core/erc20/PendleERC20.sol --- /** @dev Creates `amount` tokens and assigns them to `account`, increasing * the total supply. * * Emits a {Transfer} event with `from` set to the zero address. * * Requirements: * * - `account` cannot be the zero address. */ function _mint(address account, uint256 amount) internal virtual { require(account != address(0), "ERC20: mint to the zero address"); _beforeTokenTransfer(address(0), account, amount); _totalSupply += toUint248(amount); _balances[account] += amount; emit Transfer(address(0), account, amount); _afterTokenTransfer(address(0), account, amount); }
redeemPY
- before expiry, user needs to burn same amount of PT and YT to redeem.
- after expiry, user only needs to burn PT to redeem.
- when the state is set as expired,
firstPYIndexis set which is used to calculate the final exchange rate between SY/yield-bearing token and underlying asset. As SY token keeps generating yield (exchange rate keeps increasing), if PT delays redeem, pendle protocol pays less SY to refund user’s PT, which means pendle protocol accumulates SY as interest. If PT and PY redeem right in the expiry time, pendle protocol is left no interest. So the interest is:
/// --- pendle-core-v2-public/contracts/core/YieldContracts/PendleYieldToken.sol --- /** * @notice converts PT(+YT) tokens into SY, but interests & rewards are not redeemed at the * same time * @dev PT/YT must be transferred to this contract prior to calling */ function redeemPY(address receiver) external nonReentrant updateData returns (uint256 amountSyOut) { address[] memory receivers = new address[](1); uint256[] memory amounts = new uint256[](1); (receivers[0], amounts[0]) = (receiver, _getAmountPYToRedeem()); uint256[] memory amountSyOuts; amountSyOuts = _redeemPY(receivers, amounts); amountSyOut = amountSyOuts[0]; } function _getAmountPYToRedeem() internal view returns (uint256) { if (!isExpired()) return PMath.min(_selfBalance(PT), balanceOf(address(this))); else return _selfBalance(PT); } function _redeemPY( address[] memory receivers, uint256[] memory amountPYToRedeems ) internal returns (uint256[] memory amountSyOuts) { uint256 totalAmountPYToRedeem = amountPYToRedeems.sum(); IPPrincipalToken(PT).burnByYT(address(this), totalAmountPYToRedeem); if (!isExpired()) _burn(address(this), totalAmountPYToRedeem); uint256 index = _pyIndexCurrent(); uint256 totalSyInterestPostExpiry; amountSyOuts = new uint256[](receivers.length); for (uint256 i = 0; i < receivers.length; i++) { uint256 syInterestPostExpiry; (amountSyOuts[i], syInterestPostExpiry) = _calcSyRedeemableFromPY(amountPYToRedeems[i], index); _transferOut(SY, receivers[i], amountSyOuts[i]); totalSyInterestPostExpiry += syInterestPostExpiry; emit Burn(msg.sender, receivers[i], amountPYToRedeems[i], amountSyOuts[i]); } if (totalSyInterestPostExpiry != 0) { postExpiry.totalSyInterestForTreasury += totalSyInterestPostExpiry.Uint128(); } } function _calcSyRedeemableFromPY( uint256 amountPY, uint256 indexCurrent ) internal view returns (uint256 syToUser, uint256 syInterestPostExpiry) { syToUser = SYUtils.assetToSy(indexCurrent, amountPY); if (isExpired()) { uint256 totalSyRedeemable = SYUtils.assetToSy(postExpiry.firstPYIndex, amountPY); syInterestPostExpiry = totalSyRedeemable - syToUser; } } /// --- pendle-core-v2-public/contracts/core/StandardizedYield/SYUtils.sol --- function assetToSy(uint256 exchangeRate, uint256 assetAmount) internal pure returns (uint256) { return (assetAmount * ONE) / exchangeRate; }
redeemDueInterestAndRewards
redeemDueInterestAndRewards allows YT holder to redeem underlying interest and reward.- Interest is measured using SY rather than underlying token. This is an elegant way because it relieves the burden to redeem interest using SY and distribute to user which may be not allowed for some protocol.
- As SY can claim interest in underlying protocol, pendle only needs to calculates equivalent SY to represent interest and distribute to user.
- The deposited SY from user is guaranteed to cover all potential interest generated and the principal. So pendle doesnt need to worry that it doesn’t have enough SY to cover user’s principal and interest.
- Some protocol may generate additional reward.
PendleYieldTokenassumes underlying protocol uses SY token staked to distribute reward. So it uses SY balance to record each user’s share to those rewards. - Note that interest generated is converteed to SY which is recorded in
userInterest[user].accrued, which is also counted as share to claim additional reward. - As reward distribution is based on shares (SY) of user, and interest distribution change SY balance of user, so reward distribution needs to be executed before interest distribution.
/// --- pendle-core-v2-public/contracts/core/YieldContracts/PendleYieldToken.sol --- /** * @notice Redeems interests and rewards for `user` * @param redeemInterest will only transfer out interest for user if true * @param redeemRewards will only transfer out rewards for user if true * @dev With YT yielding interest in the form of SY, which is redeemable by users, the reward * distribution should be based on the amount of SYs that their YT currently represent, plus * their dueInterest. It has been proven and tested that _rewardSharesUser will not change over * time, unless users redeem their dueInterest or redeemPY. Due to this, it is required to * update users' accruedReward STRICTLY BEFORE transferring out their interest. */ function redeemDueInterestAndRewards( address user, bool redeemInterest, bool redeemRewards ) external nonReentrant updateData returns (uint256 interestOut, uint256[] memory rewardsOut) { if (!redeemInterest && !redeemRewards) revert Errors.YCNothingToRedeem(); // if redeemRewards == true, this line must be here for obvious reason // if redeemInterest == true, this line must be here because of the reason above _updateAndDistributeRewards(user); if (redeemRewards) { rewardsOut = _doTransferOutRewards(user, user); emit RedeemRewards(user, rewardsOut); } else { address[] memory tokens = getRewardTokens(); rewardsOut = new uint256[](tokens.length); } if (redeemInterest) { _distributeInterest(user); interestOut = _doTransferOutInterest(user, SY, factory); emit RedeemInterest(user, interestOut); } else { interestOut = 0; } }
_updateAndDistributeRewards
_updateAndDistributeRewards function update current reward index, and calcualte newly accumulated reward of user based on share (SY).Concepts:
- reward index recorded in underlying protocol represents accumulated reward per token.
- pendle also records user’s reward index to calculate accumulated reward per user. When users claim reward, their reward index is updated to current index to prevent duplicate claim.
Steps:
- It calls
_updateRewardIndexfunction to get reward token and current reward indexes from underlying protocol. When YT expires, the reward index at that time is recorded inpostExpiry.firstRewardIndex, user can only claim reward accumuated till that time.
- it calls
_rewardSharesUserfunction to calculate user’s current share (SY balance).
If user’s interest index is zero (
userInterest[user].index== 0), which means user hasn’t received any YT before and has no accrued interest yet. Therefore there is no newly accumulated reward of user, so it just returns zero reward index to represent this situation.- accrued but not claimed reward of user is recorded in
userReward[token][user].accrued
/// --- pendle-core-v2-public/contracts/core/RewardManager/RewardManagerAbstract.sol --- uint256 internal constant INITIAL_REWARD_INDEX = 1; struct UserReward { uint128 index; uint128 accrued; } // [token] => [user] => (index,accrued) mapping(address => mapping(address => UserReward)) public userReward; function _updateAndDistributeRewards(address user) internal virtual { _updateAndDistributeRewardsForTwo(user, address(0)); } function _updateAndDistributeRewardsForTwo(address user1, address user2) internal virtual { (address[] memory tokens, uint256[] memory indexes) = _updateRewardIndex(); if (tokens.length == 0) return; if (user1 != address(0) && user1 != address(this)) _distributeRewardsPrivate(user1, tokens, indexes); if (user2 != address(0) && user2 != address(this)) _distributeRewardsPrivate(user2, tokens, indexes); } // should only be callable from `_updateAndDistributeRewardsForTwo` to guarantee user != address(0) && user != address(this) function _distributeRewardsPrivate(address user, address[] memory tokens, uint256[] memory indexes) private { assert(user != address(0) && user != address(this)); uint256 userShares = _rewardSharesUser(user); for (uint256 i = 0; i < tokens.length; ++i) { address token = tokens[i]; uint256 index = indexes[i]; uint256 userIndex = userReward[token][user].index; if (userIndex == 0) { userIndex = INITIAL_REWARD_INDEX.Uint128(); } if (userIndex == index || index == 0) continue; uint256 deltaIndex = index - userIndex; uint256 rewardDelta = userShares.mulDown(deltaIndex); uint256 rewardAccrued = userReward[token][user].accrued + rewardDelta; userReward[token][user] = UserReward({index: index.Uint128(), accrued: rewardAccrued.Uint128()}); } } /// --- pendle-core-v2-public/contracts/core/YieldContracts/PendleYieldToken.sol --- function _updateRewardIndex() internal override returns (address[] memory tokens, uint256[] memory indexes) { tokens = getRewardTokens(); if (isExpired()) { indexes = new uint256[](tokens.length); for (uint256 i = 0; i < tokens.length; i++) indexes[i] = postExpiry.firstRewardIndex[tokens[i]]; } else { indexes = IStandardizedYield(SY).rewardIndexesCurrent(); } } function getRewardTokens() public view returns (address[] memory) { return IStandardizedYield(SY).getRewardTokens(); } /// @dev effectively returning the amount of SY generating rewards for this user function _rewardSharesUser(address user) internal view virtual override returns (uint256) { uint256 index = userInterest[user].index; if (index == 0) return 0; return SYUtils.assetToSy(index, balanceOf(user)) + userInterest[user].accrued; }
_doTransferOutRewards
_doTransferOutRewards transfers accumulated reward to user, and handles fee.- When an SY expires,
PendleYieldTokenclaims all remaining rewards and records them inpostExpiry.userRewardOwed, which represents the total undistributed rewards. When a user later claims their share, the claimed amount is deducted frompostExpiry.userRewardOwed.
- It calls
__doTransferOutRewardsLocalfunction to transfers reward and fee to user and treasury.
/// --- pendle-core-v2-public/contracts/core/YieldContracts/PendleYieldToken.sol --- function _doTransferOutRewards( address user, address receiver ) internal virtual override returns (uint256[] memory rewardAmounts) { address[] memory tokens = getRewardTokens(); if (isExpired()) { // post-expiry, all incoming rewards will go to the treasury // hence, we can save users one _redeemExternal here for (uint256 i = 0; i < tokens.length; i++) { uint256 owed = postExpiry.userRewardOwed[tokens[i]]; uint256 accrued = userReward[tokens[i]][user].accrued; postExpiry.userRewardOwed[tokens[i]] = (owed < accrued) ? 0 : owed - accrued; } rewardAmounts = __doTransferOutRewardsLocal(tokens, user, receiver, false); } else { rewardAmounts = __doTransferOutRewardsLocal(tokens, user, receiver, true); } } function __doTransferOutRewardsLocal( address[] memory tokens, address user, address receiver, bool allowedToRedeemExternalReward ) internal returns (uint256[] memory rewardAmounts) { address treasury = IPYieldContractFactory(factory).treasury(); uint256 feeRate = IPYieldContractFactory(factory).rewardFeeRate(); bool redeemExternalThisRound; rewardAmounts = new uint256[](tokens.length); for (uint256 i = 0; i < tokens.length; i++) { uint256 rewardPreFee = userReward[tokens[i]][user].accrued; userReward[tokens[i]][user].accrued = 0; uint256 feeAmount = rewardPreFee.mulDown(feeRate); rewardAmounts[i] = rewardPreFee - feeAmount; if (!redeemExternalThisRound && allowedToRedeemExternalReward) { if (_selfBalance(tokens[i]) < rewardPreFee) { _redeemExternalReward(); redeemExternalThisRound = true; } } _transferOut(tokens[i], treasury, feeAmount); _transferOut(tokens[i], receiver, rewardAmounts[i]); emit CollectRewardFee(tokens[i], feeAmount); } } function _redeemExternalReward() internal virtual override { IStandardizedYield(SY).claimRewards(address(this)); } /// --- pendle-core-v2-public/contracts/core/libraries/TokenHelper.sol --- function _transferOut(address token, address to, uint256 amount) internal { if (amount == 0) return; if (token == NATIVE) { (bool success, ) = to.call{value: amount}(""); require(success, "eth send failed"); } else { IERC20(token).safeTransfer(to, amount); } }
_distributeInterest
_distributeInterest function calculates interest accumualted by user and records it.- interest is generated by underlying SY represented by YT, which is measured in SY.
- is previous SY balance of user which is used to generate interest
- is the accumulated interest from underlying SY from last update time
- by dividing , we get interest measured in SY.
/// --- pendle-core-v2-public/contracts/core/YieldContracts/InterestManagerYT.sol --- struct UserInterest { uint128 index; uint128 accrued; } mapping(address => UserInterest) public userInterest; function _distributeInterest(address user) internal { _distributeInterestForTwo(user, address(0)); } function _distributeInterestForTwo(address user1, address user2) internal { uint256 index = _getInterestIndex(); if (user1 != address(0) && user1 != address(this)) _distributeInterestPrivate(user1, index); if (user2 != address(0) && user2 != address(this)) _distributeInterestPrivate(user2, index); } // should only be callable from `_distributeInterestForTwo` & make sure user != address(0) && user != address(this) function _distributeInterestPrivate(address user, uint256 currentIndex) private { assert(user != address(0) && user != address(this)); uint256 prevIndex = userInterest[user].index; if (prevIndex == currentIndex) return; if (prevIndex == 0) { userInterest[user].index = currentIndex.Uint128(); return; } uint256 principal = _YTbalance(user); uint256 interestFromYT = (principal * (currentIndex - prevIndex)).divDown(prevIndex * currentIndex); userInterest[user].accrued += interestFromYT.Uint128(); userInterest[user].index = currentIndex.Uint128(); } /// --- pendle-core-v2-public/contracts/core/YieldContracts/PendleYieldToken.sol --- function _YTbalance(address user) internal view override returns (uint256) { return balanceOf(user); }
_doTransferOutInterest
_doTransferOutRewards transfers accumulated interest to user, and handles fee./// --- pendle-core-v2-public/contracts/core/YieldContracts/InterestManagerYT.sol --- function _doTransferOutInterest( address user, address SY, address factory ) internal returns (uint256 interestAmount) { address treasury = IPYieldContractFactory(factory).treasury(); uint256 feeRate = IPYieldContractFactory(factory).interestFeeRate(); uint256 interestPreFee = userInterest[user].accrued; userInterest[user].accrued = 0; uint256 feeAmount = interestPreFee.mulDown(feeRate); interestAmount = interestPreFee - feeAmount; _transferOut(SY, treasury, feeAmount); _transferOut(SY, user, interestAmount); emit CollectInterestFee(feeAmount); }
redeemInterestAndRewardsPostExpiryForTreasury
redeemInterestAndRewardsPostExpiryForTreasury claims additional reward and interest not belongs to users to treasury./// --- pendle-core-v2-public/contracts/core/YieldContracts/PendleYieldToken.sol --- /** * @dev All rewards and interests accrued post-expiry goes to the treasury. * Reverts if called pre-expiry. */ function redeemInterestAndRewardsPostExpiryForTreasury() external nonReentrant updateData returns (uint256 interestOut, uint256[] memory rewardsOut) { if (!isExpired()) revert Errors.YCNotExpired(); address treasury = IPYieldContractFactory(factory).treasury(); address[] memory tokens = getRewardTokens(); rewardsOut = new uint256[](tokens.length); _redeemExternalReward(); for (uint256 i = 0; i < tokens.length; i++) { rewardsOut[i] = _selfBalance(tokens[i]) - postExpiry.userRewardOwed[tokens[i]]; emit CollectRewardFee(tokens[i], rewardsOut[i]); } _transferOut(tokens, treasury, rewardsOut); interestOut = postExpiry.totalSyInterestForTreasury; postExpiry.totalSyInterestForTreasury = 0; _transferOut(SY, treasury, interestOut); emit CollectInterestFee(interestOut); }
_beforeTokenTransfer
_beforeTokenTransfer updates reward and interest related information of from and to to ensure correctness of reward and interest calculation._beforeTokenTransfercalls_updateAndDistributeRewardsForTwoto distribute reward forfromandto, and update reward indexes offromandto
_beforeTokenTransfercalls_distributeInterestForTwoto distribute interest accumulated so far forfromandto, and update interest indexes offromandto
For example, if user A transfers YT to user B, it needs to distribute interest accumualted by user A and B first, and updates interest index of user B. So that user B can only claim future interest generated by those YT sent from user A. Same applies to reward.
/// --- pendle-core-v2-public/contracts/core/YieldContracts/PendleYieldToken.sol --- //solhint-disable-next-line ordering function _beforeTokenTransfer(address from, address to, uint256) internal override { if (isExpired()) _setPostExpiryData(); _updateAndDistributeRewardsForTwo(from, to); _distributeInterestForTwo(from, to); }