티스토리 뷰
Executive Summary
BNB 체인에서 DxSale의 구형 v1 레거시 유동성 락커의 비즈니스 로직 취약점으로 인해, locker안에 있던 LP 토큰들이 탈취당하여 관련된 1,400개 이상의 유동성 풀이 피해를 받아 약 73만 달러 규모의 해킹이 발생했다. 아래 설명하는 내용들은 blocksec의 weekly review를 기반으로 하며 추가적인 PoC와 사진 설명을 첨부하였습니다.
- Type : Incorrect Business Logic & Key Compromise
- Incident Date : 2026.05.29
- Chain : BNB Chain
- Total Lost : $7.3M
- Root Cause : 패치되지 않은 오래된 코드 + compromised key
- Lessons Learned : 배포 시점 이후 공격당하지 않았더라도 안전한 것이 아니며, 지속적인 점검과 권한 관리가 필요
- Vulnerable Contract(DxLockLPDep, legacy locker) : 0xEb3a9C56d963b971d320f889bE2fb8B59853e449
- Attacker Contract(Exploiter) : 0xC4574DDEF299e7E563971e200433e592EeaaFA69
- Attacker EOA (Deployer): 0x47BAcf935066b802EAA0067eC14AB035B24eB78b
- Tx ID :
- 소유권 이전 - 0x23e331a81e496890c8f6147a672c6bc45cc2d2dd17547e72713b6ee070b38d41
- BNB/BNBC 스왑 및 Pair 생성 - 0xb0d988a56f7e54197c800bf9a32a11e219f7b3d0c399005dc0fbbd3dbe691b31
- 수수료 변경 및 신규 lock position 생성 - 0xf0a45b660eebc2d6dac7a5a3add28d7388e45fc413488e9e16eb5a8206cb1f20
- 공격 Tx - 0x437b26887ba03c08b7541a86e8eeda5e92a9059244fcf814c721dc13f2c1b303
- References :
- https://blocksec.com/blog/web3-security-dxsale-squidrouter-more?utm_source=X&utm_content=weekly_blog
- https://blocksec.com/security-incident?hash=0xac8f5ed6cac1e6c6d6e96bad34f5211c8ae23cf05866b2531f3f3e63bcd36df9
- https://bingx.com/en/news/post/dxsale-exploit-drains-m-from-bnb-chain-liquidity-pools-on-may
Background
DxSale은 BNB 체인에서 운영되는 토큰 판매 및 유동성 락킹 플랫폼으로, 그 중 DxLock 컴포넌트는 프로젝트 개발자가 LP 토큰을 시간 제한 금고(time-locked vault)에 잠글 수 있도록 하여, 투자자들에게 유동성이 러그풀(rug-pull) 당하지 않는다는 보장을 제공한다.
locking 메커니즘은 다음과 같이 동작한다.
- 사용자가 토큰을 locker contract에 예치
- 호출자 주소와 토큰 ID 조합으로 인덱싱된 lock record 생성
- lock record에는 락 존재 여부, 토큰이 잠겨있는지, 잠긴 수량, 잠금해제 시점, 토큰 주소가 저장됨
- 잠금 기간이 만료되면 사용자가 unlockToken(tokenId) 를 호출하여 자신의 토큰 인출 가능
락커는 여러 사용자의 토큰을 동시에 공유 잔액에 보관하기 때문에, 개별 할당량을 별도의 lock record를 통해 추적해야 한다.
전체 풀의 보안은 인출 과정에서 호출자의 record를 올바르게 검증하고, 전송 이후 해당 기록을 업데이트 하는데 달려있다.
Vulnerability Analysis
unlockToken()에는 세가지 중첩된 결함이 존재한다. 각각 상태 업데이트나 검증이 누락된 부분이 있다.
function unlockToken(uint256 userLockerNumber) public {
require(DXLOCKERLP[msg.sender][userLockerNumber].exists, "err: LockDep - user doesnt have a locker!");
require(DXLOCKERLP[msg.sender][userLockerNumber].locked, "err: LockDep - user's tokens are not locked!");
uint256 payoutAmount = DXLOCKERLP[msg.sender][userLockerNumber].lockedAmount;
require(payoutAmount > 0, "err: LockDep - must have atleast 1 payout vested!");
if(block.timestamp > DXLOCKERLP[msg.sender][userLockerNumber].lockedTime){
DXLOCKERLP[msg.sender][userLockerNumber].locked = false;
}
require(IERC20(DXLOCKERLP[msg.sender][userLockerNumber].lpAddress).balanceOf(address(this)) >= payoutAmount, "err: Locker - no more tokens left to refund");
require(IERC20(DXLOCKERLP[msg.sender][userLockerNumber].lpAddress).transfer(msg.sender,payoutAmount), "err: Locker - Token refund to creator failed!");
emit onUnlock(msg.sender,DXLOCKERLP[msg.sender][userLockerNumber].lpAddress,payoutAmount,block.timestamp);
}
No lock-period enforcement :
잠금 기간에 관계없이 언제든 토큰을 인출할 수 있음을 말한다.
잠금 기간 검사가 require이 아닌 if 문에서 이루어진다. 잠금 해제 날짜 전에 이 함수를 호출하면 트랜잭션을 revert시키는 것이 아니라, locked flag만 아무 변화가 없는 채로 지나간다.
Locked amount never zeroed :
호출자에게 토큰 전송 후에 lockedAmount를 zero로 만들지 않는다. 이렇게 되면 해당 함수를 반복적으로 호출하여 자금을 계속해서 인출할 수 있다.
Shared-balance check instead of individual allocation :
호출자에게 토큰을 인출하기 전 잔고 확인 시, 호출자 개인의 잔고를 확인하는게 아닌 컨트랙트의 잔고를 확인한다. 이는 호출자가 충분하지 않은 lock position을 가지고도 전체 풀의 자금을 인출할 수 있게 된다.
위의 세가지 상태 업데이트 및 검증 누락이 결합되면서 함수의 모든 종료(revert)조건이 사라졌다. lock position이 있는 누구나 악용가능하며, 공격시에 소유자 권한도 필요없다.
Attack Analysis
DxSale 배포자 계정(0x47bacf93)은 EIP-7702 위임을 통해 운영되었으며, 26년 4월 15일부터 비정상적인 활동을 보였다. 대규모 소유권 이전, 수수료 변경, LP 언락 작업들이 포함되어 있었다.
26년 5월 26일 배포자가 피해 락커(DxLockLPDep, 0xeb3a9c56)의 소유권을 공격자 컨트랙트(Exploiter, 0xC4574DDE)로 이전했다.

