Intro
이번 글에서는 SY Token의 발행과 상환이 실제로 어떤 프로세스로 이루어지는지 다루도록 하겠다.
Backgrounds
Pendle Router [1]
Pendle Router(이하 Router)는 Pendle 시스템에서 다양한 SY, PT, YT 및 Market과의 상호작용을 보다 쉽게 하는 컨트랙트로, 유니스왑의 Router와 그 성격이 유사하다. 유동성 추가/제거, 토큰의 스왑(PT -> YT, YT->PT), 만기가 지난 PT의 상환을 지원한다. 또한, 최적의 가스의 효율을 위해 오프체인 데이터를 사용하며, 여러 DEX와 통합되고 Limit Order를 지원한다.
구체적으로 사용자는 IPActionMiscV3 [2]에 정의된 인터페이스를 이용하여, SY, PY 토큰을 발행, 상환을 요청할 수 있다. 또한, IPActionSwapPTV3 [3], IPActionSwapYTV3 [4]의 인터페이스를 이용하여, PT, YT, SY 토큰간 교환도 가능하게 한다.
SY Token Mint & Redeem
enum SwapType {
NONE,
KYBERSWAP,
ONE_INCH,
// ETH_WETH not used in Aggregator
ETH_WETH
}
function _mintSyFromToken(
address receiver,
address SY,
uint256 minSyOut,
TokenInput calldata inp
) internal returns (uint256 netSyOut) {
SwapType swapType = inp.swapData.swapType;
uint256 netTokenMintSy;
if (swapType == SwapType.NONE) {
_transferIn(inp.tokenIn, msg.sender, inp.netTokenIn);
netTokenMintSy = inp.netTokenIn;
} else if (swapType == SwapType.ETH_WETH) {
_transferIn(inp.tokenIn, msg.sender, inp.netTokenIn);
_wrap_unwrap_ETH(inp.tokenIn, inp.tokenMintSy, inp.netTokenIn);
netTokenMintSy = inp.netTokenIn;
} else {
_swapTokenInput(inp);
netTokenMintSy = _selfBalance(inp.tokenMintSy);
}
netSyOut = __mintSy(receiver, SY, netTokenMintSy, minSyOut, inp);
}
function _swapTokenInput(TokenInput calldata inp) internal {
if (inp.tokenIn == NATIVE) _transferIn(NATIVE, msg.sender, inp.netTokenIn);
else _transferFrom(IERC20(inp.tokenIn), msg.sender, inp.pendleSwap, inp.netTokenIn);
IPSwapAggregator(inp.pendleSwap).swap{value: inp.tokenIn == NATIVE ? inp.netTokenIn : 0}(
inp.tokenIn,
inp.netTokenIn,
inp.swapData
);
}
사용자는 mintSyFromToken
, redeemSyToToken
함수를 호출하여, SY Token을 발행 및 상환할 수 있다. mintSyFromToken
함수는 _mintSyFromToken
를 호출하는데, swapType에 따라 입력 받은 토큰의 타입에 따라 매칭이 되는 SY 토큰을 발행한다. swapType은 NONE, KYBERSWAP, ONE_INCH, ETH_WETH를 지원하는데, KYBERSWAP, ONE_INCH의 경우에는 _swapTokenInput
함수를 거쳐, Pendle Market에서 지원하는 기초자산으로 변환한다. WETH의 경우에는 ETH로 다시 변환하고, NONE의 경우에는 기초자산과 매핑되는 SY 토큰을 발행한다. 이때, 얼만큼의 SY 토큰을 발행할지는 SY 토큰마다 다르다.
//PendleAaveV3SY
function _deposit(
address tokenIn,
uint256 amountDeposited
) internal virtual override returns (uint256 amountSharesOut) {
if (tokenIn == underlying) {
IAaveV3Pool(aavePool).supply(underlying, amountDeposited, address(this), 0);
}
amountSharesOut = AaveAdapterLib.calcSharesFromAssetUp(amountDeposited, _getNormalizedIncome());
}
//sAPE
function _deposit(address, uint256 amountDeposited) internal virtual override returns (uint256 amountSharesOut) {
// Respecting APE's deposit invariant
if (amountDeposited < MIN_APE_DEPOSIT) {
revert Errors.SYApeDepositAmountTooSmall(amountDeposited);
}
_harvestAndCompound();
// As SY Base is pulling the tokenIn first, the totalAsset should exclude user's deposit
if (totalSupply() == 0) {
amountSharesOut = amountDeposited - MINIMUM_LIQUIDITY;
_mint(address(1), MINIMUM_LIQUIDITY);
} else {
uint256 priorTotalAssetOwned = getTotalAssetOwned() - amountDeposited;
amountSharesOut = (amountDeposited * totalSupply()) / priorTotalAssetOwned;
}
}
//PendleUniETHSY
function _deposit(address tokenIn, uint256 amountDeposited) internal override returns (uint256 amountSharesOut) {
if (tokenIn == NATIVE) {
amountSharesOut = IBedrockStaking(bedrockStaking).mint{value: amountDeposited}(0, type(uint256).max);
} else {
amountSharesOut = amountDeposited;
}
}
위는 PendleAaveV3SY
, sAPE
, PendleUniETHSY
의 _deposit
함수이다. PendleAaveV3SY
은 들어오는 토큰이 기초자산인 경우에는 aavePool에 공급하고, 공급된 자산을 기반으로 shares를 계산하는 반면에, sAPE
는 SY 토큰 컨트랙트가 하나의 Pool처럼 동작하는 방식을 볼 수 있다. PendleUniETHSY
이 경우에는 eth인 경우에는 BedrockStaking
에 예치하고, 기초자산인 경우에는 입력 받은 양 그대로 1:1로 민팅되는 것을 볼 수 있다.
function _redeemSyToToken(
address receiver,
address SY,
uint256 netSyIn,
TokenOutput calldata out,
bool doPull
) internal returns (uint256 netTokenOut) {
SwapType swapType = out.swapData.swapType;
if (swapType == SwapType.NONE) {
netTokenOut = __redeemSy(receiver, SY, netSyIn, out, doPull);
} else if (swapType == SwapType.ETH_WETH) {
netTokenOut = __redeemSy(address(this), SY, netSyIn, out, doPull); // ETH:WETH is 1:1
_wrap_unwrap_ETH(out.tokenRedeemSy, out.tokenOut, netTokenOut);
_transferOut(out.tokenOut, receiver, netTokenOut);
} else {
uint256 netTokenRedeemed = __redeemSy(out.pendleSwap, SY, netSyIn, out, doPull);
IPSwapAggregator(out.pendleSwap).swap(out.tokenRedeemSy, netTokenRedeemed, out.swapData);
netTokenOut = _selfBalance(out.tokenOut);
_transferOut(out.tokenOut, receiver, netTokenOut);
}
if (netTokenOut < out.minTokenOut) revert("Slippage: INSUFFICIENT_TOKEN_OUT");
}
SY 토큰을 상환할 때도 마찬가지이다. 먼저, 발행때와 마찬가지로 swapType에 따라 SY 토큰 상환을 다르게 처리한다. 이때, doPull은 라우터가 사용자로부터 SY 토큰을 approve 받아서 처리할지에 대한 플래그 값이다. SY 토큰에 해당하는 기초자산을 비율대로 소각하여, 반환한다. 단, 슬리피지가 발생할 수 있어, 슬리피지에 대한 검증을 수행한다.
Reference
- https://docs.pendle.finance/Developers/Contracts/PendleRouter
- https://github.com/pendle-finance/pendle-core-v2-public/blob/main/contracts/interfaces/IPActionMiscV3.sol
- https://github.com/pendle-finance/pendle-core-v2-public/blob/77b3630c82412b580bce6cd4a32f2c385bbb7970/contracts/interfaces/IPActionSwapPTV3.sol#L46-L69
- https://github.com/pendle-finance/pendle-core-v2-public/blob/77b3630c82412b580bce6cd4a32f2c385bbb7970/contracts/interfaces/IPActionSwapYTV3.sol#L45-L93