-
Notifications
You must be signed in to change notification settings - Fork 146
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #968 from graphprotocol/mde/horizon-escrow-and-pay…
…ments [WIP] Horizon: add escrow and payments
- Loading branch information
Showing
23 changed files
with
1,038 additions
and
5 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) {} | ||
} |
Oops, something went wrong.