-
Notifications
You must be signed in to change notification settings - Fork 0
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
Payments contract first draft #1
base: main
Are you sure you want to change the base?
Conversation
src/Payments.sol
Outdated
payer.lockupRate = payer.lockupRate - oldRate + rate; | ||
|
||
// Init hasLockupStarted flag and lockupStart for the first rail with non-zero rate | ||
if (rate > 0 && !payer.hasLockupStarted) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Take a look at the "Lockup can be settled..." part of: https://www.notion.so/filecoindev/Payments-Design-Iteration-17ddc41950c180468806c7e0097c2e14?pvs=4#185dc41950c1805da8d4ce567e343307
lockupStart
is probably misnamed. Really, it means "the last time we settled lockup". I.e., it's when the lockup rate starts applying.
My point is, I don't think we need hasLockupStarted
. We always adjust lockupStart
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, I see you've implemented accumulateLockupRate
. You don't need this code here, you just need to call accumulateLockupRate
before changing the rate (e.g., at the very top of this function). The first time this is called, that will accumulate nothing because the rate will be 0, but it will initialize lockupStart
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@Stebalien Yeah this makes sense. If totalLockupRate
is 0, the lockupstart
does not matter.
I'm fixing naming across this contract now for all state. I don't think these names accurately reflect what they're doing. lockupBase
for eg dosen't reflect that it's doing three things:
- Locking up a fixed security deposit
- Locking up accrued payments for services already taken by the client but not yet settled by the provider
- Locking up a portion of payments for future services that have yet to be provided to provide some sort of "guarantee" to the provider.
src/Payments.sol
Outdated
// 1. Fixed lockups from all rails associated with this account. | ||
// 2. Accumulated rate-based lockups that have been converted to fixed lockups across all rails. | ||
// 3. Funds reserved for future payments based on current payment rates and lockup periods across all rails. | ||
uint256 requiredTotalLockedFunds; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've updated the design doc. I've named this "lockup current" because it covers funds that are actually locked, not the current target locked. I didn't use the word required because the actually locked funds may be less than the "target" (required).
uint256 private _nextRailId; | ||
|
||
// token => owner => Account | ||
mapping(address => mapping(address => Account)) public accounts; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We're going to need two features WRT account management:
- A way to efficiently (ish) determine the status of an account. Total funds across all tokens, etc. so we can display it in some webui.
- Ideally a way to efficiently change the owner address? That's complicated.
Even if we don't support (2), we'll need some top-level account abstraction to support (1).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can't we implement those getters as view functions that can be invoked off-chain on RPC providers ? We can add some state to the contract that makes those view/getter functions slightly less compute intensive but IMO making it a view function that can be invoked off-chain means we can offload the worrying about efficiency to client <> RPC provider relationships without having to add too much state to the contract.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok, we can solve (1) by:
- Adding a
mapping(address => address[])
of account owners to tokens. - Optionally: changing
token => owner => Account
toowner => token => Account
. This isn't strictly necessary, but it mirrors the mapping in (1).
As discussed, we can drop (2) and let the user handle indirection through smart contracts.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actually, maybe we want a single owner => Account
mapping where Account
contains:
- The client's operator approvals (map of
operator => Approval
). - An array of approved client operators.
- An array of client rail IDs.
- etc...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Resolution: we're still exploring the API, let's build that first then figure out what these data structures should look at.
function approveOperator( | ||
address token, | ||
address operator, | ||
address arbiter, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Are you sure we want to approve a specific arbiter? I can see the appeal but, in that case... I think we can just let the client decide if they're ok with an arbiter on a per-rail basis.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@Stebalien You mean the client creates the rail OR that a client has to approve a rail that has been created by an operator ? Both have different UX implications.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
See my comment at https://github.com/FilOzone/payments/pull/1/files#r1961082248.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
An operator can have many different arbitrers for different rails.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So remove this.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The answer here is to trust the operator.
|
||
Account storage toAccount = accounts[token][to]; | ||
require(toAccount.ownerAddress != address(0), "to account does not exist"); | ||
require(toAccount.funds > 0, "to account has no funds"); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
They don't need funds, do they?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@Stebalien I mean not strictly speaking. We can get rid of this yeah.
|
||
Account storage fromAccount = accounts[token][from]; | ||
require(fromAccount.ownerAddress != address(0), "from account does not exist"); | ||
require(fromAccount.funds > 0, "from account has no funds"); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Technically, the sender doesn't need any funds either.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@Stebalien Technically no but I'd like to keep this so that rails are only created with clients who have interacted with the contract before. Just think it's better that way.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Well, this goes back to who can create a rail. IMO, the answer is: the client.
It would be nice if a client can go through the entire signup flow without having to deposit any funds until they actually start using the service.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We agree that neither from
or to
needs funds.
if (approval.arbitrer != address(0)) { | ||
require(arbiter == approval.arbitrer, "arbiter mismatch"); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
TBD: need to discuss approved operators and who, precisely, is allowed to call createRail
.
|
||
uint256 railId = _nextRailId++; | ||
|
||
Rail storage rail = rails[railId]; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
OT: Ethereum's "everything exists, it just defaults to zero" gets me every time. It's kind of cool but also really disconcerting.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@Stebalien What's "OT" ?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
"Off topic". I.e., "nothing to do with my review, ignore this, I just wanted to say it."
!/broadcast | ||
/broadcast/*/31337/ | ||
/broadcast/**/dry-run/ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why not just ignore all of /broadcast?
src/Payments.sol
Outdated
struct ArbitrationResult { | ||
bool approved; | ||
uint256 modifiedAmount; | ||
uint256 settledUntil; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We should consider adding an arbitrary "note" field. Could be useful to explain why the full amount isn't approved.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added this and also now returning this to the caller of settleRail
|
||
interface IArbiter { | ||
struct ArbitrationResult { | ||
bool approved; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we need approved
? IMO, we should just revert if we're not approving anything, but I'm not really a solidity expert.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@Stebalien This is for the Arbitrer contract to gracefully let us know that it is/is not rejecting the payment.
The Payment contract does revert the settlement if it sees a disapproval from the Arbitrer but doing it this way lets us decide what we wanna do when we see a rejection from the Arbitrer.
We can change this so that the Arbitrer reverts rather than returning this approved
flag and then handle the revert in the Payment contract as follows:
// Low-level call with error handling
(bool success, bytes memory returnData) = IArbitrer(Y).call(....);
if (!success) {
// Handle the error
}
but we need to write some additional encoding/decoding code here for serialisation.
No opinion either way. Wdyt ?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The other option is to not gracefully handle any reverts from the Arbitrer
contract at all and simply revert right away (in Solidity, when contract X calls contract Y and contract Y reverts -> the call to X reverts right away unless you handle it as above).
The way we've implemented it here -> any revert from Arbitrer
will bubble up right away and the settlement will revert but atleast for payment refusals -> we get to handle it gracefully and do more stuff if we want to before we revert.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The Arbitrer MUST revert if it wants to reject payment.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
if arbitrer wants to reject and NOT revert, you can just set settleUpto
to the start epoch.
src/Payments.sol
Outdated
uint256 paymentRate; | ||
uint256 lockupPeriod; | ||
uint256 lockupFixed; | ||
uint256 lastSettledAt; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hm. Now that I see this, I wonder if we should call this "settledUpTo" instead of "lastSettledAt".
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also renamed the corresponding ArbitrationResult
field to settledUpTo
} | ||
|
||
struct OperatorApproval { | ||
bool isApproved; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we need this field?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@Stebalien Yeah it's the easiest way to track the fact that an operator has been disapproved. Solidity HashMap/Array deletes aren't clean/gas cheap to do.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also, if operators create rails, it makes sense to:
- Approve the operator with no allowance.
- Let the operator create rails.
- Then give them allowance.
(IMO)
|
||
struct OperatorApproval { | ||
bool isApproved; | ||
address arbitrer; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we need this field either? Isn't it implicit?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@Stebalien My idea was to have the client have a say in who gets to be an arbitrer.
src/Payments.sol
Outdated
/// @notice Returns the minimum of two numbers. | ||
function min(uint256 a, uint256 b) internal pure returns (uint256) { | ||
return a < b ? a : b; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Make this a free function?
src/Payments.sol
Outdated
} | ||
function _settleWithRateChanges( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
} | |
function _settleWithRateChanges( | |
} | |
function _settleWithRateChanges( |
src/Payments.sol
Outdated
uint256 oldLockup = rail.lockupFixed + (rail.paymentRate * rail.lockupPeriod); | ||
uint256 newLockup = lockupFixed + (rail.paymentRate * period); | ||
|
||
// assert that operator allownace is respected |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
// assert that operator allownace is respected | |
// assert that operator allowance is respected |
src/Payments.sol
Outdated
segmentStart, | ||
segmentEnd | ||
); | ||
require(arbResult.approved, "arbitrer refused payment"); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We need to validate that:
arbResult.settledUntil <= settledUntil
.arbResult.modifiedAmount <= rate * (arbResult.settledUntil - segmentStart)
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For 1, it should be arbResult.settledUntil <=segmentEnd
, right ? Yeah making this change.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done.
src/Payments.sol
Outdated
|
||
settleAccountLockup(payer); | ||
|
||
uint256 settlementTargetEpoch = min(untilEpoch, payer.lockupLastSettledAt + rail.lockupPeriod); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Need to check that untilEpoch
is at or before the current epoch.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done.
src/Payments.sol
Outdated
if (nextChange.untilEpoch > currentEpoch && nextChange.untilEpoch <= targetEpoch) { | ||
nextBoundary = nextChange.untilEpoch; | ||
segmentRate = nextChange.rate; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've spent a lot of time staring at this code and I'm still not sure if it's right. Let's talk through it tomorrow.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree. It is involved.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let me try to simplify it more today before we sync up.
Co-authored-by: Steven Allen <steven@stebalien.com>
…ents into feat/payments-init-deposit
bool approved; | ||
uint256 modifiedAmount; | ||
uint256 settleUpto; | ||
string note; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can leave a note if successful too. For better UX, it's a catch all field for arbitrer to send us info.
|
||
} | ||
|
||
function updateRailArbiter(uint256 railId, address newArbiter) external validateRailActive(railId) onlyRailOperator(railId) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we really need this ?
address token, | ||
address from, | ||
address to, | ||
address operator, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
we don't need this as the msg.sender
will/should ALWAYS be the operator.
} | ||
|
||
// TODO: Revisit | ||
function terminateOperator(address operator) external { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
who calls this ?
answer: always the client.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@Stebalien Please can you review this function ?
@@ -0,0 +1,612 @@ | |||
// SPDX-License-Identifier: UNLICENSED |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Write down all the getter
APIs. That will determine state design.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Try to make it as easy as possible to access basic state without an Indexer.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Strike a balance between on-chain state and off-chain compute than an RPC provider needs to do to serve Getter requests.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Storing the keys of a Hasmap in an a set if you need iteration. Do it only when needed.
} | ||
|
||
// TODO: implement | ||
function terminateRail() public { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
An operator can always do this by setting everything to 0.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If a client wants to terminate a rail -> they go the operator and get them to modify the rates and lockups for the rail to 0.
} | ||
|
||
struct Rail { | ||
bool isActive; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We need this to track rail termination !
|
||
Rail storage paymentRail = rails[railId]; | ||
Account storage payer = accounts[paymentRail.token][paymentRail.from]; | ||
Account storage payee = accounts[paymentRail.token][paymentRail.to]; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Does the solidity compiler optimise this so we only lookup the map once ?
settleAccountLockup(payer); | ||
|
||
// Determine the maximum allowed epoch we can settle to based on the payer's lockupPeriod. | ||
uint256 maxSettlementEpoch = min(untilEpoch, payer.lockupLastSettledAt + paymentRail.lockupPeriod); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
can read payer.lockupLastSettledAt
from return value.
uint256 currentPaymentRate = paymentRail.paymentRate; | ||
|
||
// Retrieve any queued rate changes for this rail. | ||
RateChangeQueue.Queue storage rateQueue = railRateChangeQueues[railId]; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We can put the railRateChangeQueues
in the Rail
itself.
} | ||
|
||
// Revert if no progress was made (i.e., the final epoch did not advance). | ||
require(settledUntil > epochStart, "failed to settle: no progress made in settlement"); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Need to support settling nothing to handle partial settlements. E.g., we're going through the queue settling segment by segment and then we hit a segment where we can settle nothing. We need to accept that and settle everything so far without reverting.
uint256 railId, | ||
Rail storage rail, | ||
RateChangeQueue.Queue storage rateQueue, | ||
uint256 initialRate, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this is the current rail rate. fix naming.
uint256 activeRate = initialRate; | ||
note = ""; | ||
|
||
while (currentEpoch <= targetEpoch) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
currentEpoch < targetEpoch is what we need here. if it is equal, we are done and we should exit the loop.
RateChangeQueue.RateChange memory upcomingChange = rateQueue.peek(); | ||
bool isWithinRange = ( | ||
upcomingChange.untilEpoch >= currentEpoch && | ||
upcomingChange.untilEpoch <= targetEpoch |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This second condition won't be true if I'm trying to settle up to some historical epoch in the past.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We need to remove this assert.
|
||
// Otherwise, we have fully settled up to nextBoundary. | ||
currentEpoch = segmentEndEpoch; | ||
note = arbNote; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It would be kind of nice to somehow join these notes together when non-empty, but that isn't critical. We mostly care about the last note anyways.
// Dequeue the rate change we just passed (if it was indeed used this round). | ||
if (!rateQueue.isEmpty()) { | ||
// Peek again to confirm it's the one we used. | ||
RateChangeQueue.RateChange memory frontChange = rateQueue.peek(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How would this not be the case?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
if the queue is non-empty AND next element has expired -> until epoch is in the past or < targetEpoch -> pop.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
actually -> we can JUST dequeue if we reach here.
upcomingChange.untilEpoch <= targetEpoch | ||
); | ||
require(isWithinRange, "rate queue is in an invalid state"); | ||
nextBoundary = upcomingChange.untilEpoch; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
min(next, target)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nextBoundary = min(nextBoundary, target)
railId, | ||
rail, | ||
currentEpoch, | ||
nextBoundary, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
pass min(nextBoundary, targetEpoch)
uint256 activeRate = initialRate; | ||
note = ""; | ||
|
||
while (currentEpoch <= targetEpoch) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
fix comparison.
return (settledAmount, settledUntil, note); | ||
} | ||
|
||
function _settleWithRateChanges( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
mark the function as nonReEntrant
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Infact all functions that modify rail state should have a per-rail lock.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Audit and REALLY think through it.
* @return settledUntil The final epoch that effectively got settled. | ||
* @return note An optional note returned by the arbitrator. | ||
*/ | ||
function _settleSegment( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
rentrancy
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
per-rail settlement lock
} | ||
|
||
// railId => RateChangeQueue | ||
mapping(uint256 => RateChangeQueue.Queue) public railRateChangeQueues; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
move this to the Rail struct.
The re-entency issue: We discussed a few approaches:
We settled on:
Thinking on this more, there are a few more solutions (which I don't like but I figured I'd mention for completeness).
|
We also discussed how this relates to withdrawal. IMO, the difference between this and withdrawal is that all the "withdraw" call can do is revert. It can't cause us to change our minds and withdraw less than we initially intended so there are no funky intermediate states. |
This is the first draft for the payments contract MVP. It has the following features:
All rail modifications and creation respect the operator approval.
The following features which will be added in a subsequent PR: