Skip to content
Merged
Show file tree
Hide file tree
Changes from 15 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
1 change: 1 addition & 0 deletions foundry.toml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ remappings = [
severity = ["high", "med", "low", "info", "gas", "code-size"]
exclude_lints = [
"incorrect-shift",
"mixed-case-function",
"multi-contract-file",
"unsafe-typecast",
"unwrapped-modifier-logic",
Expand Down
128 changes: 128 additions & 0 deletions src/StreamWeightActor.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
// 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 INITIAL = 5e16; // 5%
int256 constant STEP = 5e16; // 5%

contract StreamWeightActor is UnanimousGovernance {
using IsASafe for address;
using OwnersLibrary for address;

IServiceRewardsActor immutable SRA;
Epoch immutable QUARTER;
Epoch immutable HOLD;

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();
}

function registerStream(uint64 id, WeightRecord calldata record, uint64 activationEpoch)
Comment thread
wjmelements marked this conversation as resolved.
external
unanimousNoHold(keccak256(msg.data))
{
// hold enforced in f02
FVMRewards.registerStream(id, record, activationEpoch);
}

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);
}

function removeStream(uint64 id) external unanimousNoHold(keccak256(msg.data)) {
// hold enforced in f02
FVMRewards.removeStream(id);
}

function setWeightRecords(WeightRecordUpdate[] calldata updates) external unanimousNoHold(keccak256(msg.data)) {
// hold enforced in f02
FVMRewards.setWeightRecords(updates);
}

function setDistribution(uint64 id, address writer) external unanimousNoHold(keccak256(msg.data)) {
// hold enforced in f02
FVMRewards.setDistribution(id, writer);
}

function cancelPending(uint64 id, PendingOp op) external {
// any owner can immediately cancel any pending operation
require(msg.sender.isOwner());
FVMRewards.cancelPending(id, op);
}

function cancelPendingWeight(PendingOp op) external {
// any owner can immediately cancel any pending operation
require(msg.sender.isOwner());
FVMRewards.cancelPendingWeight(op);
}

function replaceOwner(address prevOwner, address newOwner) external unanimousNoHold(keccak256(msg.data)) {
newOwner.isProbablyASafe();
prevOwner.removeOwner();
newOwner.addOwner();
}

error StepsComplete();

function quarterlyGateCheck() external {
GateParamsLibrary.GateParamsInfo storage gateParamsInfo = GateParamsLibrary.getGateParamsSlot();
GateParams memory loaded = gateParamsInfo.params;
require(loaded.steps < 8, StepsComplete());

uint64 quarter = loaded.lastCheckedQuarter + 1;
// NOTE this will enforce afterBinding()
Comment thread
wjmelements marked this conversation as resolved.
FixedU18 fpv = SRA.aggregatedFPV(quarter);

if (fpv > loaded.nextThreshold()) {
Comment thread
wjmelements marked this conversation as resolved.
Outdated
int256 start = (int256(uint256(loaded.steps)) + 2) * STEP;

WeightRecordUpdate[] memory updates = new WeightRecordUpdate[](1);
updates[0].id = SERVICE_ID;
updates[0].record.floor = INITIAL;
updates[0].record.tStart = SRA.qEnd(quarter);
updates[0].record.vStart = start;
updates[0].record.cap = start + STEP;
updates[0].record.slope =
(updates[0].record.cap - updates[0].record.vStart) / int256(uint256(Epoch.unwrap(QUARTER)));
Comment thread
wjmelements marked this conversation as resolved.
Outdated

loaded.steps++;
loaded.lastCheckedQuarter = quarter;
gateParamsInfo.params = loaded;
FVMRewards.stepWeightRecords(updates);
} else {
gateParamsInfo.params.lastCheckedQuarter = quarter;
}
}

function setGateParams(GateParams calldata params) external unanimous(keccak256(msg.data), HOLD) {
GateParamsLibrary.getGateParamsSlot().params = params;
}
}
12 changes: 12 additions & 0 deletions src/interfaces/IServiceRewardsActor.sol
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);
}
2 changes: 1 addition & 1 deletion src/lib/Epoch.sol
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// SPDX-License-Identifier: Apache-2.0 OR MIT
pragma solidity ^0.8.36;

type Epoch is uint96;
type Epoch is uint64;

