Kwenta

发布于 2023-07-18到 Mirror 阅读

StakingV2 Migration Pause - Post Mortem

At 2 AM UTC Friday, July 14th, 2023, a potential vulnerability was discovered by Kwenta smart contract engineers in the set of StakingV2 contracts that put newly minted inflationary rewards at risk. This was later validated by a proof of concept (PoC) test case. Relevant parties were alerted and gathered. Swift action was taken by the Kwenta protocolDAO (pDAO) to pause the StakingV2 contracts to secure staker funds.

Analysis continued into the weekend, with auditors engaged, to fully understand the issue and prepare a potential rollback of the migration.

A decision to reverse the migration was made on Monday.

Thankfully, the launch of StakingV2 had only occurred a few hours prior, and the vulnerability was discovered internally before funds were at risk. As of this time, all funds are safe, and a reverse migration is taking place.

Issue At a high level

The vulnerability stems from the fact that StakingV1, when stake and unstake actions occur, does not alert the StakingV2 contract of these state changes.

The Staking contract works when an action (like stake/unstake) is performed, and the amount of reward tokens owed to you in the time (based on your stake) that has passed is recorded. Because StakingV1 actions do not alert StakingV2 properly, it's possible to lose all or extract more rewards than is deserved.

The migration process was halted because the bug could be encountered during the migration flow from StakingV1 to StakingV2. If a V1 staker begins accruing V2 rewards and then attempts to unstake from StakingV1 in order to migrate to StakingV2, all V2 accrued rewards (or rewards pertaining to the now unstaked "liquid" stake) are lost because the earned amount is not properly recorded on the V2 contract.

https://github.com/Kwenta/token/commit/dfcc4ff82fce5090b57e28b1f74a88b4ca252243

function test_Actual() public {
        fundAccountAndStakeV1(user1, 10 ether);

        vm.warp(block.timestamp + 1 weeks);

        vm.prank(user1);
        stakingRewardsV1.unstake(10 ether);

        uint reward = stakingRewardsV2.rewards(user1);
        assert(reward > 0);
}

Eventually, this issue revealed deeper issues within the StakingV2 contracts. For example, it is possible to manipulate the V1 stake function to generate more rewards than are deserved.

By increasing your V1 stake (potentially with a flashloan) before claiming rewards, your earned rewards can be manipulated because your previous staked amount isn’t properly accounted for on the V2 contract.

Therefore, when you go to claim your rewards, it will be based on your new staked balance * the time since the original stake event.

function test_Flash_Attack() public {
        uint256 totalNewRewards = 100_000 ether;
        fundAccountAndStakeV2(user1, 100 ether);
        fundAccountAndStakeV2(user2, 100 ether);
        addNewRewardsToStakingRewardsV2(totalNewRewards);
        vm.warp(block.timestamp + 4 weeks);

        // flash attack
        uint256 fundsBorrwedViaFlashLoan = 100_000 ether;
        fundAccountAndStakeV1(user3, fundsBorrwedViaFlashLoan);
        vm.prank(user3);
        stakingRewardsV2.getReward();

        uint256 escrowedBalance = rewardEscrowV2.escrowedBalanceOf(user3);
        // users claimed at least 99.8% of all the rewards
        assertCloseTo(escrowedBalance, totalNewRewards, 200 ether);
        // then user would pay back the flash loan
    }

Kwenta has multiple security processes to prevent issues like these; however, there is always a non-zero chance of an incident. All new code undergoes thorough testing utilizing Foundry's testing suite. Kwenta Engineering makes use of unit, integration, fork tests, and extended testing tools like fuzz and invariant tests.

We enforce 80+% code coverage. The code is then reviewed internally by the Core Contributors (CCs) and then external auditors, such as our partners Macro and Omniscia, for vulnerabilities and potential edge cases. Lastly, post-launch, we set up monitoring tools (Tenderly alerts for invariants) and bug bounty programs to limit any potential vulnerabilities that may crop up in production.

In this case, the logic behind the Staking contracts during development was a misunderstanding. It was not enough to pull StakingV1 balances into the StakingV2 contract only when V2 balances were changed. Unfortunately, there were a few internal process failures that could have mitigated this issue.

StakingV2 is a large, intricate system with many new technical features and expected legacy support. Most of these changes were delivered as a monolithic pull request on Github, making it hard for the internal team to scrutinize every feature and modification. Secondly, even though testing was comprehensive, the specific test cases where this could have been detected were not included.

Lastly, during audits, auditors did not thoroughly review this interaction between StakingV1 and StakingV2 because it was presumed that StakingV1 functionality was out of the scope of this upgrade. These process failures forwarded this bug into production. Outcomes Kwenta CCs have decided the best course of action is to reverse the migration process until StakingV2 issues have been resolved and the code re-audited. This is the best decision for the security of the protocol. The current plan is to remove all StakingV1-related code from the StakingV2 contracts. A migration contract will also help stakers port their V1 escrow to V2 escrow (therefore, it can be staked).

In order to enable reverse migration, the following need to happen:

- Upgrade the StakingRewardsV2 contracts to unpause the unstake and unstakeEscrow functions

- Provide a recovery function to collect the newly minted, not yet distributed rewards.

- Upgrade the RewardEscrowV2 function to fix the earlyVestFee at 0, so users can vest any rewards collected in the V2 contracts without penalty

- Collect the newly minted supply from the StakingRewardsV2 contract

- Point the SupplySchedule back to StakingRewardsV1

- Launched a simple UI to help stakers unstake from V2 Possible via governance: Decrease the treasuryDiversion to route back the missed rewards to V1 stakers

For future migrations, an alternative approach will be used

A custom migration contract will be built that allows migration from V1 to V2 for escrow entries. Then the Staking V2 contracts will no longer check V1 balances; instead, all rewards will go to V2 stakers, but everyone will be able to migrate.

Join the Kwenta Community

If you haven't already, join the Kwenta community on Discord.

To be the first to learn about new updates to Kwenta, follow us on Twitter.

To trade synthetic assets and futures, visit Kwenta.