Platform: Code4rena
Start Date: 16/12/2021
Pot Size: $100,000 USDC
Total HM: 21
Participants: 25
Period: 7 days
Judge: alcueca
Total Solo HM: 12
Id: 66
League: ETH
Rank: 4/25
Findings: 6
Award: $10,797.78
π Selected for report: 10
π Solo Findings: 2
π Selected for report: hyh
5312.9473 USDC - $5,312.95
hyh
The condition isn't checked now as the whole balance is used instead of the Yeti tokens bought back from the market.
As it's not checked, the amount added to effectiveYetiTokenBalance
during rebase can exceed the actual amount of the Yeti tokens owned by the contract.
As the before check amount is calculated as the contract net worth, it can be fixed by immediate buy back, but it will not be the case.
The deficit of Yeti tokens can materialize in net worth terms as well if Yeti tokens price will raise compared to the last used one. In this case users will be cumulatively accounted with the amount of tokens that cannot be actually withdrawn from the contract, as its net holdings will be less then total usersβ claims. In other words, the contract will be in default if enough users claim after that.
Now the whole balance amount is used instead of the amount bought back from market.
Rebasing amount is added to effectiveYetiTokenBalance
, so it should be limited by extra Yeti tokens, not the whole balance:
https://github.com/code-423n4/2021-12-yetifinance/blob/main/packages/contracts/contracts/YETI/sYETIToken.sol#L247
It looks like only extra tokens should be used for the check, i.e. yetiToken.balance - effectiveYetiTokenBalance
.
Now:
function rebase() external { ... uint256 yetiTokenBalance = yetiToken.balanceOf(address(this)); uint256 valueOfContract = _getValueOfContract(yetiTokenBalance); uint256 additionalYetiTokenBalance = ... if (yetiTokenBalance < additionalYetiTokenBalance) { additionalYetiTokenBalance = yetiTokenBalance; } effectiveYetiTokenBalance = effectiveYetiTokenBalance.add(additionalYetiTokenBalance); ... function _getValueOfContract(uint _yetiTokenBalance) internal view returns (uint256) { uint256 adjustedYetiTokenBalance = _yetiTokenBalance.sub(effectiveYetiTokenBalance); uint256 yusdTokenBalance = yusdToken.balanceOf(address(this)); return div(lastBuybackPrice.mul(adjustedYetiTokenBalance), (1e18)).add(yusdTokenBalance); }
As the _getValueOfContract
function isn't used elsewhere, the logic can be simplified.
To be:
function rebase() external { ... uint256 adjustedYetiTokenBalance = (yetiToken.balanceOf(address(this))).sub(effectiveYetiTokenBalance); uint256 valueOfContract = _getValueOfContract(adjustedYetiTokenBalance); uint256 additionalYetiTokenBalance = ... if (additionalYetiTokenBalance > adjustedYetiTokenBalance) { additionalYetiTokenBalance = adjustedYetiTokenBalance; } effectiveYetiTokenBalance = effectiveYetiTokenBalance.add(additionalYetiTokenBalance); ... function _getValueOfContract(uint _adjustedYetiTokenBalance) internal view returns (uint256) { uint256 yusdTokenBalance = yusdToken.balanceOf(address(this)); return div(lastBuybackPrice.mul(_adjustedYetiTokenBalance), (1e18)).add(yusdTokenBalance); }
#0 - kingyetifinance
2022-01-05T06:44:09Z
@LilYeti:
This is the logic for the fix which we have already done:
if (yetiTokenBalance - effectiveYetiTokenBalance < additionalYetiTokenBalance)
Will look into this again before confirming as fixed to see if it is the same as the suggested error.
#1 - 0xtruco
2022-01-11T10:28:00Z
430.3487 USDC - $430.35
hyh
Stale 'carried over' price can be used for liquidations. This can cause various types of malfunctions and manipulated liquidations.
For example, if a portfolio consists of two inversely correlated assets, which move in opposite directions most of the times, and portfolio owner uses that knowledge to borrow against such a composition. Now first asset price is stale, second one is updated and it is in decline, so the link between assets looks to be broken and the system treats the portfolio as under collateralized, while in reality the inverse correlation holds, first asset market price rose to fully compensate the decline of the second asset price, and portfolio value isn't declined and no liquidation should take place.
Prices used in liquidations are obtained from PriceFeed contract, call sequence is as follows: Trove and Pool functions -> Whitelist.getValueVC -> Whitelist.getPrice -> PriceFeed.fetchPrice_v -> PriceFeed._getCurrentChainlinkResponse
PriceFeed determines that the price is usable via _badChainlinkResponse function. _badChainlinkResponse checks that timestamp is valid (not 0, not in the future), but do not check that it is recent: https://github.com/code-423n4/2021-12-yetifinance/blob/main/packages/contracts/contracts/PriceFeed.sol#L578
Moreover, _getCurrentChainlinkResponse function which provides core price structure used, ignores answeredInRound returned by ChainLink in the first place: https://github.com/code-423n4/2021-12-yetifinance/blob/main/packages/contracts/contracts/PriceFeed.sol#L756
Chainlink feed has two properties that can help determine if the answer is actual (price is fresh): updatedAt time https://docs.chain.link/docs/faq/#what-is-the-difference-between-the-data-feed-properties-updatedat-and-answeredinround and answeredInRound: https://docs.chain.link/docs/faq/#how-can-i-check-if-the-answer-to-a-round-is-being-carried-over-from-a-previous-round
This way ChainLink roundId isn't checked in PriceFeed contract used for price discovery and carried over price is treated as fresh in all VC related computations.
Liquidation mechanics is a core system functionality which is based on current market price observations. It is crucial to ensure that these observations are sound.
Best practice is to check that answeredInRound is equal to roundId and this way the price obtained is fresh and can be used for decision making. It's advised to save answeredInRound and check it against roundId returned, and use the price in PriceFeed response only if rounds match, treating it as stale otherwise.
It is advised to allow state changing operations only when the price is recent, i.e. it is better to reject an operation than to process it using stale parameter.
#0 - kingyetifinance
2022-01-07T08:08:28Z
@LilYeti : Duplicate #91 . Thorough suggestion though, severity recommended medium due to rarity of this case.
#1 - alcueca
2022-01-15T16:17:14Z
Medium Severity is a good fit, because the assets are not a direct risk, something else needs to happen first.
π Selected for report: hyh
1593.8842 USDC - $1,593.88
hyh
Transactions will not be reverted on failed transfer call, setting system state as if it was successful. This will lead to wrong state accounting down the road with a wide spectrum of possible consequences.
_safeJoeTransfer do not check for JOE.transfer call success: https://github.com/code-423n4/2021-12-yetifinance/blob/main/packages/contracts/contracts/AssetWrappers/WJLP/WJLP.sol#L268
_safeJoeTransfer is called by _sendJoeReward, which is used in reward claiming.
JOE token use transfer from OpenZeppelin ERC20: https://github.com/traderjoe-xyz/joe-core/blob/main/contracts/JoeToken.sol#L9
Which does return success code: https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/token/ERC20/ERC20.sol#L113
Trader Joe also uses checked transfer when dealing with JOE tokens: https://github.com/traderjoe-xyz/joe-core/blob/main/contracts/MasterChefJoeV3.sol#L102
Also, unwrapFor do not check for JLP.transfer call success: https://github.com/code-423n4/2021-12-yetifinance/blob/main/packages/contracts/contracts/AssetWrappers/WJLP/WJLP.sol#L166
Add a require() check for the success of JOE transfer in _safeJoeTransfer function and create and use a similar function with the same check for JLP token transfers
π Selected for report: csanuragjain
430.3487 USDC - $430.35
hyh
User's rewards can be partial lost as system will write down all current rewards as transferred, while actual transfer amount can be smaller. WJLP will be still accounted for the remaining part of rewards by Trader Joe, but there will be no link to the user to whom these rewards should be accounted for in WJLP.
WJLP's _sendJoeReward calls _safeJoeTransfer to send unclaimedJOEReward and unconditionally set unclaimedJOEReward to 0: https://github.com/code-423n4/2021-12-yetifinance/blob/main/packages/contracts/contracts/AssetWrappers/WJLP/WJLP.sol#L229
While _safeJoeTransfer will send less if there is not enough balance at the moment: https://github.com/code-423n4/2021-12-yetifinance/blob/main/packages/contracts/contracts/AssetWrappers/WJLP/WJLP.sol#L268
Trader Joe system ('an external system we are dealing with' in general if this to be an example contract) can malfunction and send less than what's due or avoid sending rewards altogether. As _MasterChefJoe.withdraw is called without withdraw call result check of any kind, this will go unnoticed and user's balance will be decreased by more than was actually transferred to a user. In other words, part of users' rewards will be lost.
Now:
function _sendJoeReward(address _rewardOwner, address _to) internal { userInfo[_rewardOwner].unclaimedJOEReward = 0; _safeJoeTransfer(_to, joeToSend); ... function _safeJoeTransfer(address _to, uint256 _amount) internal { uint256 joeBal = JOE.balanceOf(address(this)); if (_amount > joeBal) { JOE.transfer(_to, joeBal); } else { JOE.transfer(_to, _amount); } }
To be:
function _sendJoeReward(address _rewardOwner, address _to) internal { uint256 joeSent = _safeJoeTransfer(_to, joeToSend); userInfo[_rewardOwner].unclaimedJOEReward = joeToSend - joeSent; ... function _safeJoeTransfer(address _to, uint256 _amount) internal returns (uint256) { uint256 toSend = JOE.balanceOf(address(this)); if (toSend > _amount) { toSend = _amount; } bool success = JOE.transfer(_to, toSend); require(success, "JOE transfer failed"); return toSend; }
JLP safe transfer function can approach token balance availability and transfer result in the similar manner.
#0 - kingyetifinance
2022-01-05T06:14:12Z
@LilYeti: #61 duplicate: and probably severity 1 since this will lose only dust.
#1 - alcueca
2022-01-15T06:38:57Z
Duplicate of #137
π Selected for report: hyh
531.2947 USDC - $531.29
hyh
Running initialization function second time setting another token address can break the system.
Both contracts govern liquidity pools that are linked to specific tokens and use setParams to initialize token address. This has to be done only once, i.e. reinitialization shouldn't be allowed.
Unipool.setParams https://github.com/code-423n4/2021-12-yetifinance/blob/main/packages/contracts/contracts/LPRewards/Unipool.sol#L95
Pool2Unipool.setParams https://github.com/code-423n4/2021-12-yetifinance/blob/main/packages/contracts/contracts/LPRewards/Pool2Unipool.sol#L95
Also, token addresses can be immutable to save gas.
Disallow running setParams twice, following initializer pattern, and make both token variables immutable.
#0 - kingyetifinance
2022-01-05T05:38:14Z
@LilYeti: Only owner and is revoked after being called.
#1 - alcueca
2022-01-16T20:54:11Z
In the case of Pool2Unipool.sol, _renounceOwnership
is called in setRewards
, not setParams
. While it might be that the contract is not usable until setRewards
is called, setParams
can indeed be called several times. Incorrect as to spec.
π Selected for report: defsec
Also found by: 0x1f8b, Jujic, WatchPug, broccolirob, certora, cmichel, csanuragjain, hyh, jayjonah8, kenzo, robee, sirhashalot
hyh
Transactions will not be reverted on failed transfer call, setting system state as if it was successful. Namely, user collateral tokens will be removed from Stability Pool account, while actual funds will not be sent fully. This way user funds will be locked within the system.
_sendGainsToDepositor do not check for collateral assets transfer call success:
The function is used for tokens StabilityPool deposit and withdraw: StabilityPool.provideToSP, withdrawFromSP -> _sendGainsToDepositor.
Add a require() check for the success of collateral token transfers
#0 - kingyetifinance
2022-01-05T06:20:02Z
@LilYeti: safe transfer issue duplicate issue #1
#1 - kingyetifinance
2022-01-10T06:09:11Z
Fixed, nothing new compared to https://github.com/code-423n4/2021-12-yetifinance-findings/issues/94
#2 - alcueca
2022-01-15T07:32:23Z
Duplicate of #94
π Selected for report: hyh
531.2947 USDC - $531.29
hyh
On calling with arrays of different lengths various malfunctions are possible as the arrays are used as given.
withdrawColl
outcome will not be as expected by a caller.
_adjustTrove
doesn't check for array lengths and all other array providing usages of this function do check them before usage.
BorrowerOperations.withdrawColl doesn't: https://github.com/code-423n4/2021-12-yetifinance/blob/main/packages/contracts/contracts/BorrowerOperations.sol#L373
Add the check:
Now:
params._collsOut = _collsOut; params._amountsOut = _amountsOut;
To be:
require(_collsOut.length == _amountsOut.length); params._collsOut = _collsOut; params._amountsOut = _amountsOut;
#0 - 0xtruco
2022-01-11T09:57:52Z
Complete in https://github.com/code-423n4/2021-12-yetifinance/pull/11 Also #104 and #197
π Selected for report: hyh
531.2947 USDC - $531.29
hyh
If there be any emergency with system contracts or outside infrastructure, there is no way to temporary stop the operations.
BorrowerOperations: https://github.com/code-423n4/2021-12-yetifinance/blob/main/packages/contracts/contracts/BorrowerOperations.sol
TroveManager: https://github.com/code-423n4/2021-12-yetifinance/blob/main/packages/contracts/contracts/TroveManager.sol
Make user facing contracts, first of all BorrowerOperations and TroveManager, pausable.
For example, by using OpenZeppelin's approach: https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/security/Pausable.sol
It will be prudent to perform a deeper check to draw an extensive list of endpoints that require to be paused if something unexpected happens.
#0 - kingyetifinance
2022-01-05T05:48:46Z
@LilYeti: Known issue but unsure if we were going to implement this to make it less decentralized.
239.0826 USDC - $239.08
hyh
_requireValidRouterParams and _requireRouterAVAXIndicesInOrder functions along with IYetiRouter interface are unused:
_requireValidRouterParams https://github.com/code-423n4/2021-12-yetifinance/blob/main/packages/contracts/contracts/BorrowerOperations.sol#L1053
_requireRouterAVAXIndicesInOrder https://github.com/code-423n4/2021-12-yetifinance/blob/main/packages/contracts/contracts/BorrowerOperations.sol#L1067
IYetiRouter import and the interface itself: https://github.com/code-423n4/2021-12-yetifinance/blob/main/packages/contracts/contracts/BorrowerOperations.sol#L12 https://github.com/code-423n4/2021-12-yetifinance/blob/main/packages/contracts/contracts/Interfaces/IYetiRouter.sol
If it is not meant to be implemented further consider removal to enhance code readability and size
#0 - kingyetifinance
2022-01-05T05:54:55Z
@LilYeti: Should be gas optimization for unused functions.
#1 - 0xtruco
2022-01-11T10:05:08Z
Resolved in https://github.com/code-423n4/2021-12-yetifinance/pull/11 and for #184
#2 - 0xtruco
2022-02-04T05:11:49Z
@alcueca Reason for this to be a low severity for internal function that was not used?
#3 - alcueca
2022-02-04T06:06:55Z
Right, this would be a non-critical. Apologies.
π Selected for report: hyh
531.2947 USDC - $531.29
hyh
Comment is misleading, now stating the opposite of the implemented logic.
Comment states that tokens added are not less than amount repurchased
, while it should be not more
:
https://github.com/code-423n4/2021-12-yetifinance/blob/main/packages/contracts/contracts/YETI/sYETIToken.sol#L246
Now:
// Ensure that the amount of YETI tokens effectively added is >= the amount we have repurchased. if (yetiTokenBalance < additionalYetiTokenBalance) { additionalYetiTokenBalance = yetiTokenBalance; }
To be:
// Ensure that the amount of YETI tokens effectively added is <= the amount we have repurchased. if (yetiTokenBalance < additionalYetiTokenBalance) { additionalYetiTokenBalance = yetiTokenBalance; }
#0 - kingyetifinance
2022-01-05T05:57:13Z
@LilYeti: If they were suggesting our logic here was wrong, then they are correct. It should actually be
if (yetiTokenBalance - effectiveYetiTokenBalance < additionalYetiTokenBalance) {
This was pointed out by another of our independent auditors. If this is to be awarded for that bug as well then it is actually probably a medium error. However for just the comment which is what this issue references, it is non consequential error level 0 for readability.
#1 - kingyetifinance
2022-01-05T06:45:13Z
Actually probably just severity 0, it was not intended. Real bug reported here: #121
#2 - 0xtruco
2022-01-11T10:28:02Z
Resolved and in #121 https://github.com/code-423n4/2021-12-yetifinance/pull/12
#3 - alcueca
2022-01-15T17:01:19Z
Errors in comments are low severity.
96.8285 USDC - $96.83
hyh
WJLP set configuration variables via setAddresses initialize function that has no access controls, so whenever it is being run not atomically with contract creation it can be front run by an attacker. The fix is to redeploy the contracts.
WJLP.setAddresses: https://github.com/code-423n4/2021-12-yetifinance/blob/main/packages/contracts/contracts/AssetWrappers/WJLP/WJLP.sol#L102
WJLP.constructor: https://github.com/code-423n4/2021-12-yetifinance/blob/main/packages/contracts/contracts/AssetWrappers/WJLP/WJLP.sol#L82
a. Either set access rights in the constructor and restrict initialize access b. Or run setAddresses atomically along with contract construction each time
It is also advised to check for zero addressed supplied by a caller both in constructor and setAddresses. Misconfiguration with zero address also leads to redeployment.
#0 - kingyetifinance
2022-01-05T06:06:23Z
@LilYeti: Since we may have many of these, extra checks would be good. Will likely add Ownable.
π Selected for report: hyh
531.2947 USDC - $531.29
hyh
Core system logic can break up if enumeration structure be updated.
BorrowerOperations and StabilityPool check the active status of a trove by comparing TroveManager's getTroveStatus with 1: BorrowerOperations: https://github.com/code-423n4/2021-12-yetifinance/blob/main/packages/contracts/contracts/BorrowerOperations.sol#L902 https://github.com/code-423n4/2021-12-yetifinance/blob/main/packages/contracts/contracts/BorrowerOperations.sol#L907 StabilityPool: https://github.com/code-423n4/2021-12-yetifinance/blob/main/packages/contracts/contracts/StabilityPool.sol#L1104
TroveManagers inherit Status enumeration from TroveManagerBase: https://github.com/code-423n4/2021-12-yetifinance/blob/main/packages/contracts/contracts/Dependencies/TroveManagerBase.sol#L72
With further system development it will be harder to track fixes needed on enumeration change. Consider implementing TroveManager.isTroveActive(borrower) where trove.status is checked against Status.active and the corresponding boolean is returned.
#0 - kingyetifinance
2022-01-05T06:35:30Z
@LilYeti: Internal note: Make public function for this
#1 - 0xtruco
2022-01-11T10:39:57Z
Was not actually used in stab pool.
26.3494 USDC - $26.35
hyh
Console log code to be removed: https://github.com/code-423n4/2021-12-yetifinance/blob/main/packages/contracts/contracts/AssetWrappers/WJLP/WJLP.sol#L152
#0 - kingyetifinance
2022-01-06T09:20:23Z
A gas improvement actually.
#1 - 0xtruco
2022-01-11T10:30:34Z
Already Fixed #159 #256