using {
add as +,
Expand Down
4 changes: 3 additions & 1 deletion src/lib/FVMRewardTypes.sol
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
// SPDX-License-Identifier: Apache-2.0 OR MIT
pragma solidity ^0.8.36;

import {Epoch} from "./Epoch.sol";

/// @notice A stream's Distribution kind (FIP-0118 Section 2.4).
/// @dev IMPLICIT streams store no writer (f02 resolves the recipient from protocol state).
/// EXPLICIT streams are paid out per a wallet-to-share map written by their designated writer.
Expand All @@ -14,7 +16,7 @@ enum DistributionKind {
struct WeightRecord {
int256 vStart;
int256 slope;
uint64 tStart;
Epoch tStart;
int256 floor;
int256 cap;
}
Expand Down
3 changes: 2 additions & 1 deletion src/lib/FVMRewards.sol
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {NO_FLAGS} from "fvm-solidity/FVMFlags.sol";
import {CBOR_CODEC} from "fvm-solidity/FVMCodec.sol";
import {EXIT_SUCCESS} from "fvm-solidity/FVMErrors.sol";

import {Epoch} from "./Epoch.sol";
import {
REGISTER_STREAM,
REMOVE_STREAM,
Expand Down Expand Up @@ -185,7 +186,7 @@ library FVMRewards {
p = _writeArrayHeader(p, 5);
p = _writeUint(p, _u64(r.vStart));
p = _writeInt(p, r.slope);
p = _writeInt(p, int256(uint256(r.tStart)));
p = _writeInt(p, int256(uint256(Epoch.unwrap(r.tStart))));
p = _writeUint(p, _u64(r.floor));
p = _writeUint(p, _u64(r.cap));
return p;
Expand Down
106 changes: 106 additions & 0 deletions src/lib/FixedU18.sol
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)
Comment thread
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)
}
}
}
}
48 changes: 48 additions & 0 deletions src/lib/GateParams.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
// 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;
uint64 lastCheckedQuarter;
Comment thread
wjmelements marked this conversation as resolved.
Outdated
}

using GateParamsLibrary for GateParams global;

library GateParamsLibrary {
/// @custom:storage-location erc7201:Solstice.GateParams
struct GateParamsInfo {
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);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

needs a floor here

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We'll just remove the floor from the spec.

}

function init() internal {
GateParams memory params;
params.target.base = VOL_TARGET_ENTRY;
params.target.stepRatio = VOL_TARGET_RATIO;
params.lastCheckedQuarter = 1;
GateParamsLibrary.getGateParamsSlot().params = params;
}
}
4 changes: 2 additions & 2 deletions src/lib/IsASafe.sol
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ library IsASafe {

function isProbablyASafe(address account) internal view {
uint256 codesize = account.code.length;
// observed Safe proxy codesize range is 110 (v1.0.0) to 171 (v1.3.0)
require(codesize > 80 && codesize < 240, NotSafeProxy(account));
// observed Safe proxy codesize range is 60 (stripped v1.5.0) to 171 (v1.3.0)
require(codesize > 56 && codesize < 240, NotSafeProxy(account));
address implementation = IProxy(account).masterCopy();
// observed Safe masterCopy size is 20869 (v1.5.0) to 24421 (v1.4.1)
require(implementation.code.length > 8000, UnusualSafeMasterCopy(account, implementation));
Expand Down
28 changes: 28 additions & 0 deletions src/lib/UnanimousGovernance.sol
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,34 @@ contract UnanimousGovernance {
}
}

modifier unanimousNoHold(bytes32 taskId) {
Comment thread
wjmelements marked this conversation as resolved.
PendingTaskInfo storage taskInfo = PendingTaskLibrary.getTasksSlot()[taskId];
PendingTask memory loaded = taskInfo.task;
OwnerSet allOwners = OwnersLibrary.getAllOwners();

// approve
require(msg.sender.isOwner(), NotOwner(msg.sender));
OwnerSet ownerBit = msg.sender.asOwnerSet();
if (loaded.modified == UNSUBMITTED) {
emit Submitted(taskId);
} else {
require(loaded.approvals & ownerBit == EMPTY_SET, AlreadyApproved());
}
loaded.modified = currentEpoch();
loaded.approvals = loaded.approvals | ownerBit;

// store result
emit Approved(taskId, msg.sender);
if (loaded.approvals & allOwners == allOwners) {
delete taskInfo.task;
// execute now
_;
} else {
// wait
taskInfo.task = loaded;
}
}

/// @param taskId The identifier of the pending task to reject
function _veto(bytes32 taskId) internal {
// load
Expand Down
Loading