The Standard

The Standard
DeFiHardhat
20,000 USDC
View results
Submission Details
Severity: low
Invalid

Low report

Low

LiquidationPool::setPoolFeePercentage and LiquidationPool::constructor allows to set poolFeePercentage > HUNDRED_PC

To make the code more robust this constraint check should be added.

Use of tokens with blacklist or pause mechanism should be avoided

Some concreate issues have been reported as medium. However, as general rule tokens with blacklist or pause functionallities such as USDT, USDC and PAXG should be avoided. Reasons enumerated below:

  • Blacklist of LiquidationPoolManager, LiquidationPoolManager.protocol will DOS liquidations

  • Blacklist of a vault will DOS liquidations and asset recovery

  • Blacklist of LiquidationPool will DOS rewards claims.

Same issues araise if pause mechanism are tirggered. If this tokens are suddenly blacklisted, then they will be also excluded as collateral from vaults, producing massive liquidations.

LiquidationPool consider adding a totalTST state variable

By doing this, all storage access when getTotalTST will be saved, The global variable should be increased/decreased whenever increasePosition/decreasePosition is called. Then getTotalTST can be deleted

Disclaimer: Using TST.balanceOf(this) is not an option, given that anyone can transfer TST to Liquidation pool, and next invariant would not hold:

$$Total ; TST = (\sum_{p}^{p ; \in ; Positions} (p.TST)) + (\sum_{ps}^{ps ; \in ; Pending ; stakes} (ps.TST)) $$

LiquidationPoolManager::forwardRemainingRewards blacklisted token can make whole function and dependencies revert

Some token like USDC and USDT have block lists. In case protocol address is blacklisted this function will always revert. This has enormous relevance for LiquidationPoolManager::runLiquidation given that it would DOS every vault liquidation which contain these tokens

Precedent: https://www.codehawks.com/report/cljyfxlc40003jq082s0wemya#M-03

Non-critical

Consider moving empty(positions[msg.sender]) from LiquidationPool::decreasePosition to LiquidationPool::deletePosition

Seems to be a better responsability distribution

LiquidationPool::decreasePosition does not follows CEI pattern

function decreasePosition(uint256 _tstVal, uint256 _eurosVal) external {
consolidatePendingStakes();
ILiquidationPoolManager(manager).distributeFees();
require(_tstVal <= positions[msg.sender].TST && _eurosVal <= positions[msg.sender].EUROs, "invalid-decr-amount");
if (_tstVal > 0) {
- IERC20(TST).safeTransfer(msg.sender, _tstVal);
positions[msg.sender].TST -= _tstVal;
+ IERC20(TST).safeTransfer(msg.sender, _tstVal);
}
if (_eurosVal > 0) {
- IERC20(EUROs).safeTransfer(msg.sender, _eurosVal);
positions[msg.sender].EUROs -= _eurosVal;
+ IERC20(EUROs).safeTransfer(msg.sender, _eurosVal);
}
if (empty(positions[msg.sender])) deletePosition(positions[msg.sender]);
}

SmartVaultV3::liquidate allows to liquidate an already liquidated vault

Function does not check if the vault has been already liquidated

function liquidate() external onlyVaultManager {
require(undercollateralised(), "err-not-liquidatable");
+ require(!liquidated, "err-already-liquidated");
liquidated = true;
minted = 0;
liquidateNative();
ITokenManager.Token[] memory tokens = getTokenManager().getAcceptedTokens();
for (uint256 i = 0; i < tokens.length; i++) {
if (tokens[i].symbol != NATIVE) liquidateERC20(IERC20(tokens[i].addr));
}
}

Gas

Use of Beacon Proxy pattern can save SmartVault deployment gas

Use of Beacon Proxy pattern can save ton of gas for new smart vault deployment. More

LiquidationPool::deleteHolder optimize function:

The function continue iterating even though holders was already removed.

