-
Notifications
You must be signed in to change notification settings - Fork 2
feat: StreamWeightActor #17
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
Merged
Merged
Changes from all commits
Commits
Show all changes
19 commits
Select commit
Hold shift + click to select a range
133d576
feat: add StreamWeightActor with unanimousNoHold governance and tests
wjmelements c9c4809
fix: support metadata-stripped SafeProxy
wjmelements b29d163
Merge remote-tracking branch 'origin/main' into swa
wjmelements 8c3eff9
fix: change Epoch to uint64
wjmelements 6be758b
feat: GateParams, quarterlyGateCheck
wjmelements 4fbacf5
feat: use SRA.qEnd
wjmelements 16413bf
chore: forge fmt
wjmelements 6b2e4a2
feat: GateParamsLibrary.init
wjmelements d4ea2d7
test: FixedU18exp
wjmelements 284f8fb
chore: forge fmt
wjmelements e577a00
perf: simplify exp
wjmelements d8ea8ee
perf: assembly FixedU18.exp
wjmelements b12f80f
fix(swa): skip StepWeightRecords write when the gate fails
wjmelements b95a375
refactor(swa): read QUARTER and HOLD from the SRA
wjmelements b5050a8
perf(swa): skip the unused final squaring in FixedU18.exp
wjmelements 7dd29d3
docs(swa): add natspec to public methods
wjmelements b4596d7
fix: measured >= threshold
wjmelements 78e0e26
fix: step not slope
wjmelements 8f52244
fix: remove lastCheckedQuarter from GateParams
wjmelements File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,164 @@ | ||
| // SPDX-License-Identifier: Apache-2.0 OR MIT | ||
| pragma solidity ^0.8.36; | ||
|
|
||
| import {IServiceRewardsActor} from "./interfaces/IServiceRewardsActor.sol"; | ||
| import {Epoch} from "./lib/Epoch.sol"; | ||
| import {FixedU18} from "./lib/FixedU18.sol"; | ||
| import {GateParams, GateParamsLibrary} from "./lib/GateParams.sol"; | ||
| import {FVMRewards} from "./lib/FVMRewards.sol"; | ||
| import {PendingOp, Share, WeightRecord, WeightRecordUpdate} from "./lib/FVMRewardTypes.sol"; | ||
| import {OwnersLibrary} from "./lib/Owners.sol"; | ||
| import {UnanimousGovernance} from "./lib/UnanimousGovernance.sol"; | ||
| import {IsASafe} from "./lib/IsASafe.sol"; | ||
|
|
||
| uint64 constant SERVICE_ID = 2; | ||
|
|
||
| int256 constant STEP = 5e16; // 5% | ||
|
|
||
| /// @notice Owner-governed actor with sudo control over every f02 stream's weight schedule | ||
| /// (FIP-0118, solstice#3): registers, removes, reweights, and reassigns writers by stream id. | ||
| /// @dev Writes require unanimous owner approval, except `cancelPending`/`cancelPendingWeight` | ||
| /// (any single owner, immediate) and `quarterlyGateCheck` (fully permissionless). | ||
| contract StreamWeightActor is UnanimousGovernance { | ||
| using IsASafe for address; | ||
| using OwnersLibrary for address; | ||
|
|
||
| IServiceRewardsActor immutable SRA; | ||
| Epoch immutable QUARTER; | ||
| Epoch immutable HOLD; | ||
|
|
||
| /// @notice Deploys the actor with its two initial owners, bound to a Service Rewards Actor. | ||
| /// @param owner1 First owner; must be a Safe. | ||
| /// @param owner2 Second owner; must be a Safe. | ||
| /// @param sra Service Rewards Actor supplying QUARTER/HOLD and gating `quarterlyGateCheck`. | ||
| constructor(address owner1, address owner2, IServiceRewardsActor sra) { | ||
| owner1.isProbablyASafe(); | ||
| owner2.isProbablyASafe(); | ||
|
|
||
| owner1.addOwner(); | ||
| owner2.addOwner(); | ||
|
|
||
| SRA = sra; | ||
| QUARTER = sra.EPOCHS_PER_QUARTER(); | ||
| HOLD = sra.SRA_CANCEL_HOLD(); | ||
|
|
||
| GateParamsLibrary.init(); | ||
| } | ||
|
|
||
| /// @notice Queues a new implicit stream. | ||
| /// @dev f02 resolves the recipient from protocol state; no writer or share map is stored. | ||
| /// @param id Stream id to register. | ||
| /// @param record Initial weight schedule. | ||
| /// @param activationEpoch Epoch the schedule begins applying. | ||
| function registerStream(uint64 id, WeightRecord calldata record, uint64 activationEpoch) | ||
| external | ||
| unanimousNoHold(keccak256(msg.data)) | ||
| { | ||
| // hold enforced in f02 | ||
| FVMRewards.registerStream(id, record, activationEpoch); | ||
| } | ||
|
|
||
| /// @notice Queues a new explicit stream with its designated writer and initial share map. | ||
| /// @param id Stream id to register. | ||
| /// @param record Initial weight schedule. | ||
| /// @param writer Address permitted to update the share map. | ||
| /// @param shares Initial wallet-to-share map; must be valid at registration. | ||
| /// @param activationEpoch Epoch the schedule begins applying. | ||
| function registerStream( | ||
| uint64 id, | ||
| WeightRecord calldata record, | ||
| address writer, | ||
| Share[] calldata shares, | ||
| uint64 activationEpoch | ||
| ) external unanimousNoHold(keccak256(msg.data)) { | ||
| // hold enforced in f02 | ||
| FVMRewards.registerStream(id, record, writer, shares, activationEpoch); | ||
| } | ||
|
|
||
| /// @notice Queues removal of a stream. | ||
| /// @param id Stream id to remove. | ||
| function removeStream(uint64 id) external unanimousNoHold(keccak256(msg.data)) { | ||
| // hold enforced in f02 | ||
| FVMRewards.removeStream(id); | ||
| } | ||
|
|
||
| /// @notice Queues a discretionary weight-schedule write for one or more streams. | ||
| /// @param updates Id/record pairs to write. | ||
| function setWeightRecords(WeightRecordUpdate[] calldata updates) external unanimousNoHold(keccak256(msg.data)) { | ||
| // hold enforced in f02 | ||
| FVMRewards.setWeightRecords(updates); | ||
| } | ||
|
|
||
| /// @notice Queues a writer change for an explicit stream. | ||
| /// @param id Stream id whose writer changes. | ||
| /// @param writer New designated writer. | ||
| function setDistribution(uint64 id, address writer) external unanimousNoHold(keccak256(msg.data)) { | ||
| // hold enforced in f02 | ||
| FVMRewards.setDistribution(id, writer); | ||
| } | ||
|
|
||
| /// @notice Cancels a queued per-stream operation (register, remove, or setDistribution). | ||
| /// @dev Any current owner, immediate, bypassing unanimity. | ||
| /// @param id Stream id the pending operation targets. | ||
| /// @param op Kind of pending operation to cancel. | ||
| function cancelPending(uint64 id, PendingOp op) external { | ||
| // any owner can immediately cancel any pending operation | ||
| require(msg.sender.isOwner()); | ||
| FVMRewards.cancelPending(id, op); | ||
| } | ||
|
|
||
| /// @notice Cancels a queued discretionary weight-schedule write (SetWeightRecords). | ||
| /// @dev Any current owner, immediate. Cannot cancel a gate-originated StepWeightRecords write. | ||
| /// @param op Weight operation to cancel. | ||
| function cancelPendingWeight(PendingOp op) external { | ||
| // any owner can immediately cancel any pending operation | ||
| require(msg.sender.isOwner()); | ||
| FVMRewards.cancelPendingWeight(op); | ||
| } | ||
|
|
||
| /// @notice Replaces one of the two owners. | ||
| /// @param prevOwner Owner being removed. | ||
| /// @param newOwner Owner being added; must be a Safe. | ||
| function replaceOwner(address prevOwner, address newOwner) external unanimousNoHold(keccak256(msg.data)) { | ||
| newOwner.isProbablyASafe(); | ||
| prevOwner.removeOwner(); | ||
| newOwner.addOwner(); | ||
| } | ||
|
|
||
| /// @notice All 8 gate steps have already been taken. | ||
| error StepsComplete(); | ||
|
|
||
| /// @notice Advances the quarterly gate by one quarter, stepping SERVICE_ID's weight schedule | ||
| /// if the elapsed quarter's aggregated FPV cleared the next volume threshold. | ||
| /// @dev Permissionless; reverts via the SRA if the quarter's FPV is not yet bound. | ||
| function quarterlyGateCheck() external { | ||
| GateParamsLibrary.GateParamsInfo storage gateParamsInfo = GateParamsLibrary.getGateParamsSlot(); | ||
| GateParams memory loaded = gateParamsInfo.params; | ||
| require(loaded.steps < 8, StepsComplete()); | ||
|
|
||
| uint64 quarter = ++gateParamsInfo.lastCheckedQuarter; | ||
| // NOTE this will enforce afterBinding() | ||
|
wjmelements marked this conversation as resolved.
|
||
| FixedU18 fpv = SRA.aggregatedFPV(quarter); | ||
|
|
||
| if (fpv >= loaded.nextThreshold()) { | ||
| int256 next = (int256(uint256(loaded.steps)) + 3) * STEP; | ||
|
|
||
| WeightRecordUpdate[] memory updates = new WeightRecordUpdate[](1); | ||
| updates[0].id = SERVICE_ID; | ||
| updates[0].record.floor = next; | ||
| updates[0].record.tStart = SRA.qEnd(quarter); | ||
| updates[0].record.vStart = next; | ||
| updates[0].record.cap = next; | ||
| updates[0].record.slope = 0; | ||
|
|
||
| gateParamsInfo.params.steps++; | ||
| FVMRewards.stepWeightRecords(updates); | ||
| } | ||
| } | ||
|
|
||
| /// @notice Overwrites the quarterly gate's parameters; has a HOLD-epoch timelock after unanimity. | ||
| /// @param params New volume target and step state. | ||
| function setGateParams(GateParams calldata params) external unanimous(keccak256(msg.data), HOLD) { | ||
| GateParamsLibrary.getGateParamsSlot().params = params; | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,12 @@ | ||
| // SPDX-License-Identifier: Apache-2.0 OR MIT | ||
| pragma solidity ^0.8.36; | ||
|
|
||
| import {Epoch} from "../lib/Epoch.sol"; | ||
| import {FixedU18} from "../lib/FixedU18.sol"; | ||
|
|
||
| interface IServiceRewardsActor { | ||
| function aggregatedFPV(uint64 quarter) external view returns (FixedU18 filecoinPayVolume); | ||
| function qEnd(uint64 quarter) external view returns (Epoch quarterEnd); | ||
| function EPOCHS_PER_QUARTER() external view returns (Epoch oneQuarter); | ||
| function SRA_CANCEL_HOLD() external view returns (Epoch hold); | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,106 @@ | ||
| // SPDX-License-Identifier: Apache-2.0 OR MIT | ||
| pragma solidity ^0.8.36; | ||
|
|
||
| type FixedU18 is uint256; | ||
|
|
||
| using { | ||
| unsafeAdd as +, | ||
| unsafeSub as -, | ||
| unsafeMulDown as *, | ||
| divDown as /, | ||
| equals as ==, | ||
| greaterThan as >, | ||
| lessThan as <, | ||
| greaterThanOrEqualTo as >=, | ||
| lessThanOrEqualTo as <= | ||
| } for FixedU18 global; | ||
|
|
||
| using FixedU18Library for FixedU18 global; | ||
|
|
||
| uint256 constant ONE_WAD = 1 ether; | ||
| FixedU18 constant ONE = FixedU18.wrap(ONE_WAD); | ||
|
|
||
| // type(uint256).max / ONE_WAD | ||
| uint256 constant MAX_DIVIDEND_WAD = 115792089237316195423570985008687907853269984665640564039457; | ||
| FixedU18 constant MAX_DIVIDEND = FixedU18.wrap(MAX_DIVIDEND_WAD); | ||
|
|
||
| error DividendTooLarge(FixedU18 dividend); | ||
| bytes4 constant DIVIDEND_TOO_LARGE_SELECTOR = 0x7b5479ff; | ||
|
|
||
| // @dev returns zero if divisor is zero | ||
| function divDown(FixedU18 dividend, FixedU18 divisor) pure returns (FixedU18 quotient) { | ||
| assembly ("memory-safe") { | ||
| if gt(dividend, MAX_DIVIDEND_WAD) { | ||
| mstore(0, DIVIDEND_TOO_LARGE_SELECTOR) | ||
| mstore(32, dividend) | ||
| revert(28, 36) | ||
| } | ||
| quotient := div(mul(dividend, ONE_WAD), divisor) | ||
| } | ||
| } | ||
|
|
||
| // @dev overflows if sum would be greater than 2**256/10**18 (approximatly 10**59) | ||
|
wjmelements marked this conversation as resolved.
|
||
| function unsafeAdd(FixedU18 addend1, FixedU18 addend2) pure returns (FixedU18 sum) { | ||
| assembly ("memory-safe") { | ||
| sum := add(addend1, addend2) | ||
| } | ||
| } | ||
|
|
||
| // @dev underflows if subtrahend is greater than minuend | ||
| function unsafeSub(FixedU18 minuend, FixedU18 subtrahend) pure returns (FixedU18 difference) { | ||
| assembly ("memory-safe") { | ||
| difference := sub(minuend, subtrahend) | ||
| } | ||
| } | ||
|
|
||
| // @dev overflows if product would be greater than 2**256/10**18 (approximatly 10**59) | ||
| function unsafeMulDown(FixedU18 factor1, FixedU18 factor2) pure returns (FixedU18 product) { | ||
| assembly ("memory-safe") { | ||
| product := div(mul(factor1, factor2), ONE_WAD) | ||
| } | ||
| } | ||
|
|
||
| function equals(FixedU18 a, FixedU18 b) pure returns (bool) { | ||
| return FixedU18.unwrap(a) == FixedU18.unwrap(b); | ||
| } | ||
|
|
||
| function greaterThan(FixedU18 a, FixedU18 b) pure returns (bool) { | ||
| return FixedU18.unwrap(a) > FixedU18.unwrap(b); | ||
| } | ||
|
|
||
| function lessThan(FixedU18 a, FixedU18 b) pure returns (bool) { | ||
| return FixedU18.unwrap(a) < FixedU18.unwrap(b); | ||
| } | ||
|
|
||
| function greaterThanOrEqualTo(FixedU18 a, FixedU18 b) pure returns (bool) { | ||
| return FixedU18.unwrap(a) >= FixedU18.unwrap(b); | ||
| } | ||
|
|
||
| function lessThanOrEqualTo(FixedU18 a, FixedU18 b) pure returns (bool) { | ||
| return FixedU18.unwrap(a) <= FixedU18.unwrap(b); | ||
| } | ||
|
|
||
| library FixedU18Library { | ||
| // @dev overflows if product would be greater than 2**256/10**18 (approximatly 10**59) | ||
| function mul(FixedU18 factor1, uint256 factor2) internal pure returns (FixedU18 product) { | ||
| assembly ("memory-safe") { | ||
| product := mul(factor1, factor2) | ||
| } | ||
| } | ||
|
|
||
| function exp(FixedU18 base, uint64 exponent) internal pure returns (FixedU18 power) { | ||
| assembly ("memory-safe") { | ||
| power := ONE_WAD | ||
| if exponent { | ||
| for {} gt(exponent, 1) {} { | ||
| if and(1, exponent) { | ||
| power := div(mul(base, power), ONE_WAD) | ||
| } | ||
| base := div(mul(base, base), ONE_WAD) | ||
| exponent := shr(1, exponent) | ||
| } | ||
| power := div(mul(base, power), ONE_WAD) | ||
| } | ||
| } | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,47 @@ | ||
| // SPDX-License-Identifier: Apache-2.0 OR MIT | ||
| pragma solidity ^0.8.36; | ||
|
|
||
| import {FixedU18} from "./FixedU18.sol"; | ||
|
|
||
| FixedU18 constant VOL_TARGET_ENTRY = FixedU18.wrap(3500 ether); | ||
| FixedU18 constant VOL_TARGET_RATIO = FixedU18.wrap(2.7 ether); | ||
|
|
||
| struct VolumeTarget { | ||
| FixedU18 base; | ||
| FixedU18 stepRatio; | ||
| } | ||
|
|
||
| struct GateParams { | ||
| VolumeTarget target; | ||
| uint64 steps; | ||
| } | ||
|
|
||
| using GateParamsLibrary for GateParams global; | ||
|
|
||
| library GateParamsLibrary { | ||
| /// @custom:storage-location erc7201:Solstice.GateParams | ||
| struct GateParamsInfo { | ||
| uint64 lastCheckedQuarter; | ||
| GateParams params; | ||
| } | ||
|
|
||
| // keccak256(abi.encode(uint256(keccak256("Solstice.GateParams")) - 1)) & ~bytes32(uint256(0xff)); | ||
| bytes32 private constant GATE_PARAMS_SLOT = 0xf9abab00248d945495524c8caf6be2b837274c1becd1964fb3775f62fd6e4600; | ||
|
|
||
| function getGateParamsSlot() internal pure returns (GateParamsInfo storage slot) { | ||
| assembly ("memory-safe") { | ||
| slot.slot := GATE_PARAMS_SLOT | ||
| } | ||
| } | ||
|
|
||
| function nextThreshold(GateParams memory params) internal pure returns (FixedU18 fpvThreshold) { | ||
| return params.target.base * params.target.stepRatio.exp(params.steps); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. needs a floor here
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We'll just remove the floor from the spec. |
||
| } | ||
|
|
||
| function init() internal { | ||
| GateParamsInfo storage slot = GateParamsLibrary.getGateParamsSlot(); | ||
| slot.lastCheckedQuarter = 1; | ||
| slot.params.target.base = VOL_TARGET_ENTRY; | ||
| slot.params.target.stepRatio = VOL_TARGET_RATIO; | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.