Skip to content

Commit

Permalink
Merge pull request #968 from graphprotocol/mde/horizon-escrow-and-pay…
Browse files Browse the repository at this point in the history
…ments

[WIP] Horizon: add escrow and payments
  • Loading branch information
Maikol authored May 20, 2024
2 parents 7aa3798 + 65157fb commit 0cdda26
Show file tree
Hide file tree
Showing 23 changed files with 1,038 additions and 5 deletions.
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
4 changes: 4 additions & 0 deletions packages/horizon/contracts/GraphDirectory.sol
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ contract GraphDirectory {
// Legacy contracts (pre-Horizon) used for StakingBackwardCompatibility
address public immutable REWARDS_MANAGER;
address public immutable CURATION;
address public immutable GRAPH_PAYMENTS;
address public immutable GRAPH_ESCROW;

constructor(address _controller) {
CONTROLLER = _controller;
Expand All @@ -30,5 +32,7 @@ contract GraphDirectory {
GRAPH_TOKEN_GATEWAY = IController(_controller).getContractProxy(keccak256("GraphTokenGateway"));
REWARDS_MANAGER = IController(_controller).getContractProxy(keccak256("RewardsManager"));
CURATION = IController(_controller).getContractProxy(keccak256("Curation"));
GRAPH_PAYMENTS = IController(_controller).getContractProxy(keccak256("GraphPayments"));
GRAPH_ESCROW = IController(_controller).getContractProxy(keccak256("GraphEscrow"));
}
}
224 changes: 224 additions & 0 deletions packages/horizon/contracts/escrow/GraphEscrow.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,224 @@
// SPDX-License-Identifier: GPL-3.0-or-later
pragma solidity ^0.8.24;

import { IGraphToken } from "../interfaces/IGraphToken.sol";
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 "../libraries/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 {
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(IGraphToken(GRAPH_TOKEN), 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(IGraphToken(GRAPH_TOKEN), 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(IGraphToken(GRAPH_TOKEN), 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.PaymentTypes 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
IGraphToken graphToken = IGraphToken(GRAPH_TOKEN);
IGraphPayments graphPayments = IGraphPayments(GRAPH_PAYMENTS);
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;
}
39 changes: 38 additions & 1 deletion packages/horizon/contracts/interfaces/IGraphEscrow.sol
Original file line number Diff line number Diff line change
@@ -1,6 +1,43 @@
// SPDX-License-Identifier: GPL-3.0-or-later
pragma solidity ^0.8.24;

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

interface IGraphEscrow {
function getSender(address signer) external view returns (address sender);
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.PaymentTypes paymentType,
uint256 tokensDataService
) external;

function getBalance(address sender, address receiver) external view returns (uint256);
}
6 changes: 4 additions & 2 deletions packages/horizon/contracts/interfaces/IGraphPayments.sol
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,18 @@
pragma solidity ^0.8.24;

interface IGraphPayments {
// Payment types
enum PaymentTypes {
QueryFee,
IndexingFee
}

// collect funds from a sender, pay cuts and forward the rest to the receiver
function collect(
address sender,
address receiver,
address dataService,
uint256 tokens,
PaymentTypes paymentType,
uint256 tokensDataService
) external returns (uint256);
) external;
}
46 changes: 46 additions & 0 deletions packages/horizon/contracts/mocks/MockGRTToken.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@graphprotocol/contracts/contracts/token/IGraphToken.sol";

contract MockGRTToken is ERC20, IGraphToken {
constructor() ERC20("Graph Token", "GRT") {}

function burn(uint256 amount) external {}

function burnFrom(address _from, uint256 amount) external {
_burn(_from, amount);
}

function mint(address to, uint256 amount) public {
_mint(to, amount);
}

// -- Mint Admin --

function addMinter(address _account) external {}

function removeMinter(address _account) external {}

function renounceMinter() external {}

function isMinter(address _account) external view returns (bool) {}

// -- Permit --

function permit(
address _owner,
address _spender,
uint256 _value,
uint256 _deadline,
uint8 _v,
bytes32 _r,
bytes32 _s
) external {}

// -- Allowance --

function increaseAllowance(address spender, uint256 addedValue) external returns (bool) {}
function decreaseAllowance(address spender, uint256 subtractedValue) external returns (bool) {}
}
Loading

0 comments on commit 0cdda26

Please sign in to comment.