path: /src/market-making/branches/FeeDistributionBranch/FeeDistributionBranch.sol
If the invariant fails (i.e. if lastProtocolReward + lastVaultReward != lastReceivedWeth), then there is a math error in the reward splitting logic.
In this implementation the “leftover” is added to the vault reward to account for any rounding error in the fixed‑point math. The invariant ensures that the rounding “leftover” exactly makes up any difference so that all received WETH is fully distributed.
pragma solidity ^0.8.25;
import { UD60x18, ud60x18 } from "../../lib/prb-math/src/UD60x18.sol";
import "../../lib/prb-math/src/ud60x18/Helpers.sol" as Helpers;
import { Constants } from "@zaros/utils/Constants.sol";
import { Math } from "@zaros/utils/Math.sol";
import "forge-std/Test.sol";
contract DummyMarket {
uint256 public protocolReward;
uint256 public vaultReward;
function receiveWethReward(
address ,
UD60x18 protocolRewardX18,
UD60x18 vaultRewardX18
) external {
protocolReward = UD60x18.unwrap(protocolRewardX18);
vaultReward = UD60x18.unwrap(vaultRewardX18);
}
function getConnectedVaultsIds() external pure returns (uint256[] memory) {
return new uint256[](0);
}
}
contract RewardDistributor {
function handleWethRewardDistribution(
DummyMarket market,
address assetOut,
UD60x18 receivedWethX18,
UD60x18 feeRecipientsSharesX18
) public {
UD60x18 receivedProtocolWethRewardX18 = receivedWethX18.mul(feeRecipientsSharesX18);
UD60x18 receivedVaultsWethRewardX18 =
receivedWethX18.mul(ud60x18(Constants.MAX_SHARES).sub(feeRecipientsSharesX18));
UD60x18 leftover = receivedWethX18.sub(receivedProtocolWethRewardX18).sub(receivedVaultsWethRewardX18);
receivedVaultsWethRewardX18 = receivedVaultsWethRewardX18.add(leftover);
market.receiveWethReward(assetOut, receivedProtocolWethRewardX18, receivedVaultsWethRewardX18);
}
}
contract RewardDistributionInvariantTest is Test {
RewardDistributor public distributor;
DummyMarket public dummyMarket;
uint256 public lastProtocolReward;
uint256 public lastVaultReward;
uint256 public lastReceivedWeth;
address constant assetOut = address(0xBEEF);
function setUp() public {
distributor = new RewardDistributor();
dummyMarket = new DummyMarket();
}
function testFuzz_handleWethRewardDistribution(uint256 feeSharesRaw, uint256 receivedWethRaw) public {
uint256 feeShares = bound(feeSharesRaw, 0, Constants.MAX_SHARES);
uint256 receivedWeth = bound(receivedWethRaw, 0, 1e30);
UD60x18 feeRecipientsSharesX18 = ud60x18(feeShares);
UD60x18 receivedWethX18 = ud60x18(receivedWeth);
distributor.handleWethRewardDistribution(dummyMarket, assetOut, receivedWethX18, feeRecipientsSharesX18);
lastProtocolReward = dummyMarket.protocolReward();
lastVaultReward = dummyMarket.vaultReward();
lastReceivedWeth = receivedWeth;
}
function invariant_totalRewardIsDistributed() public {
uint256 totalDistributed = lastProtocolReward + lastVaultReward;
assertEq(
totalDistributed,
lastReceivedWeth,
"Invariant failure: total distributed rewards do not equal received rewards"
);
}
}
Failing tests:
Encountered 1 failing test in test/invariant/DummyMarket.t.sol:RewardDistributionInvariantTest
[FAIL: invariant_totalRewardIsDistributed persisted failure revert]
[Sequence]
sender=0x00000000000000000000000000000000000009AD addr=[test/invariant/DummyMarket.t.sol:RewardDistributor]0x5615dEB798BB3E4dFa0139dFa1b3D433Cc23b72f calldata=handleWethRewardDistribution(address,address,uint256,uint256) args=[0x000000000000000000000000000000000A9254e3, 0x0000000000000000000000000000000000000043, 4639, 2939553533 [2.939e9]]
Ran 2 tests for test/invariant/DummyMarket.t.sol:RewardDistributionInvariantTest
[FAIL: invariant_totalRewardIsDistributed persisted failure revert]
[Sequence]
sender=0x00000000000000000000000000000000000009AD addr=[test/invariant/DummyMarket.t.sol:RewardDistributor]0x5615dEB798BB3E4dFa0139dFa1b3D433Cc23b72f calldata=handleWethRewardDistribution(address,address,uint256,uint256) args=[0x000000000000000000000000000000000A9254e3, 0x0000000000000000000000000000000000000043, 4639, 2939553533 [2.939e9]]
invariant_totalRewardIsDistributed() (runs: 1, calls: 1, reverts: 1)
Mis-calculation of the rewards. I tagged this as high since this is Defi protocol and liquidity providers (LPs) would depend on correct calculations of fees and reward.