1. Overview
2023๋
8์ 2์ผ, Uwerx ํ๋ซํผ์ flashloan Attack ์ ์ด์ฉํ ๊ณต๊ฒฉ์ผ๋ก ์ฝ 32.7๋ง(174WETH )๋ฌ๋ฌ์ ํด๋นํ๋ ๊ธ์ก์ด ํ์ทจ๋์์ต๋๋ค.
๊ณต๊ฒฉ์๋ WERX๋ฅผ ์ก๊ธํ ๋ ๋ฐ์ํ๋ ์ทจ์ฝ์ ์ ์ด์ฉํ์ฌ ํ ํฐ ๊ฐ๊ฒฉ์ ์กฐ์ํ๊ณ ์ด์ต์ ์ป์์ต๋๋ค.
Uwerx์ธก์์๋ ๊ณต๊ฒฉ์์๊ฒ 20%์ bounty๋ฅผ ์ ์ํ์์ผ๋, ํ ์์ ์๋ ์์ง ๊ณต๊ฒฉ์์ ์ง๊ฐ์ ์๋ ๊ฒ์ผ๋ก ํ์ธํ์์ต๋๋ค.
What is Uwerx?
Uwerx ํ๋ซํผ์ ํ๋ฆฌ๋์์ ๊ณ ๊ฐ์ ์ฐ๊ฒฐ ์์ผ์ฃผ๋ ๋ธ๋ก์ฒด์ธ ๊ธฐ๋ฐ ์๋น์ค์
๋๋ค.
Proof-of-work(POW) ์์คํ
์ ํ์ฉํ์ฌ ๋ชจ๋ ํฌํธํด๋ฆฌ์ค ๋ฐ์ดํฐ์ ์ง์์ฌ๋ถ์ ์์ ๊ถ์ ๋ณด์ฅํฉ๋๋ค. ๋ํ, ์์ ํ๊ณ ํฌ๋ช
ํ ๊ฑฐ๋๋ฅผ ๋ณด์ฅํ๋ฉฐ, ๋ชจ๋ ๊ฒฐ์ ๋ Uwerx ์ ์ํธํํ(WERX)๋ฅผ ํตํด์ ์ด๋ฃจ์ด์ง๋๋ค. WERXํ ํฐ์ ํตํด ์ฌ์ฉ์๋ ํ๋ฆฌ๋ฏธ์ ๊ธฐ๋ฅ์ ์์ธ์คํ๊ฑฐ๋, ์๋น์ค ํ ์ธ๋ฑ์ ๋ฐ์ ์ ์์ต๋๋ค.
2. Vulnerability Analysis
ํด๋น ์ทจ์ฝ์ ์ Uwerx ์ปจํธ๋ํธ์ ํจ์ _transfer ์์ uniswapPoolAddress ๋ก WERX๋ฅผ ์ก๊ธํ ๋, ์ก๊ธํ๋ ค๋ ์์ 1% WERX์ ์ถ๊ฐ์ ์ผ๋ก burnํ๊ธฐ ๋๋ฌธ์ ๋ฐ์ํ์์ต๋๋ค.
์ค์ ๊ณต๊ฒฉ์ ๋ฐ ์ปจํธ๋ํธ๋ค์ ์ฃผ์๋ ์๋์ ๊ฐ์ต๋๋ค.
Attacker Address : 0x6057A831D43c395198A10cf2d7d6D6A063B1fCe4
Attacker Contract : 0xDA2CCfC4557BA55eAda3cBEbd0AEFfCf97Fc14CA
Attack Transaction : 0x3b19e152943f31fe0830b67315ddc89be9a066dc89174256e17bc8c2d35b5af8
Victim Address : 0x4306B12F8e824cE1fa9604BbD88f2AD4f0FE3c54
๋จผ์ , ์ทจ์ฝ์ ์ด ๋ฐ์ํ๋ Uwerx ์ปจํธ๋ํธ ๋ด๋ถ ํจ์๋ฅผ ๋จผ์ ์ดํด๋ณด๋๋ก ํ๊ฒ ์ต๋๋ค.
โข
Uwerx.sol
โฆ
function transfer
function transfer(address to, uint256 amount) public virtual override returns (bool) {
address owner = _msgSender();
_transfer(owner, to, amount); // from, to, amount
return true;
}
Solidity
๋ณต์ฌ
transfer ๋ WERX ์ก๊ธ์ ํ ๋ ์ฌ์ฉ๋๋ ํจ์๋ก, owner๋ฅผ _msgSender()๋ก ์ง์ ํ๊ณ from ์ฃผ์์ง๋ฅผ owner๋ก ์ค์ ํ์ฌ _transfer ๋ฅผ ํธ์ถํฉ๋๋ค.
โฆ
function _transfer
function _transfer(
address from,
address to,
uint256 amount
) internal virtual {
require(from != address(0), "ERC20: transfer from the zero address");
require(to != address(0), "ERC20: transfer to the zero address");
_beforeTokenTransfer(from, to, amount);
uint256 fromBalance = _balances[from];
require(fromBalance >= amount, "ERC20: transfer amount exceeds balance");
unchecked {
_balances[from] = fromBalance - amount; [1]
// Overflow not possible: the sum of all balances is capped by totalSupply, and the sum is preserved by
// decrementing then incrementing.
_balances[to] += amount;
}
if (to == uniswapPoolAddress) {
uint256 userTransferAmount = (amount * 97) / 100;
uint256 marketingAmount = (amount * 2) / 100;
uint256 burnAmount = amount - userTransferAmount - marketingAmount; [2]
emit Transfer(from, to, userTransferAmount);
emit Transfer(from, marketingWalletAddress, marketingAmount);
_burn(from, burnAmount); [3]
} else {
emit Transfer(from, to, amount);
}
_afterTokenTransfer(from, to, amount);
}
Solidity
๋ณต์ฌ
[1] _transfer ๋ฅผ ์ดํด๋ณด๋ฉด, ์ก๊ธ์์ ์๊ณ ๊ฐ ์ก๊ธํ ์๋ณด๋ค ํฐ์ง ํ์ธํ๊ณ , ์ก๊ธ์์ balance๋ฅผ amount๋งํผ ๋นผ์ค๋๋ค.
[2] ๊ทธ๋ค์ if๋ฌธ์ ๋ณด๋ฉด to์ ์ฃผ์์ง๊ฐ uniswapPoolAddress ๋ก ๊ฐ๋ ๊ฒฝ์ฐ, burnAmount ๋ฅผ ๊ณ์ฐํ๋๋ฐ amount - (amount * 97)/100 - (amount * 2)/100 ๋งํผ ๊ฐ์ด ๋ค์ด๊ฐ๋๋ค.
์ค์ง์ ์ผ๋ก, burnAmount ๋ ์ด๊ธฐ amount์ 1%๊ฐ ๋ค์ด๊ฐ๊ฒ ๋ฉ๋๋ค.
[3] burnAmount ๋ฅผ ์ธ์๋ก _burn์ ํธ์ถํฉ๋๋ค.
โฆ
function _burn
function _burn(address account, uint256 amount) internal virtual {
require(account != address(0), "ERC20: burn from the zero address");
_beforeTokenTransfer(account, address(0), amount);
uint256 accountBalance = _balances[account];
require(accountBalance >= amount, "ERC20: burn amount exceeds balance");
unchecked {
_balances[account] = accountBalance - amount;
// Overflow not possible: amount <= accountBalance <= totalSupply.
_totalSupply -= amount; [4]
}
emit Transfer(account, address(0), amount);
_afterTokenTransfer(account, address(0), amount);
}
Solidity
๋ณต์ฌ
[4] ์์ [1] ์ ๊ณผ์ ์์ ์ก๊ธ์์ balance๊ฐ์ ์ด๋ฏธ ๊ฐ์์์ผฐ์ผ๋, ์์์ ๊ณ์ฐํ burnAmount ๋งํผ์ ๋ ๊ฐ์์ํค๊ธฐ ๋๋ฌธ์ ๋ฌธ์ ๊ฐ ๋ฐ์ํฉ๋๋ค. ์ฝ๊ฒ ๋งํ๋ฉด, amount*1.01 ๋ฅผ ๋บ๋ค๊ณ ์๊ฐํ์๋ฉด ๋ฉ๋๋ค.
1% ์ด์ง๋ง, ๋๋์ ๊ธ์ก์ธ ๊ฒฝ์ฐ ์ถ๊ฐ๋ก burn์ ์ํค๋ฉด Pool์ ํ ํฐ ๊ฐ๊ฒฉ์ ํฐ ๋ณ๋์ด ์๊ธฐ๊ฒ ๋ฉ๋๋ค.
๊ณต๊ฒฉ์๋ ์ด ์ทจ์ฝ์ ์ ํตํ์ฌ, Pool์ WERXํ ํฐ์ burn ์์ผ ๊ฐ๊ฒฉ ๋ณ๋์ ํตํด ์ฐจ์ต์ ์ป์์ต๋๋ค. ๊ณต๊ฒฉ์๋ ํฐ ๊ธ์ก์ ์ด์ฉํ๊ธฐ ์ํด flashloan์ ํตํด ๊ณต๊ฒฉ์ ์งํํ์๊ณ , ์์ธํ ๊ณต๊ฒฉ ๊ธฐ๋ฒ์ ํ์ ์์ ์ค๋ช
ํ๊ฒ ์ต๋๋ค.
3. Attack Flow
3.1 Summary
์ ์ฒด ๊ณต๊ฒฉ ํ๋ฆ์ ๊ฐ๋จํ ์ ๋ฆฌํ๋ฉด, ์๋์ ๊ฐ์ต๋๋ค.
1. ๊ณต๊ฒฉ์๋ Attacker Contract๋ฅผ deployํฉ๋๋ค.
2. ๊ณต๊ฒฉ์๋ flashloan์ผ๋ก 20000WETH๋ฅผ ๋น๋ฆฝ๋๋ค.
3. Uniswap V2: WERX 2 Pool ์ ํตํด WETHโWERX๋ก swap์ ํ์ต๋๋ค.
4. ๊ณต๊ฒฉ์๋ imbalance๋ฅผ ๋ง๋ค๊ธฐ ์ํด Uniswap V2: WERX 2 Pool๋ก 4429817WERX ๋ฅผ ์ก๊ธํ์์ต๋๋ค.
5. ๊ณต๊ฒฉ์์ ์ก๊ธ์ผ๋ก ์ธํด Uniswap V2: WERX 2 Pool์ balance๊ฐ๊ณผ reserve๊ฐ์ ์ฐจ์ด(imbalance)๊ฐ ์๊ธฐ๊ฒ ๋๊ณ , ๊ณต๊ฒฉ์๋ ์ด๋ฅผ ์
์ฉํ๊ธฐ ์ํด skim(uniswapPoolAddress(0x1)) ์ ํธ์ถํ์ฌ, uwerx์ ์ทจ์ฝํ _transfer ๊ฐ ํธ์ถ๋๋๋ก ํ์์ต๋๋ค.
_transfer ๋ Uniswap V2: WERX 2 Pool์ imbalance๊ฐ์ 1% ๋ฅผ ์ถ๊ฐ์ ์ผ๋ก burn์ ์ํค๊ฒ ๋๊ณ , WERXํ ํฐ์ ๊ฐ๊ฒฉ์ด ๊ธ ์์นํ๊ฒ ๋ฉ๋๋ค. ๊ทธ ํ sync() ํจ์๋ฅผ ํธ์ถํด ์๊ณ ๋ฅผ ์
๋ฐ์ดํธํฉ๋๋ค.
6. ๊ณต๊ฒฉ์๋ ์ฐจ์ต์ ์ป๊ธฐ ์ํด WERXโWETH๋ก swap์ ํฉ๋๋ค.
7. ๊ณต๊ฒฉ์๋ flashloan์ repayํ๊ณ , ์ฐจ์ต์ ์ป๊ฒ ๋ฉ๋๋ค.
uniswapPoolAddress
์๋๋ Step 1~7์ ๋ฐ๋ผ, Attack Flow์ Token Flow๋ฅผ ๋์ํ์ํจ ์๋ฃ์
๋๋ค.
3.2 Attack Transaction
์ค์ ์ฌ์ฉ๋ transaction์ ๋ณด๋ฉด์ ์์ธํ๊ฒ ์ค๋ช
๋๋ฆฌ๋๋ก ํ๊ฒ ์ต๋๋ค.
โข
Attack Transaction : 0x3b19e152943f31fe0830b67315ddc89be9a066dc89174256e17bc8c2d35b5af8
1.
๊ณต๊ฒฉ์๋ flashloan์ ์ด์ฉํ๊ธฐ ์ํด ๊ณต๊ฒฉ์ ์ปจํธ๋ํธ๋ฅผ ๋ง๋ญ๋๋ค.
2.
๊ณต๊ฒฉ์ ์ปจํธ๋ํธ๋ Balancer์์ flashloan์ ์ด์ฉํ์ฌ 20,000WETH ๋ฅผ ๋์ถํฉ๋๋ค.
3.
ํ์ฌ Uniswap V2: WERX 2 Pool์๋ 5,097,936WERX , 174 WETH ๊ฐ ๋ด๊ฒจ์ ธ ์์ต๋๋ค.
4.
flashloan์ ํตํด ๋ฐ์ 20,000WETH ๋ฅผ Uniswap V2: WERX 2 Pool(Pool๋ก ์ค์ฌ์ ๋ถ๋ฅด๊ฒ ์ต๋๋ค.)์ ์ด์ฉํ์ฌ ๋๋ต5,053,637WERX ๋ก swapํฉ๋๋ค. swapํ์ Pool์๋ 44,298WERX ์ ๋๊ฐ ๋จ๊ฒ ๋ฉ๋๋ค.
5.
๊ณต๊ฒฉ์๋ imbalance๋ฅผ ๋ง๋ค๊ธฐ ์ํด Pool์ ๋จ๊ฒจ์ง WERX์ 100๋ฐฐ์ธ 4,429,817WERX ๋ฅผ Pool๋ก ์ก๊ธํฉ๋๋ค.
6.
๊ณต๊ฒฉ์๋Uniswap V2: WERX 2 Pool์ skim ํจ์๋ฅผ ํตํด ์ทจ์ฝ์ ์ ํธ๋ฆฌ๊ฑฐํฉ๋๋ค.
โข
function skim
function skim(address to) external lock {
address _token0 = token0; // gas savings
address _token1 = token1; // gas savings
_safeTransfer(_token0, to, IERC20(_token0).balanceOf(address(this)).sub(reserve0));
_safeTransfer(_token1, to, IERC20(_token1).balanceOf(address(this)).sub(reserve1));
}
Solidity
๋ณต์ฌ
skim ์ ํ์ฌ ์
๋ฐ์ดํธ ๋์ง ์์ ํ์ reserve ๊ฐ๊ณผ ์ค์ ํ์ balance๋ฅผ ๋ง์ถ๊ธฐ ์ํ ํจ์๋ก, ์ฐจ์ก๋งํผ to ์ ์ฃผ์์ง๋ก ๋ณด๋ด์, ๋ ๊ฐ์ ๋ง์ถฐ์ฃผ๋ ์ญํ ์ ํฉ๋๋ค.
skim์์๋ ํ ํฐ ์์ ๋ํด _safeTransfer ๊ฐ ํธ์ถ๋ฉ๋๋ค.
value๊ฐ์ ํ์ฌ Pool ์ ์ค์ ์๊ณ ์ reserve๊ฐ์ ์ฐจ์ก์ด ๋ค์ด๊ฐ๊ฒ ๋ฉ๋๋ค.
์์ 5๋ฒ์์ ๊ณต๊ฒฉ์๊ฐ Pool์ 442,9817WERX ๋ฅผ ์ก๊ธํ์ผ๋ฏ๋ก, ๊ทธ ์ก๊ธ์ก์ด ์ฐจ์ก์ด ๋ฉ๋๋ค.
โข
function _safeTransfer
function _safeTransfer(address token, address to, uint value) private {
(bool success, bytes memory data) = token.call(abi.encodeWithSelector(SELECTOR, to, value));
require(success && (data.length == 0 || abi.decode(data, (bool))), 'UniswapV2: TRANSFER_FAILED');
}
Solidity
๋ณต์ฌ
_safeTransfer ์์๋ token.call์ ์ฌ์ฉํด ํจ์๋ฅผ ํธ์ถํ๋๋ฐ, abi.encodeWithSelector(SELECTOR, to, value) ๋ ํธ์ถํ ํจ์์ ํด๋น ํจ์์ ์ ๋ฌ๋ ์ธ์๋ฅผ ์ธ์ฝ๋ฉํ๋ ์ญํ ์ ํฉ๋๋ค. SELECTOR๋ ํธ์ถํ๋ ค๋ ํจ์๋ฅผ ๋ํ๋ด๋ ๋ฐ์ดํธ ๊ฐ์
๋๋ค.
bytes4 private constant SELECTOR = bytes4(keccak256(bytes('transfer(address,uint256)')));
Solidity
๋ณต์ฌ
SELECTOR๋ transferํจ์๋ก ์ ์๋ฅผ ํด๋์์ต๋๋ค.
๊ฒฐ๊ณผ์ ์ผ๋ก, Uniswap V2: WERX 2 Pool์ skim โ _safeTransfer โ token.call โ transfer(Uwerx) โ _transfer(Uwerx) ์์๋๋ก ํธ์ถ์ด ๋ฉ๋๋ค.
๊ณต๊ฒฉ์๋ skim์ ํธ์ถํ ๋, ์์ ์ฃผ์๋ฅผ uniswapPoolAddress(0x1) ๋ก ์ค์ ํ์ฌ ์ทจ์ฝ์ ์ ํธ๋ฆฌ๊ฑฐ ํ์์ต๋๋ค. skim ๋ถํฐ transfer (Uwerx)๊น์ง ๊ณผ์ ์ ์์ ๊ธฐ์ ํ์์ผ๋ฏ๋ก, _transfer (Uwerx) ๋ถํฐ ์ค๋ช
ํ๊ฒ ์ต๋๋ค.
โข
function _transfer
function _transfer(
address from,
address to,
uint256 amount
) internal virtual {
//...//
//...//
unchecked {
_balances[from] = fromBalance - amount;
// Overflow not possible: the sum of all balances is capped by totalSupply, and the sum is preserved by
// decrementing then incrementing.
_balances[to] += amount;
}
if (to == uniswapPoolAddress) {
//...//
uint256 userTransferAmount = (amount * 97) / 100;
uint256 marketingAmount = (amount * 2) / 100;
uint256 burnAmount = amount - userTransferAmount - marketingAmount;
_burn(from, burnAmount);
//...//
Solidity
๋ณต์ฌ
Uwerx.sol์ _transfer ์ ์ธ์๊ฐ ์ด๋ป๊ฒ ๋ค์ด๊ฐ๋์ง ๋ณด๊ฒ ์ต๋๋ค.
โฆ
From : Uniswap V2: WERX 2 Pool
โฆ
To : uniswapPoolAddress(0x1)
โฆ
Amount : 4,429,817WERX ( reserve ๊ฐ๊ณผ ์ค์ ํ์ balance ์ฐจ์ก)
โฆ
burnAmount : 4,429,817WERX/100 = 44,298WERX
์ด๋ ๊ฒ ์ธ์ ๊ฐ์ด ๋ค์ด๊ฐ๊ฒ ๋๊ณ , if๋ฌธ์ผ๋ก ์ง์
ํ์ฌ _burn ์ ํธ์ถํ๊ฒ ๋ฉ๋๋ค.
โข
function _burn
function _burn(address account, uint256 amount) internal virtual {
require(account != address(0), "ERC20: burn from the zero address");
_beforeTokenTransfer(account, address(0), amount);
uint256 accountBalance = _balances[account];
require(accountBalance >= amount, "ERC20: burn amount exceeds balance");
unchecked {
_balances[account] = accountBalance - amount;
// Overflow not possible: amount <= accountBalance <= totalSupply.
_totalSupply -= amount; [4]
}
emit Transfer(account, address(0), amount);
_afterTokenTransfer(account, address(0), amount);
}
Solidity
๋ณต์ฌ
_burn ํจ์์ account์ Uniswap V2: WERX 2 Pool ์ ์ฃผ์๊ฐ ๋ค์ด๊ฐ๊ฒ ๋๊ณ , ๊ณต๊ฒฉ์๊ฐ ๊ธฐ์กด์ Pool์ ์๋ WERX์ 100๋ฐฐ(4,429,817WERX) ๋งํผ ์ก๊ธํด์ imbalance๋ฅผ ๋ง๋ค์๊ธฐ ๋๋ฌธ์, Pool์ 1% ์ฆ, ๊ฑฐ์ Pool์ ์๋ ์์ก๋งํผ burn์ ๋นํ๊ธฐ๋๋ฌธ์ ์๋์ WERX๋ง ๋จ๊ฒ๋ฉ๋๋ค.
7.
๊ทธ ๋ค์์ผ๋ก, sync๋ฅผ ํธ์ถํ์ฌ Pool์ ์๊ณ ๋ฅผ update์ํต๋๋ค.
pool์๋ 100WERX ์ 20,174WETH ๊ฐ ๋จ๊ฒ๋ฉ๋๋ค.
8.
์ฐจ์ต์ ์ป๊ธฐ ์ํด ๊ณต๊ฒฉ์๋ 4. ๊ณผ์ ์์ Pool์ ์ก๊ธ ํ๊ณ ๋จ์ 623,820WERX ๋ฅผ WERX -> WETH ๋ก ๋ค์ swap์ ํฉ๋๋ค. swap์ ํ๋ฉด WERX์ ํ ํฐ ๊ฐ์น๊ฐ ๊ธ ์์นํ๊ธฐ ๋๋ฌธ์, Pool์ ์๋ ์ ์ฒด 20,174WETH ๋ฅผ ๋ฐ๊ฒ ๋ฉ๋๋ค.
9.
์ต์ข
์ ์ผ๋ก, ๊ณต๊ฒฉ์๋ flashloan์ผ๋ก ๋ฐ์ 20,000WETH ๋ฅผ ์ํํ๊ณ , 174WETH ๋งํผ์ ์ด์ต์ ์ป๊ฒ ๋ฉ๋๋ค. ํด๋น ๊ธ์ก์ ๊ณต๊ฒฉ์์๊ฒ ์์ง ๊ทธ๋๋ก ๋จ์์์ต๋๋ค.
4. Conclusion
์ด๋ฒ ์ทจ์ฝ์ ์ Uwerx์ _transfer ํจ์์์ uniswapPoolAddress ๋ก ์ก๊ธํ๋ ๋ก์ง์์ ๋ฐ์ํ์์ต๋๋ค. flashloan Attack์ ๋น๋ฒํ๊ฒ ๋ฐ์ํ๋ ๊ณต๊ฒฉ์ผ๋ก ํฐ ๊ธ์ ์ ์์ค๋ก ์ด์ด์ง ์ ์์ต๋๋ค.
ํ ํฐ์ transfer ํ๊ฑฐ๋ burn ํ ๋๋ ๋ถ์ ํํ ๊ณ์ฐ์ด ๋์ง ์๋์ง ์ ๋ฐํ ํ์ธ์ด ํ์ํฉ๋๋ค.
78ResearchLab์์๋ ์ค๋งํธ ์ปจํธ๋ํธ ์ฝ๋/์๋น์ค ๋ก์ง์ ๋ํ ์ ์ /๋์ ๋ถ์์ผ๋ก ๋ณด์ ๊ฐ์ฌ๋ฅผ ์ํํ๊ณ ์์ต๋๋ค.
๋ณด์ ๊ฐ์ฌ๋ฅผ ํตํด ์ด๋ฌํ ๊ณต๊ฒฉ์ผ๋ก๋ถํฐ ์์ ํ Web3 ํ๊ฒฝ์ ๋ง๋ค์ด ๋๋ฆฝ๋๋ค.
contact@78researchlab.com ๋ก ๋ฉ์ผ์ ๋ณด๋ด์ฃผ์ ๋ค๋ฉด ์ฐ๋ฝ๋๋ฆฌ๊ฒ ์ต๋๋ค.




















