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

[WIP] Horizon: add escrow and payments #968

Merged
merged 17 commits into from
May 20, 2024
Merged
Show file tree
Hide file tree
Changes from 13 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
2 changes: 1 addition & 1 deletion packages/contracts/contracts/governance/Controller.sol
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// SPDX-License-Identifier: GPL-2.0-or-later

pragma solidity ^0.7.6;
pragma solidity >=0.6.12 <0.9.0;

import { IController } from "./IController.sol";
import { IManaged } from "./IManaged.sol";
Expand Down
2 changes: 1 addition & 1 deletion packages/contracts/contracts/governance/Governed.sol
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// SPDX-License-Identifier: GPL-2.0-or-later

pragma solidity ^0.7.6;
pragma solidity >=0.6.12 <0.9.0;

/**
* @title Graph Governance contract
Expand Down
2 changes: 1 addition & 1 deletion packages/contracts/contracts/governance/IController.sol
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// SPDX-License-Identifier: GPL-2.0-or-later

pragma solidity >=0.6.12 <0.8.0;
pragma solidity >=0.6.12 <0.9.0;

interface IController {
function getGovernor() external view returns (address);
Expand Down
2 changes: 1 addition & 1 deletion packages/contracts/contracts/governance/IManaged.sol
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// SPDX-License-Identifier: GPL-2.0-or-later

pragma solidity ^0.7.6;
pragma solidity >=0.6.12 <0.9.0;

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

Expand Down
2 changes: 1 addition & 1 deletion packages/contracts/contracts/governance/Managed.sol
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// SPDX-License-Identifier: GPL-2.0-or-later

pragma solidity ^0.7.6;
pragma solidity >=0.6.12 <0.9.0;

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

Expand Down
2 changes: 1 addition & 1 deletion packages/contracts/contracts/governance/Pausable.sol
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// SPDX-License-Identifier: GPL-2.0-or-later

pragma solidity ^0.7.6;
pragma solidity >=0.6.12 <0.9.0;

abstract contract Pausable {
/**
Expand Down
3 changes: 3 additions & 0 deletions packages/contracts/contracts/staking/IHorizonStaking.sol
Original file line number Diff line number Diff line change
Expand Up @@ -146,4 +146,7 @@ interface IHorizonStaking {
function getServiceProvider(address serviceProvider) external view returns (ServiceProvider memory);

function getProvision(bytes32 provision) external view returns (Provision memory);

function getDelegationCut(address serviceProvider, uint8 paymentType) external view returns (uint256 delegationCut);
function addToDelegationPool(address serviceProvider, uint256 tokens) external;
}
2 changes: 1 addition & 1 deletion packages/contracts/contracts/token/IGraphToken.sol
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// SPDX-License-Identifier: GPL-2.0-or-later

pragma solidity ^0.7.6;
pragma solidity >=0.7.6 <=0.9.0;

import "@openzeppelin/contracts/token/ERC20/IERC20.sol";

Expand Down
25 changes: 25 additions & 0 deletions packages/horizon/contracts/GraphDirectory.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
// SPDX-License-Identifier: GPL-3.0-or-later

pragma solidity 0.8.24;

import { IController } from "@graphprotocol/contracts/contracts/governance/IController.sol";
import { IHorizonStaking } from "@graphprotocol/contracts/contracts/staking/IHorizonStaking.sol";
import { IGraphToken } from "@graphprotocol/contracts/contracts/token/IGraphToken.sol";
import { IGraphEscrow } from "./interfaces/IGraphEscrow.sol";
import { IGraphPayments } from "./interfaces/IGraphPayments.sol";

contract GraphDirectory {
IController public immutable graphController;
IHorizonStaking public immutable graphStaking;
IGraphToken public immutable graphToken;
IGraphEscrow public immutable graphEscrow;
IGraphPayments public immutable graphPayments;

constructor(address _controller) {
graphController = IController(_controller);
graphStaking = IHorizonStaking(graphController.getContractProxy(keccak256("Staking")));
graphToken = IGraphToken(graphController.getContractProxy(keccak256("GraphToken")));
graphEscrow = IGraphEscrow(graphController.getContractProxy(keccak256("GraphEscrow")));
graphPayments = IGraphPayments(graphController.getContractProxy(keccak256("GraphPayments")));
}
}
10 changes: 0 additions & 10 deletions packages/horizon/contracts/SimpleTest.sol

This file was deleted.

221 changes: 221 additions & 0 deletions packages/horizon/contracts/escrow/GraphEscrow.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,221 @@
// SPDX-License-Identifier: GPL-3.0-or-later
pragma solidity ^0.8.24;

import { IGraphEscrow } from "../interfaces/IGraphEscrow.sol";
import { IGraphPayments } from "../interfaces/IGraphPayments.sol";
import { GraphDirectory } from "../GraphDirectory.sol";
import { GraphEscrowStorageV1Storage } from "./GraphEscrowStorage.sol";
import { TokenUtils } from "../utils/TokenUtils.sol";

contract GraphEscrow is IGraphEscrow, GraphEscrowStorageV1Storage, GraphDirectory {
// -- Errors --

error GraphEscrowNotGraphPayments();
error GraphEscrowInputsLengthMismatch();
error GraphEscrowInsufficientThawAmount();
error GraphEscrowInsufficientAmount(uint256 available, uint256 required);
error GraphEscrowNotThawing();
error GraphEscrowStillThawing(uint256 currentTimestamp, uint256 thawEndTimestamp);
error GraphEscrowThawingPeriodTooLong(uint256 thawingPeriod, uint256 maxThawingPeriod);
error GraphEscrowCollectorNotAuthorized(address sender, address dataService);
error GraphEscrowCollectorInsufficientAmount(uint256 available, uint256 required);

// -- Events --

event AuthorizedCollector(address indexed sender, address indexed dataService);
event ThawCollector(address indexed sender, address indexed dataService);
event CancelThawCollector(address indexed sender, address indexed dataService);
event RevokeCollector(address indexed sender, address indexed dataService);
event Deposit(address indexed sender, address indexed receiver, uint256 amount);
event CancelThaw(address indexed sender, address indexed receiver);
event Thaw(
address indexed sender,
address indexed receiver,
uint256 amount,
uint256 totalAmountThawing,
uint256 thawEndTimestamp
);
event Withdraw(address indexed sender, address indexed receiver, uint256 amount);
event Collect(address indexed sender, address indexed receiver, uint256 amount);

// -- Constructor --

constructor(
address _controller,
uint256 _revokeCollectorThawingPeriod,
uint256 _withdrawEscrowThawingPeriod
) GraphDirectory(_controller) {
if (_revokeCollectorThawingPeriod > MAX_THAWING_PERIOD) {
revert GraphEscrowThawingPeriodTooLong(_revokeCollectorThawingPeriod, MAX_THAWING_PERIOD);
}

if (_withdrawEscrowThawingPeriod > MAX_THAWING_PERIOD) {
revert GraphEscrowThawingPeriodTooLong(_withdrawEscrowThawingPeriod, MAX_THAWING_PERIOD);
}

revokeCollectorThawingPeriod = _revokeCollectorThawingPeriod;
withdrawEscrowThawingPeriod = _withdrawEscrowThawingPeriod;
}

// approve a data service to collect funds
function approveCollector(address dataService, uint256 amount) external {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

A payer could bypass the thawing by re-approving a collector for a 0 amount.

I think we should not allow re approvals for amounts that are smaller than the current allowance.

If you want to reduce the allowance it should go through the thawing period.

authorizedCollectors[msg.sender][dataService].authorized = true;
authorizedCollectors[msg.sender][dataService].amount = amount;
emit AuthorizedCollector(msg.sender, dataService);
}

// thaw a data service's collector authorization
function thawCollector(address dataService) external {
authorizedCollectors[msg.sender][dataService].thawEndTimestamp = block.timestamp + revokeCollectorThawingPeriod;
emit ThawCollector(msg.sender, dataService);
}

// cancel thawing a data service's collector authorization
function cancelThawCollector(address dataService) external {
if (authorizedCollectors[msg.sender][dataService].thawEndTimestamp == 0) {
revert GraphEscrowNotThawing();
}

authorizedCollectors[msg.sender][dataService].thawEndTimestamp = 0;
emit CancelThawCollector(msg.sender, dataService);
}

// revoke authorized collector
function revokeCollector(address dataService) external {
Collector storage collector = authorizedCollectors[msg.sender][dataService];

if (collector.thawEndTimestamp == 0) {
revert GraphEscrowNotThawing();
}

if (collector.thawEndTimestamp > block.timestamp) {
revert GraphEscrowStillThawing(block.timestamp, collector.thawEndTimestamp);
}

delete authorizedCollectors[msg.sender][dataService];
emit RevokeCollector(msg.sender, dataService);
}

// Deposit funds into the escrow for a receiver
function deposit(address receiver, uint256 amount) external {
escrowAccounts[msg.sender][receiver].balance += amount;
TokenUtils.pullTokens(graphToken, msg.sender, amount);
emit Deposit(msg.sender, receiver, amount);
}

// Deposit funds into the escrow for multiple receivers
function depositMany(address[] calldata receivers, uint256[] calldata amounts) external {
if (receivers.length != amounts.length) {
revert GraphEscrowInputsLengthMismatch();
}

uint256 totalAmount = 0;
for (uint256 i = 0; i < receivers.length; i++) {
address receiver = receivers[i];
uint256 amount = amounts[i];

totalAmount += amount;
escrowAccounts[msg.sender][receiver].balance += amount;
emit Deposit(msg.sender, receiver, amount);
}

TokenUtils.pullTokens(graphToken, msg.sender, totalAmount);
}

// Requests to thaw a specific amount of escrow from a receiver's escrow account
function thaw(address receiver, uint256 amount) external {
EscrowAccount storage account = escrowAccounts[msg.sender][receiver];
if (amount == 0) {
// 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
// is a cancel thaw request.
if (account.amountThawing == 0) {
revert GraphEscrowInsufficientThawAmount();
}
account.amountThawing = 0;
account.thawEndTimestamp = 0;
emit CancelThaw(msg.sender, receiver);
return;
}

// Check if the escrow balance is sufficient
if (account.balance < amount) {
revert GraphEscrowInsufficientAmount({ available: account.balance, required: amount });
}

// Set amount to thaw
account.amountThawing = amount;
// Set when the thaw is complete (thawing period number of seconds after current timestamp)
account.thawEndTimestamp = block.timestamp + withdrawEscrowThawingPeriod;

emit Thaw(msg.sender, receiver, amount, account.amountThawing, account.thawEndTimestamp);
}

// Withdraws all thawed escrow from a receiver's escrow account
function withdraw(address receiver) external {
EscrowAccount storage account = escrowAccounts[msg.sender][receiver];
if (account.thawEndTimestamp == 0) {
revert GraphEscrowNotThawing();
}

if (account.thawEndTimestamp > block.timestamp) {
revert GraphEscrowStillThawing({
currentTimestamp: block.timestamp,
thawEndTimestamp: account.thawEndTimestamp
});
}

// Amount is the minimum between the amount being thawed and the actual balance
uint256 amount = account.amountThawing > account.balance ? account.balance : account.amountThawing;

account.balance -= amount; // Reduce the balance by the withdrawn amount (no underflow risk)
account.amountThawing = 0;
account.thawEndTimestamp = 0;
TokenUtils.pushTokens(graphToken, msg.sender, amount);
emit Withdraw(msg.sender, receiver, amount);
}

// Collect from escrow for a receiver using sender's deposit
function collect(
address sender,
address receiver, // serviceProvider
address dataService,
uint256 amount,
IGraphPayments.PaymentType paymentType,
uint256 tokensDataService
) external {
// Check if collector is authorized and has enough funds
Collector storage collector = authorizedCollectors[sender][msg.sender];

if (!collector.authorized) {
revert GraphEscrowCollectorNotAuthorized(sender, msg.sender);
}

if (collector.amount < amount) {
revert GraphEscrowCollectorInsufficientAmount(collector.amount, amount);
}

// Reduce amount from approved collector
collector.amount -= amount;

// Collect tokens from GraphEscrow up to amount available
EscrowAccount storage account = escrowAccounts[sender][receiver];
uint256 availableAmount = account.balance - account.amountThawing;
if (availableAmount < amount) {
revert GraphEscrowInsufficientAmount(availableAmount, amount);
}

account.balance -= amount;
emit Collect(sender, receiver, amount);

// Approve tokens so GraphPayments can pull them
graphToken.approve(address(graphPayments), amount);
graphPayments.collect(receiver, dataService, amount, paymentType, tokensDataService);
}

// Get the balance of a sender-receiver pair
function getBalance(address sender, address receiver) external view returns (uint256) {
EscrowAccount storage account = escrowAccounts[sender][receiver];
return account.balance - account.amountThawing;
}
}
24 changes: 24 additions & 0 deletions packages/horizon/contracts/escrow/GraphEscrowStorage.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
// SPDX-License-Identifier: GPL-3.0-or-later
pragma solidity ^0.8.24;

import { IGraphEscrow } from "../interfaces/IGraphEscrow.sol";

contract GraphEscrowStorageV1Storage {
// Authorized collectors
mapping(address sender => mapping(address dataService => IGraphEscrow.Collector collector))
public authorizedCollectors;

// Stores how much escrow each sender has deposited for each receiver, as well as thawing information
mapping(address sender => mapping(address receiver => IGraphEscrow.EscrowAccount escrowAccount))
public escrowAccounts;

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

// Thawing period for authorized collectors
uint256 public immutable revokeCollectorThawingPeriod;

// The duration (in seconds) in which escrow funds are thawing before they can be withdrawn
uint256 public immutable withdrawEscrowThawingPeriod;
}
43 changes: 43 additions & 0 deletions packages/horizon/contracts/interfaces/IGraphEscrow.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
// SPDX-License-Identifier: GPL-3.0-or-later
pragma solidity ^0.8.24;

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

interface IGraphEscrow {
struct EscrowAccount {
uint256 balance; // Total escrow balance for a sender-receiver pair
uint256 amountThawing; // Amount of escrow currently being thawed
uint256 thawEndTimestamp; // Timestamp at which thawing period ends (zero if not thawing)
}

// Collector
struct Collector {
bool authorized;
uint256 amount;
uint256 thawEndTimestamp;
}

// Deposit funds into the escrow for a receiver
function deposit(address receiver, uint256 amount) external;

// Deposit funds into the escrow for multiple receivers
function depositMany(address[] calldata receivers, uint256[] calldata amounts) external;

// Requests to thaw a specific amount of escrow from a receiver's escrow account
function thaw(address receiver, uint256 amount) external;

// Withdraws all thawed escrow from a receiver's escrow account
function withdraw(address receiver) external;

// Collect from escrow for a receiver using sender's deposit
function collect(
address sender,
address receiver,
address dataService,
uint256 amount,
IGraphPayments.PaymentType paymentType,
uint256 tokensDataService
) external;

function getBalance(address sender, address receiver) external view returns (uint256);
}
Loading
Loading