Platform: Code4rena
Start Date: 23/02/2024
Pot Size: $36,500 USDC
Total HM: 2
Participants: 39
Period: 7 days
Judge: Dravee
Id: 338
League: ETH
Rank: 9/39
Findings: 2
Award: $272.07
π Selected for report: 0
π Solo Findings: 0
107.431 USDC - $107.43
https://github.com/code-423n4/2024-02-spectra/blob/383202d0b84985122fe1ba53cfbbb68f18ba3986/src/tokens/PrincipalToken.sol#L229-L237 https://github.com/code-423n4/2024-02-spectra/blob/383202d0b84985122fe1ba53cfbbb68f18ba3986/src/tokens/PrincipalToken.sol#L278-L287 https://github.com/code-423n4/2024-02-spectra/blob/383202d0b84985122fe1ba53cfbbb68f18ba3986/src/tokens/PrincipalToken.sol#L493-L495 https://github.com/code-423n4/2024-02-spectra/blob/383202d0b84985122fe1ba53cfbbb68f18ba3986/src/tokens/PrincipalToken.sol#L488-L490 https://github.com/code-423n4/2024-02-spectra/blob/383202d0b84985122fe1ba53cfbbb68f18ba3986/src/tokens/PrincipalToken.sol#L460-L462
Principal Token contract is an ERC5095 vault that allows users to tokenize their yield in a permissionless manner.
Important The contest page explicitly states that
PrincipalToken
must conform with the EIP-5095.
The following specification of redeem()
is taken directly from the EIP-5095:
At or after maturity, burns exactly principalAmount of Principal Tokens from
from
and sends underlyingAmount of underlying tokens toto
. MUST support a redeem flow where the Principal Tokens are burned fromholder
directly whereholder
ismsg.sender
ormsg.sender
has EIP-20 approval over the principal tokens ofholder
.
The following specification of withdraw()
is taken directly from the EIP-5095:
Burns
principalAmount
fromholder
and sends exactlyunderlyingAmount
of underlying tokens toreceiver
. MUST support a withdraw flow where the principal tokens are burned fromholder
directly whereholder
ismsg.sender
ormsg.sender
has EIP-20 approval over the principal tokens ofholder
.
Both these functions should allow an approved user to redeem or withdraw approved Principal tokens, but currently, that's impossible. All redeem
functions internally call _beforeRedeem(), which has this check in place:
if (_owner != msg.sender) { revert UnauthorizedCaller(); }
The same can be said about every withdraw
function. All of them call _beforeWithdraw(), which has the same check in place.
As per EIP-5095 both convertToUnderlying()
and convertToPrincipal()
functions MUST NOT revert unless due to integer overflow caused by an unreasonably large input.
In case of convertToUnderlying()
function, it calls _convertSharesToIBTs(), which can revert with RateError
when ibtRate
is 0.
In case of convertToPrincipal()
function, it calls _convertIBTsToShares(), which can revert with RateError
when ptRate
is 0.
As per EIP-5095 maxWithdraw()
must not revert, but it can revert because of whenNotPausedModifier
or because of the RateError
mentioned above.
PrincipalToken contract does not respect the EIP standards it claims to follow. This breaks compatibility and violates invariants stated in the README.
Ensure that PrincipalToken is EIP-compliant. Alternatively, document that some functions deviate from the ERC5095 specification.
Other
#0 - c4-pre-sort
2024-03-03T09:18:55Z
gzeon-c4 marked the issue as duplicate of #33
#1 - c4-pre-sort
2024-03-03T09:19:19Z
gzeon-c4 marked the issue as sufficient quality report
#2 - c4-judge
2024-03-11T00:32:44Z
JustDravee marked the issue as satisfactory
π Selected for report: SBSecurity
Also found by: 0xDemon, 0xLuckyLuke, 14si2o_Flint, ArmedGoose, DarkTower, Shubham, Tigerfrake, cheatc0d3, peanuts, sl1
164.6381 USDC - $164.64
https://github.com/code-423n4/2024-02-spectra/blob/383202d0b84985122fe1ba53cfbbb68f18ba3986/src/tokens/PrincipalToken.sol#L369-L374 https://github.com/code-423n4/2024-02-spectra/blob/383202d0b84985122fe1ba53cfbbb68f18ba3986/src/tokens/PrincipalToken.sol#L329-L337
claimFees()
function allows the fee collector set by the protocol to claim fees in IBT.
claimFees()
function claimFees() external override returns (uint256 assets) { if (msg.sender != IRegistry(registry).getFeeCollector()) { revert UnauthorizedCaller(); } uint256 ibts = unclaimedFeesInIBT; unclaimedFeesInIBT = 0; assets = IERC4626(ibt).redeem(ibts, msg.sender, address(this)); emit FeeClaimed(msg.sender, ibts, assets); }
claimYield()
allows users to claim their yield in IBT.
claimYield()
function claimYield(address _receiver) public override returns (uint256 yieldInAsset) { uint256 yieldInIBT = _claimYield(); if (yieldInIBT != 0) { yieldInAsset = IERC4626(ibt).redeem(yieldInIBT, _receiver, address(this)); } }
As can be seen, both these functions redeem ibts from the ERC4626 vault, but they lack minAssets
check. To protect users or feeCollector from receiving less assets for their amount of ibts, minAssets
parameter should be used.
The entire amount of ibts can be lost.
- function claimYield(address _receiver) public override returns (uint256 yieldInAsset) { + function claimYield(address _receiver, uint256 minAssets) public override returns (uint256 yieldInAsset) { uint256 yieldInIBT = _claimYield(); if (yieldInIBT != 0) { yieldInAsset = IERC4626(ibt).redeem(yieldInIBT, _receiver, address(this)); + require(yieldInAsset >= minAssets); } }
- function claimFees() external override returns (uint256 assets) { + function claimFees(uint256 minAssets) external override returns (uint256 assets) { if (msg.sender != IRegistry(registry).getFeeCollector()) { revert UnauthorizedCaller(); } uint256 ibts = unclaimedFeesInIBT; unclaimedFeesInIBT = 0; assets = IERC4626(ibt).redeem(ibts, msg.sender, address(this)); + require(assets >= minAssets); emit FeeClaimed(msg.sender, ibts, assets); }
Other
#0 - c4-pre-sort
2024-03-03T12:04:10Z
gzeon-c4 marked the issue as insufficient quality report
#1 - gzeon-c4
2024-03-03T12:05:25Z
unlikely for "claim slippage" to exists and the protocol should record claimed amount lack poc to show exploit
#2 - c4-pre-sort
2024-03-03T12:05:31Z
gzeon-c4 marked the issue as primary issue
#3 - c4-judge
2024-03-11T01:07:07Z
JustDravee marked the issue as unsatisfactory: Insufficient proof
#4 - c4-judge
2024-03-11T01:42:37Z
JustDravee marked the issue as unsatisfactory: Invalid
#5 - kazantseff
2024-03-12T08:47:13Z
Hey @JustDravee, The PT contract is built on top of the ERC4626 vault and the whole idea of it, is that rates are subject to volatility, so the claim slippage will exist. redeem function of the PT contract has slippage protection and it does exactly the same action as two of the function described here. Could you please take another look?
#6 - c4-sponsor
2024-03-14T13:08:19Z
yanisepfl (sponsor) disputed
#7 - c4-sponsor
2024-03-14T13:08:23Z
yanisepfl marked the issue as disagree with severity
#8 - yanisepfl
2024-03-14T13:13:24Z
Hello @kazantseff and @JustDravee,
While the issue reported is a good catch that we will tackle, it only points out an inconsistency in our code. The standards that we follow do not necessitate to have slippage protection, and we consider it as a good-to-have feature.
Therefore, we rather consider it as a low severity issue.
Thanks for the report!
#9 - c4-judge
2024-03-14T21:47:29Z
JustDravee removed the grade
#10 - c4-judge
2024-03-14T21:47:35Z
JustDravee changed the severity to QA (Quality Assurance)
#11 - c4-judge
2024-03-14T21:47:43Z
JustDravee marked the issue as grade-b
#12 - c4-judge
2024-03-15T11:40:44Z
JustDravee marked the issue as grade-a