Skip to content

Commit

Permalink
Prevent uninstallation of streaming payments extension with unresolve…
Browse files Browse the repository at this point in the history
…d streams
  • Loading branch information
area committed May 29, 2024
1 parent b444178 commit 9186c91
Show file tree
Hide file tree
Showing 3 changed files with 247 additions and 24 deletions.
119 changes: 102 additions & 17 deletions contracts/extensions/StreamingPayments.sol
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ contract StreamingPayments is ColonyExtensionMeta {

uint256 numStreamingPayments;
mapping(uint256 => StreamingPayment) streamingPayments;
uint256 nUnresolvedStreamingPayments;

// Modifiers

Expand Down Expand Up @@ -134,6 +135,7 @@ contract StreamingPayments is ColonyExtensionMeta {

/// @notice Called when uninstalling the extension
function uninstall() public override auth {
require(nUnresolvedStreamingPayments == 0, "streaming-payments-unresolved-payments");
selfdestruct(payable(address(colony)));
}

Expand Down Expand Up @@ -184,6 +186,10 @@ contract StreamingPayments is ColonyExtensionMeta {
0
);

if (getAmountClaimableLifetime(numStreamingPayments) > 0) {
nUnresolvedStreamingPayments++;
}

emit StreamingPaymentCreated(msgSender(), numStreamingPayments);
}

Expand Down Expand Up @@ -221,6 +227,12 @@ contract StreamingPayments is ColonyExtensionMeta {
return;
}

// If we're not claiming anything, we'll have already returned, so no need to check that
// amountToClaim is >0 here.
if (streamingPayment.pseudoAmountClaimedFromStart >= getAmountClaimableLifetime(_id)) {
nUnresolvedStreamingPayments -= 1;
}

uint256 expenditureId = setupExpenditure(
_permissionDomainId,
_childSkillIndex,
Expand Down Expand Up @@ -266,6 +278,9 @@ contract StreamingPayments is ColonyExtensionMeta {
{
claim(_permissionDomainId, _childSkillIndex, _fromChildSkillIndex, _toChildSkillIndex, _id);
StreamingPayment storage streamingPayment = streamingPayments[_id];

// This require checks that the above claim paid out the full amount the recipient is entitled to
// before any changes are made.
require(
streamingPayment.pseudoAmountClaimedFromStart >= getAmountEntitledFromStart(_id),
"streaming-payments-insufficient-funds"
Expand All @@ -275,6 +290,13 @@ contract StreamingPayments is ColonyExtensionMeta {
// Update 'claimed' as if we've had this rate since the beginning
streamingPayment.pseudoAmountClaimedFromStart = getAmountEntitledFromStart(_id);

// Note that if we're at this point, the payment prior to our editing _must_ have been resolved
// So there's no way we're going from an unresolved payment to a still-unresolved payment and accidentally
// incrementing this when we shouldn't.
if (getAmountClaimableLifetime(_id) >= streamingPayment.pseudoAmountClaimedFromStart) {
nUnresolvedStreamingPayments += 1;
}

emit PaymentTokenUpdated(msgSender(), _id, _amount);
}

Expand All @@ -297,11 +319,26 @@ contract StreamingPayments is ColonyExtensionMeta {
)
{
StreamingPayment storage streamingPayment = streamingPayments[_id];

uint256 oldLifetimeClaimable = getAmountClaimableLifetime(_id);

require(block.timestamp <= streamingPayment.startTime, "streaming-payments-already-started");
require(_startTime <= streamingPayment.endTime, "streaming-payments-invalid-start-time");

streamingPayment.startTime = _startTime;

uint256 newLifetimeClaimable = getAmountClaimableLifetime(_id);

// If current start time is in the future - as is required to be the case by the checks above -
// then pseudoAmountClaimedFromStart is 0. That means we don't need to
// compare lifetimeclaimable amounts to pseudoAmountClaimedFromStart to see if it's an unresolved payment - it's always
// going to be unresolved if the lifetimeclaimable is > 0.
if (oldLifetimeClaimable == 0 && newLifetimeClaimable > 0) {
nUnresolvedStreamingPayments += 1;
} else if (oldLifetimeClaimable > 0 && newLifetimeClaimable == 0) {
nUnresolvedStreamingPayments -= 1;
}

emit StartTimeSet(msgSender(), _id, _startTime);
}

Expand All @@ -328,8 +365,22 @@ contract StreamingPayments is ColonyExtensionMeta {
require(block.timestamp <= _endTime, "streaming-payments-invalid-end-time");
require(streamingPayment.startTime <= _endTime, "streaming-payments-invalid-end-time");

uint256 oldLifetimeClaimable = getAmountClaimableLifetime(_id);

streamingPayment.endTime = _endTime;

uint256 newLifetimeClaimable = getAmountClaimableLifetime(_id);

// Unlike when we're setting start time, we need to check if the payment is unresolved here.
bool wasResolved = streamingPayment.pseudoAmountClaimedFromStart >= oldLifetimeClaimable;
bool isResolved = streamingPayment.pseudoAmountClaimedFromStart >= newLifetimeClaimable;

if (wasResolved && !isResolved) {
nUnresolvedStreamingPayments += 1;
} else if (!wasResolved && isResolved) {
nUnresolvedStreamingPayments -= 1;
}

emit EndTimeSet(msgSender(), _id, _endTime);
}

Expand All @@ -351,25 +402,34 @@ contract StreamingPayments is ColonyExtensionMeta {
{
StreamingPayment storage streamingPayment = streamingPayments[_id];
if (streamingPayment.startTime > block.timestamp) {
streamingPayment.startTime = block.timestamp;
setStartTime(_adminPermissionDomainId, _adminChildSkillIndex, _id, block.timestamp);
}

setEndTime(_adminPermissionDomainId, _adminChildSkillIndex, _id, block.timestamp);
}

/// @notice Cancel the streaming payment, specifically by setting endTime to block.timestamp, and waive claim
/// to specified tokens already earned. Only callable by the recipient.
/// to tokens already earned. Only callable by the recipient.
/// @param _id The id of the streaming payment
function cancelAndWaive(uint256 _id) public {
StreamingPayment storage streamingPayment = streamingPayments[_id];
// slither-disable-next-line incorrect-equality
require(streamingPayment.recipient == msgSender(), "streaming-payments-not-recipient");

uint256 oldLifetimeClaimable = getAmountClaimableLifetime(_id);

if (streamingPayment.startTime > block.timestamp) {
streamingPayment.startTime = block.timestamp;
}

streamingPayment.endTime = min(streamingPayment.endTime, block.timestamp);

// If the old lifetime claimable was more than we've claimed, we've resolved this payment
if (oldLifetimeClaimable > streamingPayment.pseudoAmountClaimedFromStart) {
nUnresolvedStreamingPayments -= 1;
}
// If the newlifetimeclaimable >=0, it doesn't matter, because we're waiving our claim

streamingPayment.pseudoAmountClaimedFromStart = getAmountEntitledFromStart(_id);
emit ClaimWaived(msgSender(), _id);
}
Expand All @@ -391,6 +451,12 @@ contract StreamingPayments is ColonyExtensionMeta {
return numStreamingPayments;
}

/// @notice Get the number of unresolved streaming payments
/// @return nUnresolvedPayments The number of unresolved streaming payments
function getNUnresolvedStreamingPayments() public view returns (uint256 nUnresolvedPayments) {
return nUnresolvedStreamingPayments;
}

/// @notice Get the amount entitled to claim from the start of the stream
/// @param _id The id of the streaming payment
/// @return amount The amount entitled
Expand All @@ -400,13 +466,43 @@ contract StreamingPayments is ColonyExtensionMeta {
return 0;
}

uint256 durationToClaim = min(block.timestamp, streamingPayment.endTime) -
streamingPayment.startTime;
// slither-disable-next-line incorrect-equality
if (durationToClaim == 0) {
uint256 until = min(block.timestamp, streamingPayment.endTime);

return getAmountClaimableInTime(_id, streamingPayment.startTime, until);
}

/// @notice Get the amount claimable in the lifetime of the stream
/// @param _id The id of the streaming payment
/// @return amount The amount claimable
function getAmountClaimableLifetime(uint256 _id) public view returns (uint256 amount) {
StreamingPayment storage streamingPayment = streamingPayments[_id];
return getAmountClaimableInTime(_id, streamingPayment.startTime, streamingPayment.endTime);
}

// Internal

function getAmountClaimable(
uint256 _fundingPotId,
address _token,
uint256 _amountEntitledToClaimNow
) internal view returns (uint256) {
uint256 domainBalance = colony.getFundingPotBalance(_fundingPotId, _token);
return min(domainBalance, _amountEntitledToClaimNow);
}

function getAmountClaimableInTime(
uint256 _id,
uint256 _from,
uint256 _until
) private view returns (uint256) {
StreamingPayment storage streamingPayment = streamingPayments[_id];

if (_from >= _until) {
return 0;
}

uint256 durationToClaim = _until - _from;

// Guard against overflow in wdiv
if (durationToClaim > type(uint256).max / WAD) {
durationToClaim = type(uint256).max / WAD;
Expand All @@ -422,17 +518,6 @@ contract StreamingPayments is ColonyExtensionMeta {
return wmul(streamingPayment.amount, intervalsToClaimAsWad);
}

// Internal

function getAmountClaimable(
uint256 _fundingPotId,
address _token,
uint256 _amountEntitledToClaimNow
) internal view returns (uint256) {
uint256 domainBalance = colony.getFundingPotBalance(_fundingPotId, _token);
return min(domainBalance, _amountEntitledToClaimNow);
}

function setupExpenditure(
uint256 _permissionDomainId,
uint256 _childSkillIndex,
Expand Down
31 changes: 30 additions & 1 deletion docs/interfaces/extensions/streamingpayments.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ Cancel the streaming payment, specifically by setting endTime to block.timestamp

### `cancelAndWaive(uint256 _id)`

Cancel the streaming payment, specifically by setting endTime to block.timestamp, and waive claim to specified tokens already earned. Only callable by the recipient.
Cancel the streaming payment, specifically by setting endTime to block.timestamp, and waive claim to tokens already earned. Only callable by the recipient.


**Parameters**
Expand Down Expand Up @@ -92,6 +92,23 @@ Called when upgrading the extension



### `getAmountClaimableLifetime(uint256 _id):uint256 amount`

Get the amount claimable in the lifetime of the stream


**Parameters**

|Name|Type|Description|
|---|---|---|
|_id|uint256|The id of the streaming payment

**Return Parameters**

|Name|Type|Description|
|---|---|---|
|amount|uint256|The amount claimable

### `getAmountEntitledFromStart(uint256 _id):uint256 amount`

Get the amount entitled to claim from the start of the stream
Expand All @@ -109,6 +126,18 @@ Get the amount entitled to claim from the start of the stream
|---|---|---|
|amount|uint256|The amount entitled

### `getNUnresolvedStreamingPayments():uint256 nUnresolvedPayments`

Get the number of unresolved streaming payments



**Return Parameters**

|Name|Type|Description|
|---|---|---|
|nUnresolvedPayments|uint256|The number of unresolved streaming payments

### `getNumStreamingPayments():uint256 numPayments`

Get the total number of streaming payments
Expand Down
Loading

0 comments on commit 9186c91

Please sign in to comment.