Skip to content

Commit

Permalink
Merge remote-tracking branch 'origin/master' into ETH-623_maxOperator…
Browse files Browse the repository at this point in the history
…s_value

* origin/master:
  build: generate function selector list to selectors.txt (#671)
  ETH-293: Added numeric precision to StakeWeightedAllocationPolicy (#670)
  test: add a longer "scenario" test to Sponsorship (#667)
  StreamrConfig test (#666)
  ETH-617: Voter selection and pseudorandomness cleanup (#665)
  Activate delegationPolicy and undelegationPolicy in _transfer (#658)
  feat: pay for slashing by burning operator's self-delegation (#657)
  Clean up tokenomics contract tests' imports (#664)
  • Loading branch information
samt1803 committed Oct 4, 2023
2 parents 4d02c36 + 042946e commit c8033a6
Show file tree
Hide file tree
Showing 28 changed files with 1,844 additions and 461 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
// SPDX-License-Identifier: MIT

pragma solidity ^0.8.0;

interface IRandomOracle {
function getRandomBytes32() external returns (bytes32);
}
Original file line number Diff line number Diff line change
Expand Up @@ -207,11 +207,28 @@ contract Operator is Initializable, ERC2771ContextUpgradeable, IERC677Receiver,
function _transfer(address from, address to, uint amount) internal override {
// enforce minimum delegation amount, but allow transfering everything (i.e. fully undelegate)
uint minimumDelegationWei = streamrConfig.minimumDelegationWei();
if (balanceOf(to) + amount < minimumDelegationWei ||
(balanceOf(from) < amount + minimumDelegationWei && balanceOf(from) != amount)) {
if ((balanceOf(from) < amount + minimumDelegationWei && balanceOf(from) != amount)
|| balanceOf(to) + amount < minimumDelegationWei) {
revert DelegationBelowMinimum();
}

// transfer creates a new delegator: check if the delegation policy allows this "delegation"
if (balanceOf(to) == 0) {
if (address(delegationPolicy) != address(0)) {
moduleCall(address(delegationPolicy), abi.encodeWithSelector(delegationPolicy.onDelegate.selector, to));
}
}

super._transfer(from, to, amount);

// check if the undelegation policy allows this transfer
// zero reflects that the "undelegation" (transfer) already happened above.
// We can't do a correct check beforehand by passing in the amount because it would have to happen in the middle
// of the transfer "after undelegation but before delegation", so we would actually have to burn then mint. But this works just as well.
if (address(undelegationPolicy) != address(0)) {
moduleCall(address(undelegationPolicy), abi.encodeWithSelector(undelegationPolicy.onUndelegate.selector, from, 0));
}

emit BalanceUpdate(from, balanceOf(from), totalSupply());
emit BalanceUpdate(to, balanceOf(to), totalSupply());
}
Expand Down Expand Up @@ -505,12 +522,24 @@ contract Operator is Initializable, ERC2771ContextUpgradeable, IERC677Receiver,
if (indexOfSponsorships[sponsorship] == 0) {
revert NotMyStakedSponsorship();
}

// operator pays for slashing by undelegating worth slashing
uint amountOperatorTokens = moduleCall(address(exchangeRatePolicy), abi.encodeWithSelector(exchangeRatePolicy.operatorTokenToDataInverse.selector, amountSlashed));
_burn(owner, min(balanceOf(owner), amountOperatorTokens));
emit BalanceUpdate(owner, balanceOf(owner), totalSupply());

// operator value is decreased by the slashed amount => exchange rate doesn't change (unless the operator ran out of tokens)
slashedIn[sponsorship] += amountSlashed;
totalSlashedInSponsorshipsWei += amountSlashed;

emit StakeUpdate(sponsorship, stakedInto[sponsorship] - slashedIn[sponsorship]);
emit OperatorValueUpdate(totalStakedIntoSponsorshipsWei - totalSlashedInSponsorshipsWei, token.balanceOf(address(this)));
}

function min(uint a, uint b) internal pure returns (uint) {
return a < b ? a : b;
}

function onKick(uint, uint receivedPayoutWei) external {
Sponsorship sponsorship = Sponsorship(_msgSender());
if (indexOfSponsorships[sponsorship] == 0) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ contract DefaultDelegationPolicy is IDelegationPolicy, Operator {
* @dev - the first delegation must be self-delegation since at first balance(owner) == 0
* @dev - if minimumSelfDelegationFraction == 0, then any delegations are fine, AS LONG AS the owner has some tokens
* @param delegator The address of the delegator
*/
*/
function onDelegate(address delegator) external {
// owner can always add delegation, even if for some reason the self-delegation requirement is violated (possibly the limit was changed)
if (delegator == owner) { return; }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ import "./SponsorshipPolicies/IKickPolicy.sol";
import "./SponsorshipPolicies/IAllocationPolicy.sol";
import "./StreamrConfig.sol";


/**
* `Sponsorship` ("Stream Agreement") holds the sponsors' tokens and allocates them to operators
* Those tokens are the *sponsorship* that the *sponsor* puts on servicing the stream
Expand Down Expand Up @@ -90,7 +89,7 @@ contract Sponsorship is Initializable, ERC2771ContextUpgradeable, IERC677Receive
uint public minOperatorCount;
uint public minHorizonSeconds;
uint public remainingWei;
uint public earningsWei; // only the IAllocationPolicy should modify this!
uint public earningsWei; // allocated but not withdrawn tokens; only the IAllocationPolicy should modify this!

function getMyStake() public view returns (uint) {
return stakedWei[_msgSender()];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,40 +5,35 @@ pragma solidity ^0.8.13;
import "./IAllocationPolicy.sol";
import "../Sponsorship.sol";

// import "hardhat/console.sol";

// allocation happens over time, so there's necessarily lots of "relying on time" here
/* solhint-disable not-rely-on-time */

/**
* @dev note: ...perStake variables are per FULL TOKEN stake for numerical precision reasons, internally.
* @dev Don't ever expose them outside! We don't want to deal with non-standard "full tokens", e.g. USDC has 6 decimals instead of 18
* @dev Detailed reason: if incomePerSecondPerStake were per stake-wei, then because stake typically is greater than the payout in one second,
* @dev the quotient would always be zero.
* @dev Example: 1 DATA/second, 1000 DATA staked:
* @dev - incomePerSecondPerStake(wei) would be 1e18 / 1000e18 < 1, which becomes zero
* @dev - incomePerSecondPerStake(token) however is 1e18 / 1000 = 1e15, which is fine
* @dev Sanity check: There's order of 1e9 of DATA full tokens in existence, and one year is 3e7 seconds, so the precision is good enough
* @dev for the case where ALL data is staked on a sponsorship that pays 1 DATA/year
*/
contract StakeWeightedAllocationPolicy is IAllocationPolicy, Sponsorship {
struct LocalStorage {
uint incomePerSecond; // wei, total income velocity, distributed to operators, decided by sponsor upon creation
uint cumulativeWeiPerStake; // cumulative income over time, per stake FULL TOKEN unit (wei x 1e18)
uint incomePerSecond; // wei, total income velocity, distributed to operators, decided by sponsor upon creation
uint cumulativeIncomePerStake; // cumulative income/stake over time, multiplied by 1e36 to avoid rounding to zero in calculations

/**
* The per-stake-unit allocation (wei / full token stake) of each operator is the integral over time of incomePerSecond (divided by total stake),
* calculated as cumulativeWeiPerStake (upper limit, common to all operators) minus cumulativeReference (lower limit, just for this operator)
* This reference point will be updated when stake changes because that's when the operator-specific allocation weight changes,
* Each operator's income/stake is the integral over time of incomePerSecond divided by total stake, calculated as
* cumulativeIncomePerStake (upper limit, common to all operators) minus cumulativeReference (lower limit, just for this operator)
* This cumulativeReference will be updated when stake changes because that's when the operator-specific allocation weight changes,
* so we save the result of the integral up to that point and continue integrating from there with the new weight.
*/
mapping(address => uint) cumulativeReference;
/** Remember how much earnings there were before the last cumulativeReference update */
mapping(address => uint) earningsBeforeReferenceUpdate;
/** Remember how much earnings there were before the last cumulativeReference update, multiplied by 1e36 for numerical reasons */
mapping(address => uint) earningsBeforeReferenceUpdateTimes1e36;

// the current unallocated funds will run out if more sponsorship is not added
uint defaultedWei; // lost income during the current insolvency; reported in InsolvencyEnded event, not used in allocations
uint defaultedWeiPerStake; // lost cumulativeWeiPerStake during the current insolvency; reported in InsolvencyEnded event, not used in allocations
/**
* The current unallocated funds will run out if more sponsorship is not added. defaultedWei is the lost income during the current insolvency
* @dev Reported in InsolvencyEnded event, not used in allocations
**/
uint defaultedWei;
/**
* Lost cumulativeIncomePerStake during the current insolvency
* @dev Reported in InsolvencyEnded event, not used in allocations, multiplied by 1e36 for numerical reasons
**/
uint defaultedPerStake;

// calculation inputs in the beginning of the currently running update period
// explicitly stored in the end of last update() because they will be the primary inputs to next update()
Expand Down Expand Up @@ -66,9 +61,12 @@ contract StakeWeightedAllocationPolicy is IAllocationPolicy, Sponsorship {
LocalStorage storage local = localData();
(uint newAllocationsWei,) = calculateSinceLastUpdate();

uint cumulativeWeiPerStake = local.cumulativeWeiPerStake + newAllocationsWei * 1e18 / local.lastUpdateTotalStake;
uint newEarningsPerStakeInFullTokens = cumulativeWeiPerStake - localData().cumulativeReference[operator];
return localData().earningsBeforeReferenceUpdate[operator] + stakedWei[operator] * newEarningsPerStakeInFullTokens / 1e18;
// Round up if the remainder is less than what's possible to intentionally cause with incomePerSecond's precision:
// minimum non-zero value for incomePerSecond is 1 wei/second; 1e25/1e36 = 1/1e11 of 1 wei/second < 1 wei / 300 years
// This fixes rounding errors such as 10/3 * 3 = 9.9999... = 9, now it will give 10 which is correct
uint cumulativeIncomePerStake = local.cumulativeIncomePerStake + newAllocationsWei * 1e36 / local.lastUpdateTotalStake;
uint newEarningsTimes1e36PerStake = cumulativeIncomePerStake - localData().cumulativeReference[operator];
return (localData().earningsBeforeReferenceUpdateTimes1e36[operator] + stakedWei[operator] * newEarningsTimes1e36PerStake + 1e25) / 1e36;
}

/**
Expand Down Expand Up @@ -97,7 +95,7 @@ contract StakeWeightedAllocationPolicy is IAllocationPolicy, Sponsorship {
}

/**
* Update the localData so that all subsequent calculations can use localData().cumulativeWeiPerStake
* Update the localData so that all subsequent calculations can use localData().cumulativeIncomePerStake
* New funds that may have entered in the meanwhile are only counted towards the next update,
* so they appear the have arrived after this update() call.
*/
Expand All @@ -114,14 +112,14 @@ contract StakeWeightedAllocationPolicy is IAllocationPolicy, Sponsorship {
emit InsolvencyStarted(getInsolvencyTimestamp());
}
localVars.defaultedWei += newDefaultsWei;
localVars.defaultedWeiPerStake += newDefaultsWei * 1e18 / localVars.lastUpdateTotalStake;
localVars.defaultedPerStake += newDefaultsWei * 1e36 / localVars.lastUpdateTotalStake;
}

if (newAllocationsWei > 0) {
// move funds from sponsorship to earnings, add to the cumulativeWeiPerStake integral
// move funds from sponsorship to earnings, add to the cumulativeIncomePerStake integral
earningsWei += newAllocationsWei;
remainingWei -= newAllocationsWei;
localVars.cumulativeWeiPerStake += newAllocationsWei * 1e18 / localVars.lastUpdateTotalStake;
localVars.cumulativeIncomePerStake += newAllocationsWei * 1e36 / localVars.lastUpdateTotalStake;
}

// save values for next update: adjust income velocity for a possibly changed number of operators
Expand All @@ -142,13 +140,13 @@ contract StakeWeightedAllocationPolicy is IAllocationPolicy, Sponsorship {
/** When operator joins, the current reference point is reset, and later the operator's allocation can be measured from the accumulated difference */
function onJoin(address operator) external {
update();
localData().cumulativeReference[operator] = localData().cumulativeWeiPerStake;
localData().cumulativeReference[operator] = localData().cumulativeIncomePerStake;
}

/** When operator leaves, its state is cleared as if it had never joined */
function onLeave(address operator) external {
update();
delete localData().earningsBeforeReferenceUpdate[operator];
delete localData().earningsBeforeReferenceUpdateTimes1e36[operator];
delete localData().cumulativeReference[operator];
}

Expand All @@ -162,24 +160,24 @@ contract StakeWeightedAllocationPolicy is IAllocationPolicy, Sponsorship {
// must use pre-increase stake for the past period => undo the stakeChangeWei just for the calculation
uint oldStakeWei = uint(int(stakedWei[operator]) - stakeChangeWei);

// Reference Point Update => move new earnings since last reference update to earningsBeforeReferenceUpdate
uint newEarningsPerStakeInFullTokens = local.cumulativeWeiPerStake - local.cumulativeReference[operator];
local.earningsBeforeReferenceUpdate[operator] += oldStakeWei * newEarningsPerStakeInFullTokens / 1e18;
local.cumulativeReference[operator] = local.cumulativeWeiPerStake; // <- this is the reference update
// Reference Point Update => move (scaled) new earnings since last reference update to earningsBeforeReferenceUpdateTimes1e36
uint newEarningsTimes1e36PerStake = local.cumulativeIncomePerStake - local.cumulativeReference[operator];
local.earningsBeforeReferenceUpdateTimes1e36[operator] += oldStakeWei * newEarningsTimes1e36PerStake;
local.cumulativeReference[operator] = local.cumulativeIncomePerStake; // <- this is the reference update
}

/** @return payoutWei how many tokens to send out from Sponsorship */
function onWithdraw(address operator) external returns (uint payoutWei) {
update();

// calculate payout FIRST, before zeroing earningsBeforeReferenceUpdate
// calculate payout before zeroing earningsBeforeReferenceUpdateTimes1e36, because it's used in the calculation
payoutWei = getEarningsWei(operator);
earningsWei -= payoutWei;

// update reference point, also zero the "unpaid earnings" because they will be paid out
LocalStorage storage local = localData();
local.cumulativeReference[operator] = local.cumulativeWeiPerStake;
local.earningsBeforeReferenceUpdate[operator] = 0;
local.cumulativeReference[operator] = local.cumulativeIncomePerStake;
local.earningsBeforeReferenceUpdateTimes1e36[operator] = 0;
}

function onSponsor(address, uint amount) external {
Expand All @@ -191,8 +189,8 @@ contract StakeWeightedAllocationPolicy is IAllocationPolicy, Sponsorship {
// don't distribute anything yet but start counting again
LocalStorage storage localVars = localData();
if (localVars.defaultedWei > 0) {
emit InsolvencyEnded(block.timestamp, localVars.defaultedWeiPerStake, localVars.defaultedWei);
localVars.defaultedWeiPerStake = 0;
emit InsolvencyEnded(block.timestamp, localVars.defaultedPerStake / 1e18, localVars.defaultedWei);
localVars.defaultedPerStake = 0;
localVars.defaultedWei = 0;
}
emit ProjectedInsolvencyUpdate(getInsolvencyTimestamp());
Expand Down
Loading

0 comments on commit c8033a6

Please sign in to comment.