✅DEX
integer division precision loss
Description
The goal of this level is for you to hack the basic DEX contract below and steal the funds by price manipulation.
You will start with 10 tokens of token1
and 10 of token2
. The DEX contract starts with 100 of each token.
You will be successful in this level if you manage to drain all of at least 1 of the 2 tokens from the contract, and allow the contract to report a "bad" price of the assets.
Quick note
Normally, when you make a swap with an ERC20 token, you have to approve
the contract to spend your tokens for you. To keep with the syntax of the game, we've just added the approve
method to the contract itself. So feel free to use contract.approve(contract.address, <uint amount>)
instead of calling the tokens directly, and it will automatically approve spending the two tokens by the desired amount. Feel free to ignore the SwappableToken
contract otherwise.
Things that might help:
How is the price of the token calculated?
How does the
swap
method work?How do you
approve
a transaction of an ERC20?
Code Audit
This challenge has a helper contract SwappableToken
that overrides the approve()
function. The new approve()
implementation prevents the DEX from approving anyone:
The main contract DEX
:
The essence of this contract is the getSwapPrice()
function:
Calculating price in this way is vulnerable to "price manipulation" attack. The correct way of doing this is using an external oracle such as Chainlink.
How does this price manipulation work? In Solidity, integer division rounds down. For example, 5 / 2
is evaluated to 2 in Solidity, which should be 2.5 in reality. In this DEX contract, if we sell 1 token1
but token2 * amount < token1
, getSwapPrice()
will return 0. That is, we sell a token and get nothing back. In this way, we can drain a token poll.
Solution
Here is the plan:
Swap all
token1
fortoken2
.Swap all
token2
fortoken1
.Repeat above steps until
token1
ortoken2
gets drained.
For each round, the user will hold more tokens than the previous round. Here is a nice table I found in this writeup:
Detailed computation:
Here are the steps to pwn this level:
Step 1: Set allowance to 500 (any large number suffices):
Step 2: Get token addresses:
Step 3: Perform back-and-forth swaps:
Step 4: Verify if token1
is drained:
If this returns 0 then we are all good.
Summary
The integer math portion aside, getting prices or any sort of data from any single source is a massive attack vector in smart contracts.
You can clearly see from this example, that someone with a lot of capital could manipulate the price in one fell swoop, and cause any applications relying on it to use the the wrong price.
The exchange itself is decentralized, but the price of the asset is centralized, since it comes from 1 dex. This is why we need oracles. Oracles are ways to get data into and out of smart contracts. We should be getting our data from multiple independent decentralized sources, otherwise we can run this risk.
Chainlink Data Feeds are a secure, reliable, way to get decentralized data into your smart contracts. They have a vast library of many different sources, and also offer secure randomness, ability to make any API call, modular oracle network creation, upkeep, actions, and maintainance, and unlimited customization.
Uniswap TWAP Oracles relies on a time weighted price model called TWAP. While the design can be attractive, this protocol heavily depends on the liquidity of the DEX protocol, and if this is too low, prices can be easily manipulated.
Here is an example of getting data from a Chainlink data feed (on the kovan testnet):
Further Reading
The vulnerability in this challenge can be fixed by using a price oracle. However, price oracles are not perfect. They could introduce other vulnerabilities into the system. samczsun has an introductory article regarding price oracles:
Last updated