Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat/twl reward bracket #251

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitmodules
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,6 @@
path = dep/tapioca-mocks
url = https://github.com/Tapioca-DAO/tapioca-mocks
branch = main
[submodule "dep/tapiocaz"]
path = dep/tapiocaz
url = https://github.com/Tapioca-DAO/tapiocaz
10 changes: 8 additions & 2 deletions contracts/governance/twTAP.sol
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,8 @@ contract TwTAP is
ITwTapMagnitudeMultiplier public twTapMagnitudeMultiplier;
uint256 constant MULTIPLIER_PRECISION = 1e18;

uint256 public constant REWARD_MULTIPLIER_BRACKET = 100_000; // 10% brackets
uint256 public constant REWARD_CAP_BRACKET = 50_000; // 5% cap to floor/ceil the reward
/// @notice The minimum amount of weeks to start decaying the cumulative
uint256 public minWeeksToDecay = 2;

Expand Down Expand Up @@ -412,7 +414,11 @@ contract TwTAP is

// Revert if the lock is x time bigger than the cumulative
if (magnitude > (lastEpochCumulative * growthCapBps) / 1e4) revert NotValid();
uint256 multiplier = computeTarget(dMIN, dMAX, magnitude * _amount, pool.cumulative * pool.totalDeposited);
uint256 multiplier = capCumulativeReward(
computeTarget(dMIN, dMAX, magnitude * _amount, pool.cumulative * pool.totalDeposited),
REWARD_MULTIPLIER_BRACKET,
REWARD_CAP_BRACKET
);

// Calculate twAML voting weight
bool divergenceForce;
Expand Down Expand Up @@ -737,7 +743,7 @@ contract TwTAP is
}

/**
* @notice Set the minimum amount of weeks to start decaying the cumulative
* @notice Set the minimum amount of weeks to start decaying the cumulative
*/
function setMinWeeksToDecay(uint256 _minWeeksToDecay) external onlyOwner {
minWeeksToDecay = _minWeeksToDecay;
Expand Down
8 changes: 7 additions & 1 deletion contracts/options/TapiocaOptionBroker.sol
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,8 @@ contract TapiocaOptionBroker is Pausable, Ownable, PearlmitHandler, IERC721Recei
ITobMagnitudeMultiplier public tobMagnitudeMultiplier;
uint256 constant MULTIPLIER_PRECISION = 1e18;

uint256 public constant REWARD_MULTIPLIER_BRACKET = 50_000; // 5% brackets
uint256 public constant REWARD_CAP_BRACKET = 20_000; // 2% cap to floor/ceil the reward
/// @notice The minimum amount of weeks to start decaying the cumulative
uint256 public minWeeksToDecay = 2;

Expand Down Expand Up @@ -347,7 +349,11 @@ contract TapiocaOptionBroker is Pausable, Ownable, PearlmitHandler, IERC721Recei
uint256 target;
{
(uint256 totalPoolShares,) = tOLP.getTotalPoolDeposited(uint256(lock.sglAssetID));
target = computeTarget(dMIN, dMAX, magnitude * uint256(lock.ybShares), pool.cumulative * totalPoolShares);
target = capCumulativeReward(
computeTarget(dMIN, dMAX, magnitude * uint256(lock.ybShares), pool.cumulative * totalPoolShares),
REWARD_MULTIPLIER_BRACKET,
REWARD_CAP_BRACKET
);
}

// Revert if the lock 4x the last epoch cumulative
Expand Down
16 changes: 16 additions & 0 deletions contracts/options/twAML.sol
Original file line number Diff line number Diff line change
Expand Up @@ -154,4 +154,20 @@ abstract contract TWAML is FullMath {
z = 1;
}
}

