Skip to content

Commit

Permalink
feat: separate escrow accounts for each collector (#1058)
Browse files Browse the repository at this point in the history
* feat: separate escrow accounts for each collector

Signed-off-by: Tomás Migone <tomas@edgeandnode.com>

* feat: escrow account per collector

Signed-off-by: Tomás Migone <tomas@edgeandnode.com>

---------

Signed-off-by: Tomás Migone <tomas@edgeandnode.com>
  • Loading branch information
tmigone authored Oct 8, 2024
1 parent e40c8fe commit a76a0d6
Show file tree
Hide file tree
Showing 9 changed files with 91 additions and 73 deletions.
61 changes: 38 additions & 23 deletions packages/horizon/contracts/interfaces/IPaymentsEscrow.sol
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,9 @@ import { IGraphPayments } from "./IGraphPayments.sol";
* collector contract which implements the {IPaymentsCollector} interface.
*/
interface IPaymentsEscrow {
/// @notice Escrow account for a payer-receiver pair
/// @notice Escrow account for a payer-collector-receiver tuple
struct EscrowAccount {
// Total token balance for the payer-receiver pair
// Total token balance for the payer-collector-receiver tuple
uint256 balance;
// Amount of tokens currently being thawed
uint256 tokensThawing;
Expand Down Expand Up @@ -70,12 +70,13 @@ interface IPaymentsEscrow {
event RevokeCollector(address indexed payer, address indexed collector);

/**
* @notice Emitted when a payer deposits funds into the escrow for a payer-receiver pair
* @notice Emitted when a payer deposits funds into the escrow for a payer-collector-receiver tuple
* @param payer The address of the payer
* @param collector The address of the collector
* @param receiver The address of the receiver
* @param tokens The amount of tokens deposited
*/
event Deposit(address indexed payer, address indexed receiver, uint256 tokens);
event Deposit(address indexed payer, address indexed collector, address indexed receiver, uint256 tokens);

/**
* @notice Emitted when a payer cancels an escrow thawing
Expand All @@ -85,29 +86,38 @@ interface IPaymentsEscrow {
event CancelThaw(address indexed payer, address indexed receiver);

/**
* @notice Emitted when a payer thaws funds from the escrow for a payer-receiver pair
* @notice Emitted when a payer thaws funds from the escrow for a payer-collector-receiver tuple
* @param payer The address of the payer
* @param collector The address of the collector
* @param receiver The address of the receiver
* @param tokens The amount of tokens being thawed
* @param thawEndTimestamp The timestamp at which the thawing period ends
*/
event Thaw(address indexed payer, address indexed receiver, uint256 tokens, uint256 thawEndTimestamp);
event Thaw(
address indexed payer,
address indexed collector,
address indexed receiver,
uint256 tokens,
uint256 thawEndTimestamp
);

/**
* @notice Emitted when a payer withdraws funds from the escrow for a payer-receiver pair
* @notice Emitted when a payer withdraws funds from the escrow for a payer-collector-receiver tuple
* @param payer The address of the payer
* @param collector The address of the collector
* @param receiver The address of the receiver
* @param tokens The amount of tokens withdrawn
*/
event Withdraw(address indexed payer, address indexed receiver, uint256 tokens);
event Withdraw(address indexed payer, address indexed collector, address indexed receiver, uint256 tokens);

/**
* @notice Emitted when a collector collects funds from the escrow for a payer-receiver pair
* @notice Emitted when a collector collects funds from the escrow for a payer-collector-receiver tuple
* @param payer The address of the payer
* @param collector The address of the collector
* @param receiver The address of the receiver
* @param tokens The amount of tokens collected
*/
event EscrowCollected(address indexed payer, address indexed receiver, uint256 tokens);
event EscrowCollected(address indexed payer, address indexed collector, address indexed receiver, uint256 tokens);

// -- Errors --

Expand Down Expand Up @@ -211,26 +221,28 @@ interface IPaymentsEscrow {
function revokeCollector(address collector) external;

/**
* @notice Deposits funds into the escrow for a payer-receiver pair, where
* @notice Deposits funds into the escrow for a payer-collector-receiver tuple, where
* the payer is the transaction caller.
* @dev Emits a {Deposit} event
* @param collector The address of the collector
* @param receiver The address of the receiver
* @param tokens The amount of tokens to deposit
*/
function deposit(address receiver, uint256 tokens) external;
function deposit(address collector, address receiver, uint256 tokens) external;

/**
* @notice Deposits funds into the escrow for a payer-receiver pair, where
* @notice Deposits funds into the escrow for a payer-collector-receiver tuple, where
* the payer can be specified.
* @dev Emits a {Deposit} event
* @param payer The address of the payer
* @param collector The address of the collector
* @param receiver The address of the receiver
* @param tokens The amount of tokens to deposit
*/
function depositTo(address payer, address receiver, uint256 tokens) external;
function depositTo(address payer, address collector, address receiver, uint256 tokens) external;

/**
* @notice Thaw a specific amount of escrow from a payer-receiver's escrow account.
* @notice Thaw a specific amount of escrow from a payer-collector-receiver's escrow account.
* The payer is the transaction caller.
* If `tokens` is zero and funds were already thawing it will cancel the thawing.
* Note that repeated calls to this function will overwrite the previous thawing amount
Expand All @@ -240,13 +252,14 @@ interface IPaymentsEscrow {
*
* Emits a {Thaw} event. If `tokens` is zero it will emit a {CancelThaw} event.
*
* @param collector The address of the collector
* @param receiver The address of the receiver
* @param tokens The amount of tokens to thaw
*/
function thaw(address receiver, uint256 tokens) external;
function thaw(address collector, address receiver, uint256 tokens) external;

/**
* @notice Withdraws all thawed escrow from a payer-receiver's escrow account.
* @notice Withdraws all thawed escrow from a payer-collector-receiver's escrow account.
* The payer is the transaction caller.
* Note that the withdrawn funds might be less than the thawed amount if there were
* payment collections in the meantime.
Expand All @@ -255,12 +268,13 @@ interface IPaymentsEscrow {
*
* Emits a {Withdraw} event
*
* @param collector The address of the collector
* @param receiver The address of the receiver
*/
function withdraw(address receiver) external;
function withdraw(address collector, address receiver) external;

/**
* @notice Collects funds from the payer-receiver's escrow and sends them to {GraphPayments} for
* @notice Collects funds from the payer-collector-receiver's escrow and sends them to {GraphPayments} for
* distribution using the Graph Horizon Payments protocol.
* The function will revert if there are not enough funds in the escrow.
* @dev Requirements:
Expand All @@ -272,22 +286,23 @@ interface IPaymentsEscrow {
* @param payer The address of the payer
* @param receiver The address of the receiver
* @param tokens The amount of tokens to collect
* @param collector The address of the collector
* @param dataService The address of the data service
* @param tokensDataService The amount of tokens that {GraphPayments} should send to the data service
*/
function collect(
IGraphPayments.PaymentTypes paymentType,
address payer,
address receiver,
uint256 tokens,
address collector,
address dataService,
uint256 tokensDataService
) external;

/**
* @notice Get the balance of a payer-receiver pair
* @notice Get the balance of a payer-collector-receiver tuple
* @param payer The address of the payer
* @param collector The address of the collector
* @param receiver The address of the receiver
*/
function getBalance(address payer, address receiver) external view returns (uint256);
function getBalance(address payer, address collector, address receiver) external view returns (uint256);
}
49 changes: 26 additions & 23 deletions packages/horizon/contracts/payments/PaymentsEscrow.sol
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,11 @@ contract PaymentsEscrow is Initializable, MulticallUpgradeable, GraphDirectory,
mapping(address payer => mapping(address collector => IPaymentsEscrow.Collector collectorDetails))
public authorizedCollectors;

/// @notice Escrow account details for payer-receiver pairs
mapping(address payer => mapping(address receiver => IPaymentsEscrow.EscrowAccount escrowAccount))
/// @notice Escrow account details for payer-collector-receiver tuples
mapping(address payer => mapping(address collector => mapping(address receiver => IPaymentsEscrow.EscrowAccount escrowAccount)))
public escrowAccounts;

/// @notice The maximum thawing period (in seconds) for both escrow withdrawal and signer revocation
/// @notice The maximum thawing period (in seconds) for both escrow withdrawal and collector revocation
/// @dev This is a precautionary measure to avoid inadvertedly locking funds for too long
uint256 public constant MAX_WAIT_PERIOD = 90 days;

Expand Down Expand Up @@ -126,22 +126,22 @@ contract PaymentsEscrow is Initializable, MulticallUpgradeable, GraphDirectory,
/**
* @notice See {IPaymentsEscrow-deposit}
*/
function deposit(address receiver, uint256 tokens) external override notPaused {
_deposit(msg.sender, receiver, tokens);
function deposit(address collector, address receiver, uint256 tokens) external override notPaused {
_deposit(msg.sender, collector, receiver, tokens);
}

/**
* @notice See {IPaymentsEscrow-depositTo}
*/
function depositTo(address payer, address receiver, uint256 tokens) external override notPaused {
_deposit(payer, receiver, tokens);
function depositTo(address payer, address collector, address receiver, uint256 tokens) external override notPaused {
_deposit(payer, collector, receiver, tokens);
}

/**
* @notice See {IPaymentsEscrow-thaw}
*/
function thaw(address receiver, uint256 tokens) external override notPaused {
EscrowAccount storage account = escrowAccounts[msg.sender][receiver];
function thaw(address collector, address receiver, uint256 tokens) external override notPaused {
EscrowAccount storage account = escrowAccounts[msg.sender][collector][receiver];

// if amount thawing is zero and requested amount is zero this is an invalid request.
// otherwise if amount thawing is greater than zero and requested amount is zero this
Expand All @@ -159,14 +159,14 @@ contract PaymentsEscrow is Initializable, MulticallUpgradeable, GraphDirectory,
account.tokensThawing = tokens;
account.thawEndTimestamp = block.timestamp + WITHDRAW_ESCROW_THAWING_PERIOD;

emit Thaw(msg.sender, receiver, tokens, account.thawEndTimestamp);
emit Thaw(msg.sender, collector, receiver, tokens, account.thawEndTimestamp);
}

/**
* @notice See {IPaymentsEscrow-withdraw}
*/
function withdraw(address receiver) external override notPaused {
EscrowAccount storage account = escrowAccounts[msg.sender][receiver];
function withdraw(address collector, address receiver) external override notPaused {
EscrowAccount storage account = escrowAccounts[msg.sender][collector][receiver];
require(account.thawEndTimestamp != 0, PaymentsEscrowNotThawing());
require(
account.thawEndTimestamp < block.timestamp,
Expand All @@ -180,7 +180,7 @@ contract PaymentsEscrow is Initializable, MulticallUpgradeable, GraphDirectory,
account.tokensThawing = 0;
account.thawEndTimestamp = 0;
_graphToken().pushTokens(msg.sender, tokens);
emit Withdraw(msg.sender, receiver, tokens);
emit Withdraw(msg.sender, collector, receiver, tokens);
}

/**
Expand All @@ -195,15 +195,18 @@ contract PaymentsEscrow is Initializable, MulticallUpgradeable, GraphDirectory,
uint256 tokensDataService
) external override notPaused {
// Check if collector is authorized and has enough funds
Collector storage collector = authorizedCollectors[payer][msg.sender];
require(collector.allowance >= tokens, PaymentsEscrowInsufficientAllowance(collector.allowance, tokens));
Collector storage collectorDetails = authorizedCollectors[payer][msg.sender];
require(
collectorDetails.allowance >= tokens,
PaymentsEscrowInsufficientAllowance(collectorDetails.allowance, tokens)
);

// Check if there are enough funds in the escrow account
EscrowAccount storage account = escrowAccounts[payer][receiver];
EscrowAccount storage account = escrowAccounts[payer][msg.sender][receiver];
require(account.balance >= tokens, PaymentsEscrowInsufficientBalance(account.balance, tokens));

// Reduce amount from approved collector and account balance
collector.allowance -= tokens;
collectorDetails.allowance -= tokens;
account.balance -= tokens;

uint256 balanceBefore = _graphToken().balanceOf(address(this));
Expand All @@ -217,14 +220,14 @@ contract PaymentsEscrow is Initializable, MulticallUpgradeable, GraphDirectory,
PaymentsEscrowInconsistentCollection(balanceBefore, balanceAfter, tokens)
);

emit EscrowCollected(payer, receiver, tokens);
emit EscrowCollected(payer, msg.sender, receiver, tokens);
}

/**
* @notice See {IPaymentsEscrow-getBalance}
*/
function getBalance(address payer, address receiver) external view override returns (uint256) {
EscrowAccount storage account = escrowAccounts[payer][receiver];
function getBalance(address payer, address collector, address receiver) external view override returns (uint256) {
EscrowAccount storage account = escrowAccounts[payer][collector][receiver];
return account.balance - account.tokensThawing;
}

Expand All @@ -234,9 +237,9 @@ contract PaymentsEscrow is Initializable, MulticallUpgradeable, GraphDirectory,
* @param _receiver The address of the receiver
* @param _tokens The amount of tokens to deposit
*/
function _deposit(address _payer, address _receiver, uint256 _tokens) private {
escrowAccounts[_payer][_receiver].balance += _tokens;
function _deposit(address _payer, address _collector, address _receiver, uint256 _tokens) private {
escrowAccounts[_payer][_collector][_receiver].balance += _tokens;
_graphToken().pullTokens(msg.sender, _tokens);
emit Deposit(_payer, _receiver, _tokens);
emit Deposit(_payer, _collector, _receiver, _tokens);
}
}
4 changes: 2 additions & 2 deletions packages/horizon/test/escrow/GraphEscrow.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ contract GraphEscrowTest is HorizonStakingSharedTest {
vm.assume(thawAmount > 0);
vm.assume(amount > thawAmount);
_depositTokens(amount);
escrow.thaw(users.indexer, thawAmount);
escrow.thaw(users.verifier, users.indexer, thawAmount);
_;
}

Expand All @@ -49,7 +49,7 @@ contract GraphEscrowTest is HorizonStakingSharedTest {

function _depositTokens(uint256 tokens) internal {
token.approve(address(escrow), tokens);
escrow.deposit(users.indexer, tokens);
escrow.deposit(users.verifier, users.indexer, tokens);
}

function _approveEscrow(uint256 tokens) internal {
Expand Down
14 changes: 7 additions & 7 deletions packages/horizon/test/escrow/collect.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,10 @@ contract GraphEscrowCollectTest is GraphEscrowTest {
address _dataService,
uint256 _tokensDataService
) private {
(, address _collector, ) = vm.readCallers();

// Previous balances
(uint256 previousPayerEscrowBalance,,) = escrow.escrowAccounts(_payer, _receiver);
(uint256 previousPayerEscrowBalance,,) = escrow.escrowAccounts(_payer, _collector, _receiver);
CollectPaymentData memory previousBalances = CollectPaymentData({
escrowBalance: token.balanceOf(address(escrow)),
paymentsBalance: token.balanceOf(address(payments)),
Expand All @@ -41,7 +43,7 @@ contract GraphEscrowCollectTest is GraphEscrowTest {
});

vm.expectEmit(address(escrow));
emit IPaymentsEscrow.EscrowCollected(_payer, _receiver, _tokens);
emit IPaymentsEscrow.EscrowCollected(_payer, _collector, _receiver, _tokens);
escrow.collect(_paymentType, _payer, _receiver, _tokens, _dataService, _tokensDataService);

// Calculate cuts
Expand All @@ -51,11 +53,9 @@ contract GraphEscrowCollectTest is GraphEscrowTest {
_dataService,
_paymentType
);
uint256 tokensProtocol = _tokens * protocolPaymentCut / MAX_PPM;
uint256 tokensDelegation = _tokens * delegatorCut / MAX_PPM;

// After balances
(uint256 afterPayerEscrowBalance,,) = escrow.escrowAccounts(_payer, _receiver);
(uint256 afterPayerEscrowBalance,,) = escrow.escrowAccounts(_payer, _collector, _receiver);
CollectPaymentData memory afterBalances = CollectPaymentData({
escrowBalance: token.balanceOf(address(escrow)),
paymentsBalance: token.balanceOf(address(payments)),
Expand All @@ -68,12 +68,12 @@ contract GraphEscrowCollectTest is GraphEscrowTest {
});

// Check receiver balance after payment
uint256 receiverExpectedPayment = _tokens - _tokensDataService - tokensProtocol - tokensDelegation;
uint256 receiverExpectedPayment = _tokens - _tokensDataService - _tokens * protocolPaymentCut / MAX_PPM - _tokens * delegatorCut / MAX_PPM;
assertEq(afterBalances.receiverBalance - previousBalances.receiverBalance, receiverExpectedPayment);
assertEq(token.balanceOf(address(payments)), 0);

// Check delegation pool balance after payment
assertEq(afterBalances.delegationPoolBalance - previousBalances.delegationPoolBalance, tokensDelegation);
assertEq(afterBalances.delegationPoolBalance - previousBalances.delegationPoolBalance, _tokens * delegatorCut / MAX_PPM);

// Check that the escrow account has been updated
assertEq(previousBalances.escrowBalance, afterBalances.escrowBalance + _tokens);
Expand Down
2 changes: 1 addition & 1 deletion packages/horizon/test/escrow/deposit.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ contract GraphEscrowDepositTest is GraphEscrowTest {
*/

function testDeposit_Tokens(uint256 amount) public useGateway useDeposit(amount) {
(uint256 indexerEscrowBalance,,) = escrow.escrowAccounts(users.gateway, users.indexer);
(uint256 indexerEscrowBalance,,) = escrow.escrowAccounts(users.gateway, users.verifier, users.indexer);
assertEq(indexerEscrowBalance, amount);
}
}
Loading

0 comments on commit a76a0d6

Please sign in to comment.