Skip to main content
This is a fairly simple bug that let anyone mint rewardTokens. Background: https://x.com/storming0x/status/1473321779250802693

Description

  • The deposit function lets anyone mint rewardTokens if
    • the from address is a contract and has an owner and where msg.sender == owner
  • It is intended that the from address sends the underlying token to this contract
    • But it does not check whether delegatedTransferERC20 is implemented correctly
    • it can be a noop
  • The attacker can mint rewardTokens to himself

Solution

While this is an obvious bug, an invariant could have prevented an exploit of the bug. Invariant:
function assertionIsSolvent() public view {
    require(visr.balanceOf(address(adopter)) >= vvisr.totalSupply(), "Reward tokens are undercollateralized");
}
Alternatively, the assertion could test the correct behavior of the previous transaction. The assertion could be triggered by a call to the deposit function.
function assertion_triggerDeposit_remainsSolvent() public view {
    // Get pre-transaction state
    ph.forkPreTx();
    uint256 preBalanceCollateral = visr.balanceOf(address(adopter));
    uint256 preTotalSupplyRewards = vvisr.totalSupply();

    // Get post-transaction state
    ph.forkPostTx();
    uint256 postBalanceCollateral = visr.balanceOf(address(adopter));
    uint256 postTotalSupplyRewards = vvisr.totalSupply();

    // Compare ratios using cross-multiplication to avoid precision loss
    // preBalance / preSupply >= postBalance / postSupply
    // => preBalance * postSupply >= postBalance * preSupply
    require(
        preBalanceCollateral * postTotalSupplyRewards >= postBalanceCollateral * preTotalSupplyRewards,
        "Reward tokens are undercollateralized"
    );
}