Skip to content
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

feat: separate escrow accounts for each collector #1058

Merged
merged 2 commits into from
Oct 8, 2024
Merged
Show file tree
Hide file tree
Changes from all 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
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