- 공격자가 PancakeSwap에서 BNB를 BNBC로 교환 후, 유동성을 공급하여 새로운 Cake-LP(BNB/BNBC) Pair를 생성하여 0.323 LP 토큰을 받았다.
- 이전 단계에서 소유권 이전을 통해 획득한 owner 권한을 사용해, changeFees(1) 를 호출하여 lock 생성 수수료를 1 wei로 설정했다.
- 이어서 createLocker 를 호출하여 0.323 LP 토큰을 새로운 lock position에 예치했다.
- 예치 이후 즉시, changeFees(10^36)을 호출하여 수수료를 급격히 올려 다른 사용자가 새로운 락을 생성하지 못하도록 차단했다.
- 공격자가 exploit 함수 0x11b432b4 호출하여, 단일 트랜잭션안에서 반복적인 unlockToken() 호출을 실행했다.
- 공격자는 PancakeSwap에서 removeLiquidity()를 호출하여 인출한 LP 토큰을 BNB와 WBNB로 바꿨다.
- 공격자는 PancakeSwap을 통해 기초 토큰을 WBNB와 BUSD로 교환 후, 외부 주소로 전송했다. BSScan은 26년5월 28일부터 31일까지 4일간 공격자 컨트랙트에서 123,447건의 트랜잭션을 기록했다.
위의 공격 과정 중 5번에 해당하는 트랜잭션 0x437b2688에서는 tokenId=131을 통해 unlockToken()을 500번 호출하여 161.4LP token 인출하였다.

Conclusion
이 해킹사고의 root cause는 unlockToken 함수에서 세 가지 상태 업데이트가 누락된 것이다. 각각의 직접적인 수정 방안은 다음과 같다.
- if 대신 require을 사용해 락 기간을 강제할 것
- 매번 출금 후 locked amount를 0으로 초기화
- caller의 개별 잔액을 확인할 것
위 결함들을 통해 lock position을 만든 모든 사용자가 owner 권한 없이도 반복 호출을 통해 전체 풀을 고갈시킬 수 있었다.
PoC
Defi-Incident-PoC/src/test/2026-05/DxSale_exp.sol at main · chettz/Defi-Incident-PoC
Defi-Incident-PoC/src/test/2026-05/DxSale_exp.sol at main · chettz/Defi-Incident-PoC
Contribute to chettz/Defi-Incident-PoC development by creating an account on GitHub.
github.com
'Web3 > Defi-Hack' 카테고리의 다른 글
| SC Exploit | Reentrancy (0) | 2025.10.15 |
|---|---|
| SC Exploit | Denial-of-Service (0) | 2025.10.14 |
| Static Analysis Tools (0) | 2025.10.13 |
- Total
- Today
- Yesterday
- gaslimit
- price manipulation
- view functions
- eip7702
- ERC20
- MagicNumber
- oracles
- storageslot
- 토큰전송
- dynamic array slot
- aderyn
- 모빌리티 보안
- 블록구조
- smart contract
- eip1599
- ethereum
- Delegatecall
- puzzlewallet
- type2 transaction
- ethrnaut
- DEFI
- Blockchain
- 토큰화주식
- rebase token
- 계정추상화
- solidity
- 이더넛
- Ethernaut
- DexTwo
- erc4337
| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 1 | 2 | 3 | 4 | 5 | ||
| 6 | 7 | 8 | 9 | 10 | 11 | 12 |
| 13 | 14 | 15 | 16 | 17 | 18 | 19 |
| 20 | 21 | 22 | 23 | 24 | 25 | 26 |
| 27 | 28 | 29 | 30 |