/**
* @notice Will ceil/floor the reward amount to the nearest number by increment of `_multiplier`.
* @dev e.g: tOB: 1, multiplier: 5, cap: 2
* twTap: 1, multiplier: 10, cap: 5
*/
function capCumulativeReward(uint256 _amount, uint256 _multiplier, uint256 _cap) internal pure returns (uint256) {
uint256 remainder = _amount % _multiplier;
if (remainder == 0) {
return _amount;
}
if (remainder > _cap) {
return _amount + _multiplier - remainder;
}
return _amount - remainder;
}
}
1 change: 1 addition & 0 deletions dep/tapiocaz
Submodule tapiocaz added at 4b5494
12 changes: 7 additions & 5 deletions test/unit/UnitBaseTest.sol
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,9 @@ contract UnitBaseTest is TestHelper {
address public ENDPOINT_B;

function setUp() public virtual override {
vm.label(adminAddr, "Admin");
vm.label(aliceAddr, "Alice");
vm.label(bobAddr, "Bob");

// Peripheral contracts
pearlmit = createPearlmit(adminAddr);
Expand Down Expand Up @@ -280,14 +282,14 @@ contract UnitBaseTest is TestHelper {
address _mainToken,
IPearlmit _pearlmit,
address _owner
) internal returns (Penrose penrose, BigBang bbMasterContract, Singularity sglMasterContract) {
) internal returns (Penrose _penrose, BigBang bbMasterContract, Singularity sglMasterContract) {
address tapStrat = address(createYieldBoxEmptyStrategy(_yb, _tap));
address mainTokenStrat = address(createYieldBoxEmptyStrategy(_yb, _mainToken));

uint256 tapAssetId = IYieldBox(_yb).registerAsset(TokenType.ERC20, _tap, tapStrat, 0);
uint256 mainTokenAssetId = IYieldBox(_yb).registerAsset(TokenType.ERC20, _mainToken, mainTokenStrat, 0);

penrose = new Penrose(
_penrose = new Penrose(
IYieldBox(_yb),
ICluster(_cluster),
BoringIERC20(_tap),
Expand All @@ -301,9 +303,9 @@ contract UnitBaseTest is TestHelper {
bbMasterContract = new BigBang();
sglMasterContract = new Singularity();
vm.startPrank(adminAddr);
penrose.registerBigBangMasterContract(address(bbMasterContract), IPenrose.ContractType.mediumRisk);
penrose.registerSingularityMasterContract(address(sglMasterContract), IPenrose.ContractType.mediumRisk);
penrose.setUsdoToken(address(usdoMock), usdoMockTokenId);
_penrose.registerBigBangMasterContract(address(bbMasterContract), IPenrose.ContractType.mediumRisk);
_penrose.registerSingularityMasterContract(address(sglMasterContract), IPenrose.ContractType.mediumRisk);
_penrose.setUsdoToken(address(usdoMock), usdoMockTokenId);
vm.stopPrank();
}

Expand Down
46 changes: 46 additions & 0 deletions test/unit/options/twL/twl_capCumulativeReward.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity 0.8.22;

import {TWAML} from "contracts/options/twAML.sol";

import "forge-std/Test.sol";

contract twl_CapCumulativeReward is TWAML, Test {
uint256 constant TOB_MULTIPLIER = 5 * 1e4;
uint256 constant TOB_CAP = 2 * 1e4;
uint256 constant MIN_TOB_AMOUNT = 1e4;
uint256 constant MAX_TOB_AMOUNT = 50 * 1e4;

uint256 constant TWTAP_MULTIPLIER = 10 * 1e4;
uint256 constant TWTAP_CAP = 5 * 1e4;
uint256 constant MIN_TWTAP_AMOUNT = 1e4;
uint256 constant MAX_TWTAP_AMOUNT = 100 * 1e4;

/**
* @dev It should cap the cumulative reward to the nearest multiplier of 5.
*/
function test_shouldCapCumulativeRewardOnTob(uint256 _amount) external {
_amount = bound(_amount, MIN_TOB_AMOUNT, MAX_TOB_AMOUNT);

// it should cap the cumulative reward
assertEq(
capCumulativeReward(_amount, TOB_MULTIPLIER, TOB_CAP) % TOB_MULTIPLIER,
0,
"twl_CapCumulativeReward::test_shouldCapCumulativeRewardOnTob: Invalid amount"
);
}

/**
* @dev It should cap the cumulative reward to the nearest multiplier of 10.
*/
function test_shouldCapCumulativeRewardOnTwTap(uint256 _amount) external {
_amount = bound(_amount, MIN_TWTAP_AMOUNT, MAX_TWTAP_AMOUNT);

// it should cap the cumulative reward
assertEq(
capCumulativeReward(_amount, TWTAP_MULTIPLIER, TWTAP_CAP) % TWTAP_MULTIPLIER,
0,
"twl_CapCumulativeReward::test_shouldCapCumulativeRewardOnTwTap: Invalid amount"
);
}
}
21 changes: 21 additions & 0 deletions test/unit/options/twTap/twTapBaseTest.sol
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ contract twTapBaseTest is UnitBaseTest {

// Constants
uint256 public EPOCH_DURATION = 1 weeks;
uint256 public MIN_LOCK_DURATION = 4;
uint256 public MAX_LOCK_DURATION = 6;

// Addresses
address public PAYMENT_TOKEN_RECEIVER = address(bytes20(keccak256("PAYMENT_TOKEN_RECEIVER")));
Expand Down Expand Up @@ -124,4 +126,23 @@ contract twTapBaseTest is UnitBaseTest {
twTap.participate(aliceAddr, _amount, _duration * twTap.EPOCH_DURATION());
vm.stopPrank();
}

function _participate(address _to, uint256 _amount, uint256 _duration) internal {
vm.startPrank(_to);
tapOFT.freeMint(_to, _amount);
tapOFT.approve(address(pearlmit), _amount);
pearlmit.approve(20, address(tapOFT), 0, address(twTap), uint200(_amount), uint48(block.timestamp + 1));
twTap.participate(_to, _amount, _duration * twTap.EPOCH_DURATION());
vm.stopPrank();
}

function _boundValues(uint256 _lockAmount, uint256 _lockDuration) internal returns (uint256, uint256) {
_lockAmount = bound(_lockAmount, 1, type(uint88).max);
_lockDuration = bound(_lockDuration, MIN_LOCK_DURATION, MAX_LOCK_DURATION);
return (_lockAmount, _lockDuration);
}

function _toWeek(uint256 _timestamp) internal returns (uint256) {
return (_timestamp - twTap.creation()) / twTap.EPOCH_DURATION();
}
}
194 changes: 194 additions & 0 deletions test/unit/options/twTap/twTap_advanceWeek.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,194 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity 0.8.22;

import {twTapBaseTest, TwTAP} from "test/unit/options/twTap/twTapBaseTest.sol";
import {WeekTotals, Participation} from "contracts/governance/twTAP.sol";

contract twTap_advanceWeek is twTapBaseTest {
uint256 constant PARTICIPATION_ID = 1;

modifier whenParticipating(address _to, uint256 _lockAmount, uint256 _lockDuration) {
(_lockAmount, _lockDuration) = _boundValues(_lockAmount, _lockDuration);
_participate(_to, _lockAmount, _lockDuration);
_;
}

function test_RevertWhen_CallerDoesNotHaveRole() external {
// it should revert
vm.expectRevert(TwTAP.NotAuthorized.selector);
twTap.advanceWeek(1);
}

modifier whenCallerHasRole() {
_;
_resetPrank(adminAddr);
cluster.setRoleForContract(adminAddr, keccak256("NEW_EPOCH"), true);
}

function test_WhenTimeDidNotAdvanceEnoughToAdvanceWeek() external whenCallerHasRole {
// it should do nothing
vm.startPrank(adminAddr);
twTap.advanceWeek(1);

assertEq(
twTap.lastProcessedWeek(),
0,
"twTap_advanceWeek::test_WhenTimeDidNotAdvanceEnoughToAdvanceWeek: Invalid epoch"
);
}

modifier whenTimeAdvancedEnough() {
skip(twTap.EPOCH_DURATION());
_;
}

function test_WhenTimeAdvancedEnough(uint256 _lockAmount, uint256 _lockDuration)
external
whenParticipating(aliceAddr, _lockAmount, _lockDuration)
whenCallerHasRole
whenTimeAdvancedEnough
{
_resetPrank(adminAddr);

// it should emit AdvanceEpoch
vm.expectEmit(true, true, false, false);
emit TwTAP.AdvanceEpoch(1, 0);
twTap.advanceWeek(1);

// it should pass week net active votes
int256 netActiveVotesBef = twTap.weekTotals(0);
int256 netActiveVotesAft = twTap.weekTotals(1);
assertGt(
netActiveVotesAft,
netActiveVotesBef,
"twTap_advanceWeek::test_WhenTimeAdvancedEnough: Invalid week net active votes"
);

// it should update lastProcessedWeek
assertEq(twTap.lastProcessedWeek(), 1, "twTap_advanceWeek::test_WhenTimeAdvancedEnough: Invalid epoch");
}

function test_WhenDecayRateIsNotSet(uint256 _lockAmount, uint256 _lockDuration)
external
whenParticipating(aliceAddr, _lockAmount, _lockDuration)
whenCallerHasRole
whenTimeAdvancedEnough
{
_resetPrank(adminAddr);

// it should do nothing
(,,, uint256 cumulativeBef) = twTap.twAML();
twTap.advanceWeek(1);
(,,, uint256 cumulativeAft) = twTap.twAML();
assertEq(cumulativeAft, cumulativeBef, "twTap_advanceWeek::test_WhenDecayRateIsNotSet: Invalid cumulative");
}

modifier whenDecayRateIsBiggerThan0() {
_resetPrank(adminAddr);
twTap.setDecayRateBps(1000);
_;
}

function test_RevertWhen_EpochSmallerThan2(uint256 _lockAmount, uint256 _lockDuration)
external
whenParticipating(aliceAddr, _lockAmount, _lockDuration)
whenCallerHasRole
whenTimeAdvancedEnough
whenDecayRateIsBiggerThan0
{
// it should revert
_resetPrank(adminAddr);
vm.expectRevert(TwTAP.EpochTooLow.selector);
twTap.advanceWeek(1);
}

/// @notice We already skipped one epoch in `whenTimeAdvancedEnough()`
modifier whenEpochBiggerOrEqual2(uint256 _lockDuration) {
uint256 unlockEpoch = _getUnlockEpoch();
skip(twTap.EPOCH_DURATION() * (unlockEpoch - 1));
_;
}

modifier whenLiquidityDecreased() {
twTap.exitPosition(1); // First we need to exit the position to decrease the liquidity
_;
}

function test_WhenLiquidityDecreasedMoreThanTheDecayActivation(uint256 _lockAmount, uint256 _lockDuration)
external
whenParticipating(aliceAddr, _lockAmount, _lockDuration)
whenCallerHasRole
whenTimeAdvancedEnough
whenEpochBiggerOrEqual2(_lockDuration)
whenDecayRateIsBiggerThan0
whenLiquidityDecreased
{
_resetPrank(adminAddr);

(,,, uint256 cumulativeBef) = twTap.twAML();
// We need to skip the decay activation
skip(twTap.EPOCH_DURATION());
twTap.advanceWeek(_getUnlockEpoch());
(,,, uint256 cumulativeAft) = twTap.twAML();

// it should decay
assertGt(
cumulativeBef,
cumulativeAft,
"twTap_advanceWeek::test_WhenLiquidityDecreasedMoreThanTheDecayActivation: Invalid cumulative"
);
}

function test_WhenLiquidityDidNotDecreasedMoreThanHeDecayActivation(uint256 _lockAmount, uint256 _lockDuration)
external
whenParticipating(aliceAddr, 1, _lockDuration)
whenCallerHasRole
whenTimeAdvancedEnough
whenDecayRateIsBiggerThan0
whenEpochBiggerOrEqual2(_lockDuration)
whenLiquidityDecreased
{
// it should not decay

(,,, uint256 cumulativeBef) = twTap.twAML();
skip(twTap.EPOCH_DURATION());
twTap.advanceWeek(_getUnlockEpoch());
(,,, uint256 cumulativeAft) = twTap.twAML();

// it should decay
Participation memory participation = twTap.getParticipation(PARTICIPATION_ID);
assertEq(
cumulativeBef,
cumulativeBef - participation.averageMagnitude,
"twTap_advanceWeek::test_WhenLiquidityDidNotDecreasedMoreThanHeDecayActivation: Invalid cumulative"
);
}

function test_WhenLiquidityDidNotDecrease(uint256 _lockAmount, uint256 _lockDuration)
external
whenParticipating(aliceAddr, _lockAmount, _lockDuration)
whenCallerHasRole
whenTimeAdvancedEnough
whenDecayRateIsBiggerThan0
whenEpochBiggerOrEqual2(_lockDuration)
{
// it should not decay
_resetPrank(adminAddr);

(,,, uint256 cumulativeBef) = twTap.twAML();
// We need to skip the decay activation
skip(twTap.EPOCH_DURATION());
twTap.advanceWeek(1);
(,,, uint256 cumulativeAft) = twTap.twAML();

// it should decay
assertEq(
cumulativeBef, cumulativeAft, "twTap_advanceWeek::test_WhenLiquidityDidNotDecrease: Invalid cumulative"
);
}

function _getUnlockEpoch() internal returns (uint256) {
Participation memory participation = twTap.getParticipation(PARTICIPATION_ID);
return _toWeek(participation.expiry);
}
}
Loading