DeFi

Decentralized Finance (DeFi) is a financial system and application based on blockchain technology. It aims to remove the intermediary roles of traditional financial institutions by utilizing smart contracts and decentralized technologies. DeFi aims to make financial services more open, transparent, and trustless, enabling decentralized asset management, trading, lending, and other financial activities. While DeFi plays a significant role in the cryptocurrency and blockchain industry, it is important to acknowledge that DeFi projects still face challenges and risks. In the following example, we will discuss the perspective of smart contract vulnerabilities.

Read-Only Reentrancy

The ZkSync-based Lending Protocol EraLend has been attacked. The attacker exploited the Read-Only Reentrancy vulnerability in the code to manipulate the pricing of LP tokens and acquire a large amount of funds from the platform.

There is a reentrancy vulnerability in the SyncSwap code. The attacker invokes the _burn function of SyncSwap to burn their own LP tokens while borrowing cTokens within the reentrancy. The borrowed amount is calculated based on the reserves of SyncSwap at that time. However, the reserves are not updated during the reentrancy, so the calculated amount uses the pre-burn reserves. After the reentrancy ends, the reserves are updated. The attacker repays the loan, and the calculation now uses the updated reserves. This allows the attacker to repay with fewer tokens than borrowed, resulting in a profit equal to the difference.

// Burns liquidity and transfers pool tokens. 
_burn(address(this),params.liquidity);  // burn LP tokens
_transferTokens(token0, params.to, params.amount0, params.withdrawMode);
_transferTokens(token1, params.to, params.amount1, params.withdrawMode);
// Updates balances.
unchecked {
    params.balance0 -= params.amount0; 
    params.balance1 -= params.amount1;
}
// Calls callback with data. 
// Reserves are not updated at this point to allow read the old values. 
if (_callback != address(0)) {
    params.sender = _getVerifiedSender(_sender); 
    params.callbackData = _callbackData;
    // Call ctoken for borrowing
    ICallback(_callback).syncSwapBaseBurnCallback(params);
}
//reserve not updated
_updateReserves(params.balance0, params.balance1); 
if (_feeOn) { 
    invariantLast = _computelnvariant(params.balance0, params.balance1);
}
_amounts = new TokenAmount[](2);
_amounts[0] = TokenAmount(token0, params.amount0);
_amounts[1] = TokenAmount(token1, params.amount1); 
emit Burn(msg.sender, params.amount0, params.amount1, params.liquidity, params.to);

Suggestion: Update the value of reserves before executing the callback to ensure that the calculated amount uses the latest reserves. This will help prevent attackers from manipulating token pricing by exploiting the reentrancy vulnerability.

// Burns liquidity and transfers pool tokens.
_burn(address(this), params.liquidity); // burn LP tokens
_transferTokens(token0, params.to, params.amount0, params.withdrawMode);
_transferTokens(token1, params.to, params.amount1, params.withdrawMode);
// Updates balances.
unchecked {
    params.balance0 -= params.amount0;
    params.balance1 -= params.amount1;
}
// Calls callback with data.
// Update reserves before the callback to use the latest values.
_updateReserves(params.balance0, params.balance1);
if (_callback != address(0)) {
    params.sender = _getVerifiedSender(_sender);
    params.callbackData = _callbackData;
    // Call ctoken for borrowing
    ICallback(_callback).syncSwapBaseBurnCallback(params);
}
if (_feeOn) {
    invariantLast = _computeInvariant(params.balance0, params.balance1);
}
_amounts = new TokenAmount[](2);
_amounts[0] = TokenAmount(token0, params.amount0);
_amounts[1] = TokenAmount(token1, params.amount1);
emit Burn(msg.sender, params.amount0, params.amount1, params.liquidity, params.to);

Slippage

Uniswap is currently one of the largest decentralized exchanges. Its goal is to provide a decentralized and trustless trading platform where users can trade Ethereum and other ERC-20 tokens. During the execution of trades, there can be a discrepancy between the actual executed price and the expected price due to insufficient liquidity in the pool or market price fluctuations. This difference is known as slippage. If the slippage is too high, traders may not be able to execute trades at the expected price, resulting in increased transaction costs or reduced profits. To mitigate the adverse effects of slippage, traders can control slippage by setting appropriate limit orders or adjusting trade sizes. While slippage itself is not a vulnerability or attack, malicious users can exploit it to perform sandwich attacks.

Issue:lack slippage check.

The following code snippet lacks slippage checks in the swap transaction, which has been exploited in a sandwich attack. The attacker performed a front-running trade by buying token1 before the victim's swap token0 for token1 transaction. This caused a decrease in the quantity of token1 in the liquidity pool and an increase in the price of token1. Subsequently, the victim was affected by slippage and purchased token1 at a higher price than expected. Finally, the attacker performed a reverse trade by swapping token1 for token0, profiting from the victim's trading behavior.

IUniswapRouterV2(uniswap).swapExactTokensForTokens(balance, 0 /* zero min return */,path, address(this), now);

Suggestion: To incorporate slippage protection into the swapExactTokensForTokens function, we have introduced a new function called setMinReturnPercentage. This function allows the contract's administrator to set the min-return ratio. By default, the minimum return ratio is set to 100, indicating a minimum return rate of 1%. The swapExactTokensWithSlippageProtection function is designed to execute trades with slippage protection. Within this function, we calculate the minReturnAmount to ensure that the actual received amount exceeds this value. Subsequently, we call the original swapExactTokensForTokens function, passing minReturnAmount as a parameter.

//Define a variable to store the min-return ratio, for example 0.01 represents a minimum return rate of 1%.
uint256 public minReturnPercentage = 100;
function setMinReturnPercentage(uint256 percentage) external onlyOwner{
    // Add a function to set the min-return ratio, only the contract's administrator can call this function.
    require(msg.sender == owner, "Only the contract owner can set the minimum return percentage");
    minReturnPercentage = percentage;
}
function swapExactTokensWithSlippageProtection(
    uint256 balance,
    uint256 minReturn,
    address[] memory path,
    address to,
    uint256 deadline
) external {
    // calculate the min-return amount
    uint256 minReturnAmount = (balance * minReturnPercentage) / 100;
    // swap
    IUniswapRouterV2(uniswap).swapExactTokensForTokens(
        balance,
        minReturnAmount,
        path,
        to,
        deadline
    );
} 

Last updated