function deleteHolder(address _holder) private {
- for (uint256 i = 0; i < holders.length; i++) {
+ uint256 holdersLen = holders.length;
+ for (uint256 i; i < holdersLen;) {
if (holders[i] == _holder) {
- holders[i] = holders[holders.length - 1];
+ holders[i] = holders[holdersLen - 1];
holders.pop();
+ return;
}
+ unchecked{ ++i}
}
}

LiquidationPool::consolidatePendingStakes and LiquidationPool::deletePendingStake can be optimeze to decrease considerable number of iterations

Whenever a holder increase his position, a pending stake is created, and it is push into pendingStakes, meaning that this new pending stake is put at the end of the array. This means that pendingStakes acts as a queue

When consolidatePendingStakes is called, it verifies if 24 hours has passed since each pending stake has been created, and each time it detects that a pending stake should be consolidated, it increase corresponding position TST and EUROS, and it shift the array to the right. To do this last shift, given solidity limitations each element is copied one position to the right. Therefore this last set of shifts is done once per position consolidated.

Given that we know that a pending stake that was pushed first has an earlier or equal deadline than a pending stake pushed after, we can simplify this set of shifts to a single one.

function consolidatePendingStakes() private {
uint256 deadline = block.timestamp - 1 days;
uint256 shiftFrom;
uint256 pendingStakesLen = pendingStakes.length;
for (uint256 i; i < pendingStakesLen; i) {
PendingStake memory _stake = pendingStakes[i];
if (_stake.createdAt < deadline) {
positions[_stake.holder].holder = _stake.holder;
positions[_stake.holder].TST += _stake.TST;
positions[_stake.holder].EUROs += _stake.EUROs;
}else{
shiftFrom = i;
break;
}
unchecked{++i;}
}
shiftPendingStakes(shiftFrom);
}
function shiftPendingStakes(uint256 shiftFrom) private{
uint256 pendingStakesLen = pendingStakes.length;
uint256 positionTracker
for (uint256 i = shiftFrom; i < pendingStakesLen;) {
pendingStakes[positionTracker] = pendingStakes[i];
unchecked{
++positionTracker;
++i;
}
}
for (uint256 i = positionTracker; i < pendingStakesLen;){
pendingStakes.pop();
unchecked{
++i;
}
}
}

SmartVaultV3::getToken optimize loop

Apart from common optimization, return instruction can be added to avoid keep iterating inside the loop if the token was found:

for (uint256 i = 0; i < tokens.length; i++) {
+ uint256 tokensLen = tokens.length;
+ for (uint256 i = 0; i < tokensLen; ) {
- if (tokens[i].symbol == _symbol) _token = tokens[i];
+ if (tokens[i].symbol == _symbol) return tokens[i]
+ unchecked{++i}
}

Use unchecked{++i / --i} in for loops

Instance(23)

// LiquidationPool.sol
49: for (uint256 i = 0; i < holders.length; i++) {
56: for (uint256 i = 0; i < holders.length; i++) {
59: for (uint256 i = 0; i < pendingStakes.length; i++) {
67: for (uint256 i = 0; i < _tokens.length; i++) {
74: for (uint256 i = 0; i < pendingStakes.length; i++) {
97: for (uint256 i = 0; i < holders.length; i++) {
106: for (uint256 i = _i; i < pendingStakes.length - 1; i++) {
113: for (uint256 i = 0; i < holders.length; i++) {
121: for (int256 i = 0; uint256(i) < pendingStakes.length; i++) {
129: i--;
166: for (uint256 i = 0; i < _tokens.length; i++) {
186: for (uint256 i = 0; i < holders.length; i++) {
190: for (uint256 i = 0; i < pendingStakes.length; i++) {
197: for (uint256 i = 0; i < _assets.length; i++) {
211: for (uint256 j = 0; j < holders.length; j++) {
215: for (uint256 i = 0; i < _assets.length; i++) {
// LiquidationPoolManager.sol
44: for (uint256 i = 0; i < _tokens.length; i++) {
66: for (uint256 i = 0; i < tokens.length; i++) {
// SmartVaultManagerV5.sol
57: for (uint256 i = 0; i < idsLength; i++) {
// SmartVaultV3.sol
69: for (uint256 i = 0; i < acceptedTokens.length; i++) {
86: for (uint256 i = 0; i < acceptedTokens.length; i++) {
120: for (uint256 i = 0; i < tokens.length; i++) {
179: for (uint256 i = 0; i < tokens.length; i++) {

Cache length from storage/memory arrays when iterating over loops to save gas

Instance(23)

// LiquidationPool.sol
49: for (uint256 i = 0; i < holders.length; i++) {
56: for (uint256 i = 0; i < holders.length; i++) {
59: for (uint256 i = 0; i < pendingStakes.length; i++) {
67: for (uint256 i = 0; i < _tokens.length; i++) {
74: for (uint256 i = 0; i < pendingStakes.length; i++) {
97: for (uint256 i = 0; i < holders.length; i++) {
106: for (uint256 i = _i; i < pendingStakes.length - 1; i++) {
113: for (uint256 i = 0; i < holders.length; i++) {
121: for (int256 i = 0; uint256(i) < pendingStakes.length; i++) {
129: i--;
166: for (uint256 i = 0; i < _tokens.length; i++) {
186: for (uint256 i = 0; i < holders.length; i++) {
190: for (uint256 i = 0; i < pendingStakes.length; i++) {
197: for (uint256 i = 0; i < _assets.length; i++) {
211: for (uint256 j = 0; j < holders.length; j++) {
215: for (uint256 i = 0; i < _assets.length; i++) {
// LiquidationPoolManager.sol
44: for (uint256 i = 0; i < _tokens.length; i++) {
66: for (uint256 i = 0; i < tokens.length; i++) {
// SmartVaultManagerV5.sol
57: for (uint256 i = 0; i < idsLength; i++) {
// SmartVaultV3.sol
69: for (uint256 i = 0; i < acceptedTokens.length; i++) {
86: for (uint256 i = 0; i < acceptedTokens.length; i++) {
120: for (uint256 i = 0; i < tokens.length; i++) {
179: for (uint256 i = 0; i < tokens.length; i++) {

Do not intialize variable with default values

Instance(23)

// LiquidationPool.sol
49: for (uint256 i = 0; i < holders.length; i++) {
56: for (uint256 i = 0; i < holders.length; i++) {
59: for (uint256 i = 0; i < pendingStakes.length; i++) {
67: for (uint256 i = 0; i < _tokens.length; i++) {
74: for (uint256 i = 0; i < pendingStakes.length; i++) {
97: for (uint256 i = 0; i < holders.length; i++) {
106: for (uint256 i = _i; i < pendingStakes.length - 1; i++) {
113: for (uint256 i = 0; i < holders.length; i++) {
121: for (int256 i = 0; uint256(i) < pendingStakes.length; i++) {
129: i--;
166: for (uint256 i = 0; i < _tokens.length; i++) {
186: for (uint256 i = 0; i < holders.length; i++) {
190: for (uint256 i = 0; i < pendingStakes.length; i++) {
197: for (uint256 i = 0; i < _assets.length; i++) {
211: for (uint256 j = 0; j < holders.length; j++) {
215: for (uint256 i = 0; i < _assets.length; i++) {
// LiquidationPoolManager.sol
44: for (uint256 i = 0; i < _tokens.length; i++) {
66: for (uint256 i = 0; i < tokens.length; i++) {
// SmartVaultManagerV5.sol
57: for (uint256 i = 0; i < idsLength; i++) {
// SmartVaultV3.sol
69: for (uint256 i = 0; i < acceptedTokens.length; i++) {
86: for (uint256 i = 0; i < acceptedTokens.length; i++) {
120: for (uint256 i = 0; i < tokens.length; i++) {
179: for (uint256 i = 0; i < tokens.length; i++) {

Functions guaranteed to revert when called by normal users can be marked payable

If a function modifier such as onlyOwner is used, the function will revert if a normal user tries to pay the function. Marking the function as payable will lower the gas cost for legitimate callers because the compiler will not include checks for whether a payment was provided. The extra opcodes avoided are CALLVALUE(2), DUP1(3), ISZERO(3), PUSH2(3), JUMPI(10), PUSH1(3), DUP1(3), REVERT(0), JUMPDEST(1), POP(2), which costs an average of about 21 gas per call to the function, in addition to the extra deployment cost (2400 per instance).

// LiquidationPool.sol
function distributeFees(uint256 _amount) external onlyManager {
// SmartVaultManagerV5
function liquidateVault(uint256 _tokenId) external onlyLiquidator {
function setMintFeeRate(uint256 _rate) external onlyOwner {
function setBurnFeeRate(uint256 _rate) external onlyOwner {
function setSwapFeeRate(uint256 _rate) external onlyOwner {
function setWethAddress(address _weth) external onlyOwner() {
function setSwapRouter2(address _swapRouter) external onlyOwner() {
function setNFTMetadataGenerator(address _nftMetadataGenerator) external onlyOwner() {
function setSmartVaultDeployer(address _smartVaultDeployer) external onlyOwner() {
function setProtocolAddress(address _protocol) external onlyOwner() {
function setLiquidatorAddress(address _liquidator) external onlyOwner() {
// SmartVaultV3.sol
function liquidate() external onlyVaultManager {
function setOwner(address _newOwner) external onlyVaultManager {
Updates

Lead Judging Commences

hrishibhat Lead Judge almost 2 years ago
Submission Judgement Published
Invalidated
Reason: Non-acceptable severity
Assigned finding tags:

informational/invalid

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.

Give us feedback!