From 2cea50f11d5fd780a289fcfde4a7cfb031e8fb8f Mon Sep 17 00:00:00 2001 From: Miguel de Elias Date: Wed, 10 Apr 2024 17:36:41 -0300 Subject: [PATCH 01/15] chore: add GraphEscrow --- packages/horizon/contracts/GraphEscrow.sol | 50 +++++++++++++++++++ .../horizon/contracts/GraphEscrowStorage.sol | 17 +++++++ .../contracts/interfaces/IGraphEscrow.sol | 25 ++++++++++ .../contracts/interfaces/IGraphPayments.sol | 13 +++++ packages/subgraph-service/lib/forge-std | 1 + 5 files changed, 106 insertions(+) create mode 100644 packages/horizon/contracts/GraphEscrow.sol create mode 100644 packages/horizon/contracts/GraphEscrowStorage.sol create mode 100644 packages/horizon/contracts/interfaces/IGraphEscrow.sol create mode 100644 packages/horizon/contracts/interfaces/IGraphPayments.sol create mode 160000 packages/subgraph-service/lib/forge-std diff --git a/packages/horizon/contracts/GraphEscrow.sol b/packages/horizon/contracts/GraphEscrow.sol new file mode 100644 index 000000000..a6a58c7e1 --- /dev/null +++ b/packages/horizon/contracts/GraphEscrow.sol @@ -0,0 +1,50 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity ^0.8.24; + +import { IGraphToken } from "@graphprotocol/contracts/contracts/token/IGraphToken.sol"; + +import { IGraphPayments } from "./interfaces/IGraphPayments.sol"; +import { GraphEscrowStorageV1Storage } from "./GraphEscrowStorage.sol"; + +contract GraphEscrow is GraphEscrowStorageV1Storage { + // -- Errors -- + + error GraphEscrowNotGraphPayments(); + + // -- Immutable variables -- + + IGraphToken public immutable graphToken; + IGraphPayments public immutable graphPayments; + + // -- Modifier -- + + modifier onlyGraphPayments() { + if (msg.sender != address(graphPayments)) { + revert GraphEscrowNotGraphPayments(); + } + _; + } + + // -- Constructor -- + + constructor(address _graphToken, address _graphPayments, uint256 _withdrawEscrowThawingPeriod) { + graphToken = IGraphToken(_graphToken); + graphPayments = IGraphPayments(_graphPayments); + _withdrawEscrowThawingPeriod = _withdrawEscrowThawingPeriod; + } + + // 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 (up to amount available in escrow) for a receiver using sender's deposit + function collect(address sender, address receiver, uint256 amount) external onlyGraphPayments {} +} diff --git a/packages/horizon/contracts/GraphEscrowStorage.sol b/packages/horizon/contracts/GraphEscrowStorage.sol new file mode 100644 index 000000000..aa8567d8b --- /dev/null +++ b/packages/horizon/contracts/GraphEscrowStorage.sol @@ -0,0 +1,17 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity ^0.8.24; + +import { IGraphEscrow } from "./interfaces/IGraphEscrow.sol"; + +contract GraphEscrowStorageV1Storage { + // 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 duration (in seconds) in which escrow funds are thawing before they can be withdrawn + uint256 public immutable withdrawEscrowThawingPeriod; + + // 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; +} diff --git a/packages/horizon/contracts/interfaces/IGraphEscrow.sol b/packages/horizon/contracts/interfaces/IGraphEscrow.sol new file mode 100644 index 000000000..8e6ca58fd --- /dev/null +++ b/packages/horizon/contracts/interfaces/IGraphEscrow.sol @@ -0,0 +1,25 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity ^0.8.24; + +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) + } + + // 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 (up to amount available in escrow) for a receiver using sender's deposit + function collect(address sender, address receiver, uint256 amount) external; +} diff --git a/packages/horizon/contracts/interfaces/IGraphPayments.sol b/packages/horizon/contracts/interfaces/IGraphPayments.sol new file mode 100644 index 000000000..2dc89374a --- /dev/null +++ b/packages/horizon/contracts/interfaces/IGraphPayments.sol @@ -0,0 +1,13 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity ^0.8.24; + +interface IGraphPayments { + function approveCollector(address dataService) external; + function collect( + address sender, + address receiver, + uint256 amount, + uint256 paymentType, + uint256 dataServiceCut + ) external; +} diff --git a/packages/subgraph-service/lib/forge-std b/packages/subgraph-service/lib/forge-std new file mode 160000 index 000000000..bb4ceea94 --- /dev/null +++ b/packages/subgraph-service/lib/forge-std @@ -0,0 +1 @@ +Subproject commit bb4ceea94d6f10eeb5b41dc2391c6c8bf8e734ef From ac75c308831f9cd26aff754540ec91196c50301a Mon Sep 17 00:00:00 2001 From: Miguel de Elias Date: Fri, 12 Apr 2024 13:03:58 -0300 Subject: [PATCH 02/15] chore: add comment --- packages/horizon/contracts/interfaces/IGraphPayments.sol | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/horizon/contracts/interfaces/IGraphPayments.sol b/packages/horizon/contracts/interfaces/IGraphPayments.sol index 2dc89374a..2836844c4 100644 --- a/packages/horizon/contracts/interfaces/IGraphPayments.sol +++ b/packages/horizon/contracts/interfaces/IGraphPayments.sol @@ -2,7 +2,10 @@ pragma solidity ^0.8.24; interface IGraphPayments { + // approve a data service to collect funds function approveCollector(address dataService) external; + + // collect funds from a sender, pay cuts and forward the rest to the receiver function collect( address sender, address receiver, From cb1c50a069adb277db441d36a1ed14cf15992112 Mon Sep 17 00:00:00 2001 From: Miguel de Elias Date: Fri, 12 Apr 2024 15:21:48 -0300 Subject: [PATCH 03/15] chore: add GraphPayments interface and data --- packages/horizon/contracts/GraphEscrow.sol | 3 +- .../horizon/contracts/GraphEscrowStorage.sol | 6 +-- packages/horizon/contracts/GraphPayments.sol | 50 +++++++++++++++++++ .../contracts/GraphPaymentsStorage.sol | 19 +++++++ .../contracts/interfaces/IGraphPayments.sol | 18 ++++++- 5 files changed, 91 insertions(+), 5 deletions(-) create mode 100644 packages/horizon/contracts/GraphPayments.sol create mode 100644 packages/horizon/contracts/GraphPaymentsStorage.sol diff --git a/packages/horizon/contracts/GraphEscrow.sol b/packages/horizon/contracts/GraphEscrow.sol index a6a58c7e1..06f6e4b42 100644 --- a/packages/horizon/contracts/GraphEscrow.sol +++ b/packages/horizon/contracts/GraphEscrow.sol @@ -3,10 +3,11 @@ pragma solidity ^0.8.24; import { IGraphToken } from "@graphprotocol/contracts/contracts/token/IGraphToken.sol"; +import { IGraphEscrow } from "./interfaces/IGraphEscrow.sol"; import { IGraphPayments } from "./interfaces/IGraphPayments.sol"; import { GraphEscrowStorageV1Storage } from "./GraphEscrowStorage.sol"; -contract GraphEscrow is GraphEscrowStorageV1Storage { +contract GraphEscrow is IGraphEscrow, GraphEscrowStorageV1Storage { // -- Errors -- error GraphEscrowNotGraphPayments(); diff --git a/packages/horizon/contracts/GraphEscrowStorage.sol b/packages/horizon/contracts/GraphEscrowStorage.sol index aa8567d8b..472915481 100644 --- a/packages/horizon/contracts/GraphEscrowStorage.sol +++ b/packages/horizon/contracts/GraphEscrowStorage.sol @@ -8,10 +8,10 @@ contract GraphEscrowStorageV1Storage { mapping(address sender => mapping(address receiver => IGraphEscrow.EscrowAccount escrowAccount)) public escrowAccounts; - // The duration (in seconds) in which escrow funds are thawing before they can be withdrawn - uint256 public immutable withdrawEscrowThawingPeriod; - // 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; + + // The duration (in seconds) in which escrow funds are thawing before they can be withdrawn + uint256 public immutable withdrawEscrowThawingPeriod; } diff --git a/packages/horizon/contracts/GraphPayments.sol b/packages/horizon/contracts/GraphPayments.sol new file mode 100644 index 000000000..7131d7abd --- /dev/null +++ b/packages/horizon/contracts/GraphPayments.sol @@ -0,0 +1,50 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity ^0.8.24; + +import { IGraphToken } from "@graphprotocol/contracts/contracts/token/IGraphToken.sol"; +import { IHorizonStaking } from "@graphprotocol/contracts/contracts/staking/IHorizonStaking.sol"; + +import { IGraphPayments } from "./interfaces/IGraphPayments.sol"; +import { IGraphEscrow } from "./interfaces/IGraphEscrow.sol"; +import { GraphPaymentsStorageV1Storage } from "./GraphPaymentsStorage.sol"; + +contract GraphEscrow is IGraphPayments, GraphPaymentsStorageV1Storage { + // -- Errors -- + + // -- Immutable variables -- + + IGraphToken public immutable graphToken; + IHorizonStaking public immutable staking; + IGraphEscrow public immutable graphEscrow; + + // -- Modifier -- + + // -- Constructor -- + + constructor(address _graphToken, address _staking, address _graphEscrow) { + graphToken = IGraphToken(_graphToken); + staking = IHorizonStaking(_staking); + graphEscrow = IGraphEscrow(_graphEscrow); + } + + // approve a data service to collect funds + function approveCollector(address dataService) external {} + + // thaw a data service's collector authorization + function thawCollector(address dataService) external {} + + // cancel thawing a data service's collector authorization + function cancelThawCollector(address dataService) external {} + + // revoke authorized collector + function revokeCollector(address dataService) external {} + + // collect funds from a sender, pay cuts and forward the rest to the receiver + function collect( + address sender, + address receiver, + uint256 amount, + uint256 paymentType, + uint256 dataServiceCut + ) external {} +} diff --git a/packages/horizon/contracts/GraphPaymentsStorage.sol b/packages/horizon/contracts/GraphPaymentsStorage.sol new file mode 100644 index 000000000..ebf732f70 --- /dev/null +++ b/packages/horizon/contracts/GraphPaymentsStorage.sol @@ -0,0 +1,19 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity ^0.8.24; + +import { IGraphPayments } from "./interfaces/IGraphPayments.sol"; + +contract GraphPaymentsStorageV1Storage { + // Authorized collectors + mapping(address sender => mapping(address collector => uint256 thawEndTimestamp)) public authorizedCollectors; + + // The maximum thawing period (in seconds) for removing collector authorization + // This is a precautionary measure to avoid inadvertedly locking collectors for too long + uint256 public constant MAX_THAWING_PERIOD = 90 days; + + // Thawing period for authorized collectors + uint256 public immutable revokeCollectorThawingPeriod; + + // The graph protocol payment cut + uint256 public immutable protocolPaymentCut; +} diff --git a/packages/horizon/contracts/interfaces/IGraphPayments.sol b/packages/horizon/contracts/interfaces/IGraphPayments.sol index 2836844c4..b008878fb 100644 --- a/packages/horizon/contracts/interfaces/IGraphPayments.sol +++ b/packages/horizon/contracts/interfaces/IGraphPayments.sol @@ -2,15 +2,31 @@ pragma solidity ^0.8.24; interface IGraphPayments { + // Payment types + enum PaymentType { + Null, + Indexing, + Query + } + // approve a data service to collect funds function approveCollector(address dataService) external; + // thaw a data service's collector authorization + function thawCollector(address dataService) external; + + // cancel thawing a data service's collector authorization + function cancelThawCollector(address dataService) external; + + // revoke authorized collector + function revokeCollector(address dataService) external; + // collect funds from a sender, pay cuts and forward the rest to the receiver function collect( address sender, address receiver, uint256 amount, - uint256 paymentType, + PaymentType paymentType, uint256 dataServiceCut ) external; } From f22adef7046982efbdc5c3633e31a3c164964574 Mon Sep 17 00:00:00 2001 From: Miguel de Elias Date: Fri, 12 Apr 2024 18:16:44 -0300 Subject: [PATCH 04/15] chore: implement Escrow deposit --- .../contracts/governance/Controller.sol | 2 +- .../contracts/governance/Governed.sol | 2 +- .../contracts/governance/IController.sol | 2 +- .../contracts/governance/IManaged.sol | 2 +- .../contracts/governance/Managed.sol | 2 +- .../contracts/governance/Pausable.sol | 2 +- .../contracts/contracts/token/IGraphToken.sol | 2 +- packages/horizon/contracts/GraphDirectory.sol | 25 +++++++++ packages/horizon/contracts/GraphEscrow.sol | 20 +++---- packages/horizon/contracts/GraphPayments.sol | 4 +- packages/horizon/contracts/SimpleTest.sol | 10 ---- .../contracts/interfaces/IGraphPayments.sol | 5 +- packages/horizon/lib/forge-std | 2 +- packages/horizon/package.json | 1 + packages/horizon/test/GraphEscrow.t.sol | 54 +++++++++++++++++++ packages/horizon/test/SimpleTest.t.sol | 17 ------ packages/horizon/test/SimpleTest.ts | 23 -------- packages/horizon/test/mocks/MockGRTToken.sol | 46 ++++++++++++++++ yarn.lock | 8 +++ 19 files changed, 157 insertions(+), 72 deletions(-) create mode 100644 packages/horizon/contracts/GraphDirectory.sol delete mode 100644 packages/horizon/contracts/SimpleTest.sol create mode 100644 packages/horizon/test/GraphEscrow.t.sol delete mode 100644 packages/horizon/test/SimpleTest.t.sol delete mode 100644 packages/horizon/test/SimpleTest.ts create mode 100644 packages/horizon/test/mocks/MockGRTToken.sol diff --git a/packages/contracts/contracts/governance/Controller.sol b/packages/contracts/contracts/governance/Controller.sol index affb29a05..2f3654109 100644 --- a/packages/contracts/contracts/governance/Controller.sol +++ b/packages/contracts/contracts/governance/Controller.sol @@ -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"; diff --git a/packages/contracts/contracts/governance/Governed.sol b/packages/contracts/contracts/governance/Governed.sol index f692b2d19..9902eafc9 100644 --- a/packages/contracts/contracts/governance/Governed.sol +++ b/packages/contracts/contracts/governance/Governed.sol @@ -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 diff --git a/packages/contracts/contracts/governance/IController.sol b/packages/contracts/contracts/governance/IController.sol index 7df3d94ee..0549c723d 100644 --- a/packages/contracts/contracts/governance/IController.sol +++ b/packages/contracts/contracts/governance/IController.sol @@ -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); diff --git a/packages/contracts/contracts/governance/IManaged.sol b/packages/contracts/contracts/governance/IManaged.sol index 76f05e0fb..4cedf9c0b 100644 --- a/packages/contracts/contracts/governance/IManaged.sol +++ b/packages/contracts/contracts/governance/IManaged.sol @@ -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"; diff --git a/packages/contracts/contracts/governance/Managed.sol b/packages/contracts/contracts/governance/Managed.sol index fb65e71b9..f0172dc94 100644 --- a/packages/contracts/contracts/governance/Managed.sol +++ b/packages/contracts/contracts/governance/Managed.sol @@ -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"; diff --git a/packages/contracts/contracts/governance/Pausable.sol b/packages/contracts/contracts/governance/Pausable.sol index 552b0aa15..5ff29c19e 100644 --- a/packages/contracts/contracts/governance/Pausable.sol +++ b/packages/contracts/contracts/governance/Pausable.sol @@ -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 { /** diff --git a/packages/contracts/contracts/token/IGraphToken.sol b/packages/contracts/contracts/token/IGraphToken.sol index 8255e18d5..f24467d42 100644 --- a/packages/contracts/contracts/token/IGraphToken.sol +++ b/packages/contracts/contracts/token/IGraphToken.sol @@ -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"; diff --git a/packages/horizon/contracts/GraphDirectory.sol b/packages/horizon/contracts/GraphDirectory.sol new file mode 100644 index 000000000..d229bd0c4 --- /dev/null +++ b/packages/horizon/contracts/GraphDirectory.sol @@ -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"))); + } +} diff --git a/packages/horizon/contracts/GraphEscrow.sol b/packages/horizon/contracts/GraphEscrow.sol index 06f6e4b42..b2acd630c 100644 --- a/packages/horizon/contracts/GraphEscrow.sol +++ b/packages/horizon/contracts/GraphEscrow.sol @@ -5,17 +5,17 @@ import { IGraphToken } from "@graphprotocol/contracts/contracts/token/IGraphToke import { IGraphEscrow } from "./interfaces/IGraphEscrow.sol"; import { IGraphPayments } from "./interfaces/IGraphPayments.sol"; +import { GraphDirectory } from "./GraphDirectory.sol"; import { GraphEscrowStorageV1Storage } from "./GraphEscrowStorage.sol"; -contract GraphEscrow is IGraphEscrow, GraphEscrowStorageV1Storage { +contract GraphEscrow is IGraphEscrow, GraphEscrowStorageV1Storage, GraphDirectory { // -- Errors -- error GraphEscrowNotGraphPayments(); - // -- Immutable variables -- + // -- Events -- - IGraphToken public immutable graphToken; - IGraphPayments public immutable graphPayments; + event Deposit(address indexed sender, address indexed receiver, uint256 amount); // -- Modifier -- @@ -28,14 +28,16 @@ contract GraphEscrow is IGraphEscrow, GraphEscrowStorageV1Storage { // -- Constructor -- - constructor(address _graphToken, address _graphPayments, uint256 _withdrawEscrowThawingPeriod) { - graphToken = IGraphToken(_graphToken); - graphPayments = IGraphPayments(_graphPayments); - _withdrawEscrowThawingPeriod = _withdrawEscrowThawingPeriod; + constructor(address _controller, uint256 _withdrawEscrowThawingPeriod) GraphDirectory(_controller) { + withdrawEscrowThawingPeriod = _withdrawEscrowThawingPeriod; } // Deposit funds into the escrow for a receiver - function deposit(address receiver, uint256 amount) external {} + function deposit(address receiver, uint256 amount) external { + escrowAccounts[msg.sender][receiver].balance += amount; + graphToken.transferFrom(msg.sender, address(this), amount); + emit Deposit(msg.sender, receiver, amount); + } // Deposit funds into the escrow for multiple receivers function depositMany(address[] calldata receivers, uint256[] calldata amounts) external {} diff --git a/packages/horizon/contracts/GraphPayments.sol b/packages/horizon/contracts/GraphPayments.sol index 7131d7abd..6990518d3 100644 --- a/packages/horizon/contracts/GraphPayments.sol +++ b/packages/horizon/contracts/GraphPayments.sol @@ -8,7 +8,7 @@ import { IGraphPayments } from "./interfaces/IGraphPayments.sol"; import { IGraphEscrow } from "./interfaces/IGraphEscrow.sol"; import { GraphPaymentsStorageV1Storage } from "./GraphPaymentsStorage.sol"; -contract GraphEscrow is IGraphPayments, GraphPaymentsStorageV1Storage { +contract GraphPayments is IGraphPayments, GraphPaymentsStorageV1Storage { // -- Errors -- // -- Immutable variables -- @@ -44,7 +44,7 @@ contract GraphEscrow is IGraphPayments, GraphPaymentsStorageV1Storage { address sender, address receiver, uint256 amount, - uint256 paymentType, + IGraphPayments.PaymentType paymentType, uint256 dataServiceCut ) external {} } diff --git a/packages/horizon/contracts/SimpleTest.sol b/packages/horizon/contracts/SimpleTest.sol deleted file mode 100644 index 5cc28d9cc..000000000 --- a/packages/horizon/contracts/SimpleTest.sol +++ /dev/null @@ -1,10 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0 -pragma solidity >=0.4.0 <0.9.0; - -import { Test } from "@graphprotocol/contracts/contracts/staking/IHorizonStaking.sol"; - -contract SimpleTest is Test { - function test() external pure returns (uint256) { - return 42; - } -} diff --git a/packages/horizon/contracts/interfaces/IGraphPayments.sol b/packages/horizon/contracts/interfaces/IGraphPayments.sol index b008878fb..ebc05ef34 100644 --- a/packages/horizon/contracts/interfaces/IGraphPayments.sol +++ b/packages/horizon/contracts/interfaces/IGraphPayments.sol @@ -4,9 +4,8 @@ pragma solidity ^0.8.24; interface IGraphPayments { // Payment types enum PaymentType { - Null, - Indexing, - Query + IndexingFees, + QueryFees } // approve a data service to collect funds diff --git a/packages/horizon/lib/forge-std b/packages/horizon/lib/forge-std index bb4ceea94..e4aef94c1 160000 --- a/packages/horizon/lib/forge-std +++ b/packages/horizon/lib/forge-std @@ -1 +1 @@ -Subproject commit bb4ceea94d6f10eeb5b41dc2391c6c8bf8e734ef +Subproject commit e4aef94c1768803a16fe19f7ce8b65defd027cfd diff --git a/packages/horizon/package.json b/packages/horizon/package.json index ec23f62cb..59dcaee89 100644 --- a/packages/horizon/package.json +++ b/packages/horizon/package.json @@ -20,6 +20,7 @@ "@nomicfoundation/hardhat-network-helpers": "^1.0.0", "@nomicfoundation/hardhat-toolbox": "^4.0.0", "@nomicfoundation/hardhat-verify": "^2.0.0", + "@openzeppelin/contracts": "^5.0.2", "@typechain/ethers-v6": "^0.5.0", "@typechain/hardhat": "^9.0.0", "@types/chai": "^4.2.0", diff --git a/packages/horizon/test/GraphEscrow.t.sol b/packages/horizon/test/GraphEscrow.t.sol new file mode 100644 index 000000000..2f77dbfbc --- /dev/null +++ b/packages/horizon/test/GraphEscrow.t.sol @@ -0,0 +1,54 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +import "forge-std/Test.sol"; + +import { Controller } from "@graphprotocol/contracts/contracts/governance/Controller.sol"; + +import { GraphEscrow } from "contracts/GraphEscrow.sol"; +import { GraphPayments } from "contracts/GraphPayments.sol"; + +import "./mocks/MockGRTToken.sol"; + +contract GraphEscrowTest is Test { + GraphEscrow escrow; + + Controller controller; + MockGRTToken token; + + address governor = address(0xA1); + uint256 initialSupply = 1000000 ether; + uint256 withdrawEscrowThawingPeriod = 60; + + address sender; + address receiver; + + // Setup + + function setUp() public { + vm.prank(governor); + controller = new Controller(); + token = new MockGRTToken(); + + vm.startPrank(governor); + controller.setContractProxy(keccak256("GraphToken"), address(token)); + vm.stopPrank(); + + escrow = new GraphEscrow(address(controller), withdrawEscrowThawingPeriod); + + sender = address(0xB1); + receiver = address(0xB2); + } + + function testDeposit() public { + token.mint(sender, 10000 ether); + vm.startPrank(sender); + token.approve(address(escrow), 1000 ether); + escrow.deposit(receiver, 1000 ether); + vm.stopPrank(); + + + (uint256 receiverEscrowBalance,,) = escrow.escrowAccounts(sender, receiver); + assertEq(receiverEscrowBalance, 1000 ether); + } +} \ No newline at end of file diff --git a/packages/horizon/test/SimpleTest.t.sol b/packages/horizon/test/SimpleTest.t.sol deleted file mode 100644 index b130e5d34..000000000 --- a/packages/horizon/test/SimpleTest.t.sol +++ /dev/null @@ -1,17 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0 -pragma solidity 0.8.10; - -import "forge-std/Test.sol"; -import { SimpleTest } from "../contracts/SimpleTest.sol"; - -contract ContractTest is Test { - SimpleTest simpleTest; - - function setUp() public { - simpleTest = new SimpleTest(); - } - - function test_NumberIs42() public { - assertEq(simpleTest.test(), 42); - } -} diff --git a/packages/horizon/test/SimpleTest.ts b/packages/horizon/test/SimpleTest.ts deleted file mode 100644 index cfcfb1443..000000000 --- a/packages/horizon/test/SimpleTest.ts +++ /dev/null @@ -1,23 +0,0 @@ -import hardhat from 'hardhat' - -import { expect } from 'chai' -import { loadFixture } from '@nomicfoundation/hardhat-toolbox/network-helpers' - -const ethers = hardhat.ethers - -describe('SimpleTest', function () { - async function deployFixture() { - const [owner] = await ethers.getSigners() - const SimpleTest = await ethers.getContractFactory('SimpleTest') - const simpleTest = await SimpleTest.deploy() - return { simpleTest, owner } - } - - describe('Deployment', function () { - it('Should return 42', async function () { - const { simpleTest } = await loadFixture(deployFixture) - - expect(await simpleTest.test()).to.equal(42) - }) - }) -}) diff --git a/packages/horizon/test/mocks/MockGRTToken.sol b/packages/horizon/test/mocks/MockGRTToken.sol new file mode 100644 index 000000000..11cf5c0f8 --- /dev/null +++ b/packages/horizon/test/mocks/MockGRTToken.sol @@ -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) {} +} \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index 27dfd84b0..00e728326 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2935,6 +2935,7 @@ __metadata: "@nomicfoundation/hardhat-network-helpers": "npm:^1.0.0" "@nomicfoundation/hardhat-toolbox": "npm:^4.0.0" "@nomicfoundation/hardhat-verify": "npm:^2.0.0" + "@openzeppelin/contracts": "npm:^5.0.2" "@typechain/ethers-v6": "npm:^0.5.0" "@typechain/hardhat": "npm:^9.0.0" "@types/chai": "npm:^4.2.0" @@ -5044,6 +5045,13 @@ __metadata: languageName: node linkType: hard +"@openzeppelin/contracts@npm:^5.0.2": + version: 5.0.2 + resolution: "@openzeppelin/contracts@npm:5.0.2" + checksum: d042661db7bb2f3a4b9ef30bba332e86ac20907d171f2ebfccdc9255cc69b62786fead8d6904b8148a8f26946bc7c15eead91b95f75db0c193a99d52e528663e + languageName: node + linkType: hard + "@openzeppelin/defender-admin-client@npm:^1.46.0": version: 1.54.1 resolution: "@openzeppelin/defender-admin-client@npm:1.54.1" From 7f1431cceaae1b1aec77546794aff8cd476ac468 Mon Sep 17 00:00:00 2001 From: Miguel de Elias Date: Wed, 17 Apr 2024 12:26:10 -0300 Subject: [PATCH 05/15] chore: add GraphEscrow implementation --- packages/horizon/contracts/GraphEscrow.sol | 96 +++++++++++++++++++++- packages/horizon/test/GraphEscrow.t.sol | 72 +++++++++++++++- 2 files changed, 163 insertions(+), 5 deletions(-) diff --git a/packages/horizon/contracts/GraphEscrow.sol b/packages/horizon/contracts/GraphEscrow.sol index b2acd630c..afb23d97b 100644 --- a/packages/horizon/contracts/GraphEscrow.sol +++ b/packages/horizon/contracts/GraphEscrow.sol @@ -12,10 +12,25 @@ contract GraphEscrow is IGraphEscrow, GraphEscrowStorageV1Storage, GraphDirector // -- Errors -- error GraphEscrowNotGraphPayments(); + error GraphEscrowInputsLengthMismatch(); + error GraphEscrowInsufficientThawAmount(); + error GraphEscrowInsufficientAmount(uint256 available, uint256 required); + error GraphEscrowNotThawing(); + error GraphEscrowStillThawing(uint256 currentTimestamp, uint256 thawEndTimestamp); // -- Events -- 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); // -- Modifier -- @@ -40,14 +55,87 @@ contract GraphEscrow is IGraphEscrow, GraphEscrowStorageV1Storage, GraphDirector } // Deposit funds into the escrow for multiple receivers - function depositMany(address[] calldata receivers, uint256[] calldata amounts) external {} + 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); + } + + graphToken.transferFrom(msg.sender, address(this), totalAmount); + } // Requests to thaw a specific amount of escrow from a receiver's escrow account - function thaw(address receiver, uint256 amount) external {} + 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 {} + 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; + graphToken.transfer(msg.sender, amount); + emit Withdraw(msg.sender, receiver, amount); + } // Collect from escrow (up to amount available in escrow) for a receiver using sender's deposit - function collect(address sender, address receiver, uint256 amount) external onlyGraphPayments {} + function collect(address sender, address receiver, uint256 amount) external onlyGraphPayments { + EscrowAccount storage account = escrowAccounts[sender][receiver]; + uint256 available = account.balance - account.amountThawing; + + // TODO: should we revert if not enough funds are available? + uint256 collectAmount = amount > available ? available : amount; + + account.balance -= collectAmount; + graphToken.transfer(msg.sender, collectAmount); + emit Collect(sender, receiver, collectAmount); + } } diff --git a/packages/horizon/test/GraphEscrow.t.sol b/packages/horizon/test/GraphEscrow.t.sol index 2f77dbfbc..662b30d64 100644 --- a/packages/horizon/test/GraphEscrow.t.sol +++ b/packages/horizon/test/GraphEscrow.t.sol @@ -15,8 +15,9 @@ contract GraphEscrowTest is Test { Controller controller; MockGRTToken token; + address graphPayments = address(0xA1); - address governor = address(0xA1); + address governor = address(0xA2); uint256 initialSupply = 1000000 ether; uint256 withdrawEscrowThawingPeriod = 60; @@ -32,6 +33,7 @@ contract GraphEscrowTest is Test { vm.startPrank(governor); controller.setContractProxy(keccak256("GraphToken"), address(token)); + controller.setContractProxy(keccak256("GraphPayments"), graphPayments); vm.stopPrank(); escrow = new GraphEscrow(address(controller), withdrawEscrowThawingPeriod); @@ -47,8 +49,76 @@ contract GraphEscrowTest is Test { escrow.deposit(receiver, 1000 ether); vm.stopPrank(); + (uint256 receiverEscrowBalance,,) = escrow.escrowAccounts(sender, receiver); + assertEq(receiverEscrowBalance, 1000 ether); + } + + function testDepositMany() public { + address otherReceiver = address(0xB3); + address[] memory receivers = new address[](2); + receivers[0] = receiver; + receivers[1] = otherReceiver; + + uint256[] memory amounts = new uint256[](2); + amounts[0] = 1000 ether; + amounts[1] = 2000 ether; + + token.mint(sender, 3000 ether); + vm.startPrank(sender); + token.approve(address(escrow), 3000 ether); + escrow.depositMany(receivers, amounts); + vm.stopPrank(); (uint256 receiverEscrowBalance,,) = escrow.escrowAccounts(sender, receiver); assertEq(receiverEscrowBalance, 1000 ether); + + (uint256 otherReceiverEscrowBalance,,) = escrow.escrowAccounts(sender, otherReceiver); + assertEq(otherReceiverEscrowBalance, 2000 ether); + } + + function testThaw() public { + token.mint(sender, 1000 ether); + vm.startPrank(sender); + token.approve(address(escrow), 1000 ether); + escrow.deposit(receiver, 1000 ether); + escrow.thaw(receiver, 100 ether); + vm.stopPrank(); + + (, uint256 amountThawing,uint256 thawEndTimestamp) = escrow.escrowAccounts(sender, receiver); + assertEq(amountThawing, 100 ether); + assertEq(thawEndTimestamp, block.timestamp + withdrawEscrowThawingPeriod); + } + + function testWithdraw() public { + token.mint(sender, 1000 ether); + vm.startPrank(sender); + token.approve(address(escrow), 1000 ether); + escrow.deposit(receiver, 1000 ether); + escrow.thaw(receiver, 100 ether); + + // advance time + skip(withdrawEscrowThawingPeriod + 1); + + escrow.withdraw(receiver); + vm.stopPrank(); + + (uint256 receiverEscrowBalance,,) = escrow.escrowAccounts(sender, receiver); + assertEq(receiverEscrowBalance, 900 ether); + } + + function testCollect() public { + token.mint(sender, 1000 ether); + vm.startPrank(sender); + token.approve(address(escrow), 1000 ether); + escrow.deposit(receiver, 1000 ether); + vm.stopPrank(); + + vm.prank(graphPayments); + escrow.collect(sender, receiver, 100 ether); + + (uint256 receiverEscrowBalance,,) = escrow.escrowAccounts(sender, receiver); + assertEq(receiverEscrowBalance, 900 ether); + uint256 graphPaymentsBalance = token.balanceOf(graphPayments); + assertEq(graphPaymentsBalance, 100 ether); } } \ No newline at end of file From 30bbbc6b39cdf77e1055b5e9b3eda1b9103670f4 Mon Sep 17 00:00:00 2001 From: Miguel de Elias Date: Thu, 18 Apr 2024 13:56:41 -0300 Subject: [PATCH 06/15] chore: add GraphPayments implementation --- .../contracts/staking/IHorizonStaking.sol | 2 + packages/horizon/contracts/GraphPayments.sol | 91 ++++++++++-- .../contracts/GraphPaymentsStorage.sol | 3 +- .../contracts/interfaces/IGraphPayments.sol | 6 + packages/horizon/foundry.toml | 3 +- packages/horizon/test/GraphDeployments.t.sol | 87 ++++++++++++ packages/horizon/test/GraphEscrow.t.sol | 1 - packages/horizon/test/GraphPayments.t.sol | 133 ++++++++++++++++++ .../horizon/test/mocks/MockHorizonStaking.sol | 43 ++++++ 9 files changed, 351 insertions(+), 18 deletions(-) create mode 100644 packages/horizon/test/GraphDeployments.t.sol create mode 100644 packages/horizon/test/GraphPayments.t.sol create mode 100644 packages/horizon/test/mocks/MockHorizonStaking.sol diff --git a/packages/contracts/contracts/staking/IHorizonStaking.sol b/packages/contracts/contracts/staking/IHorizonStaking.sol index a4101ff52..f6bf1dd87 100644 --- a/packages/contracts/contracts/staking/IHorizonStaking.sol +++ b/packages/contracts/contracts/staking/IHorizonStaking.sol @@ -146,4 +146,6 @@ interface IHorizonStaking { function getServiceProvider(address serviceProvider) external view returns (ServiceProvider memory); function getProvision(bytes32 provision) external view returns (Provision memory); + + function getDelegatorCut(address serviceProvider, uint256256 paymentType) external returns (address, uint256); } diff --git a/packages/horizon/contracts/GraphPayments.sol b/packages/horizon/contracts/GraphPayments.sol index 6990518d3..93c80c7bc 100644 --- a/packages/horizon/contracts/GraphPayments.sol +++ b/packages/horizon/contracts/GraphPayments.sol @@ -6,45 +6,106 @@ import { IHorizonStaking } from "@graphprotocol/contracts/contracts/staking/IHor import { IGraphPayments } from "./interfaces/IGraphPayments.sol"; import { IGraphEscrow } from "./interfaces/IGraphEscrow.sol"; +import { GraphDirectory } from "./GraphDirectory.sol"; import { GraphPaymentsStorageV1Storage } from "./GraphPaymentsStorage.sol"; -contract GraphPayments is IGraphPayments, GraphPaymentsStorageV1Storage { +contract GraphPayments is IGraphPayments, GraphPaymentsStorageV1Storage, GraphDirectory { // -- Errors -- - // -- Immutable variables -- + error GraphPaymentsNotThawing(); + error GraphPaymentsStillThawing(uint256 currentTimestamp, uint256 thawEndTimestamp); + error GraphPaymentsCollectorNotAuthorized(address sender, address dataService); - IGraphToken public immutable graphToken; - IHorizonStaking public immutable staking; - IGraphEscrow public immutable graphEscrow; + // -- 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); // -- Modifier -- + // -- Parameters -- + + uint256 private immutable MAX_PPM = 1000000; // 100% in parts per million + // -- Constructor -- - constructor(address _graphToken, address _staking, address _graphEscrow) { - graphToken = IGraphToken(_graphToken); - staking = IHorizonStaking(_staking); - graphEscrow = IGraphEscrow(_graphEscrow); + constructor( + address _controller, + uint256 _revokeCollectorThawingPeriod, + uint256 _protocolPaymentCut + ) GraphDirectory(_controller) { + revokeCollectorThawingPeriod = _revokeCollectorThawingPeriod; + protocolPaymentCut = _protocolPaymentCut; } // approve a data service to collect funds - function approveCollector(address dataService) external {} + function approveCollector(address dataService) external { + authorizedCollectors[msg.sender][dataService].authorized = true; + emit AuthorizedCollector(msg.sender, dataService); + } // thaw a data service's collector authorization - function thawCollector(address dataService) external {} + 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 {} + function cancelThawCollector(address dataService) external { + authorizedCollectors[msg.sender][dataService].thawEndTimestamp = 0; + emit CancelThawCollector(msg.sender, dataService); + } // revoke authorized collector - function revokeCollector(address dataService) external {} + function revokeCollector(address dataService) external { + Collector storage collector = authorizedCollectors[msg.sender][dataService]; + + if (collector.thawEndTimestamp == 0) { + revert GraphPaymentsNotThawing(); + } + + if (collector.thawEndTimestamp > block.timestamp) { + revert GraphPaymentsStillThawing(block.timestamp, collector.thawEndTimestamp); + } + + delete authorizedCollectors[msg.sender][dataService]; + emit RevokeCollector(msg.sender, dataService); + } // collect funds from a sender, pay cuts and forward the rest to the receiver function collect( address sender, - address receiver, + address receiver, // serviceProvider uint256 amount, IGraphPayments.PaymentType paymentType, uint256 dataServiceCut - ) external {} + ) external { + Collector storage collector = authorizedCollectors[sender][msg.sender]; + + if (!collector.authorized) { + revert GraphPaymentsCollectorNotAuthorized(sender, msg.sender); + } + + // Collect tokens from GraphEscrow + graphEscrow.collect(sender, receiver, amount); + + // Pay protocol cut + uint256 protocolCut = (amount * protocolPaymentCut) / MAX_PPM; + graphToken.burn(protocolCut); + + // Pay data service cut + uint256 dataServicePayment = (amount * dataServiceCut) / MAX_PPM; + graphToken.transfer(msg.sender, dataServicePayment); + + // Get delegation cut + (address delegatorAddress, uint256 delegatorCut) = graphStaking.getDelegatorCut(receiver, uint256256(paymentType)); + uint256 delegatorPayment = (amount * delegatorCut) / MAX_PPM; + graphToken.transfer(delegatorAddress, delegatorPayment); + + // Pay the rest to the receiver + uint256 receiverPayment = amount - protocolCut - dataServicePayment - delegatorPayment; + graphToken.transfer(receiver, receiverPayment); + } } diff --git a/packages/horizon/contracts/GraphPaymentsStorage.sol b/packages/horizon/contracts/GraphPaymentsStorage.sol index ebf732f70..e649aea0c 100644 --- a/packages/horizon/contracts/GraphPaymentsStorage.sol +++ b/packages/horizon/contracts/GraphPaymentsStorage.sol @@ -5,7 +5,8 @@ import { IGraphPayments } from "./interfaces/IGraphPayments.sol"; contract GraphPaymentsStorageV1Storage { // Authorized collectors - mapping(address sender => mapping(address collector => uint256 thawEndTimestamp)) public authorizedCollectors; + mapping(address sender => mapping(address dataService => IGraphPayments.Collector collector)) + public authorizedCollectors; // The maximum thawing period (in seconds) for removing collector authorization // This is a precautionary measure to avoid inadvertedly locking collectors for too long diff --git a/packages/horizon/contracts/interfaces/IGraphPayments.sol b/packages/horizon/contracts/interfaces/IGraphPayments.sol index ebc05ef34..5d5fe4ca9 100644 --- a/packages/horizon/contracts/interfaces/IGraphPayments.sol +++ b/packages/horizon/contracts/interfaces/IGraphPayments.sol @@ -8,6 +8,12 @@ interface IGraphPayments { QueryFees } + // Authorized collector + struct Collector { + bool authorized; + uint256 thawEndTimestamp; + } + // approve a data service to collect funds function approveCollector(address dataService) external; diff --git a/packages/horizon/foundry.toml b/packages/horizon/foundry.toml index 55f7ffd31..3582d5476 100644 --- a/packages/horizon/foundry.toml +++ b/packages/horizon/foundry.toml @@ -3,4 +3,5 @@ src = 'contracts' out = 'build' libs = ['node_modules', 'lib'] test = 'test' -cache_path = 'cache_forge' \ No newline at end of file +cache_path = 'cache_forge' +fs_permissions = [{ access = "read", path = "./"}] \ No newline at end of file diff --git a/packages/horizon/test/GraphDeployments.t.sol b/packages/horizon/test/GraphDeployments.t.sol new file mode 100644 index 000000000..2b4a1af2f --- /dev/null +++ b/packages/horizon/test/GraphDeployments.t.sol @@ -0,0 +1,87 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +import "forge-std/Test.sol"; + +import { Controller } from "@graphprotocol/contracts/contracts/governance/Controller.sol"; + +import { GraphEscrow } from "contracts/GraphEscrow.sol"; +import { GraphPayments } from "contracts/GraphPayments.sol"; +import { IGraphPayments } from "contracts/interfaces/IGraphPayments.sol"; + +import "./mocks/MockHorizonStaking.sol"; +import "./mocks/MockGRTToken.sol"; + +contract GraphDeployments is Test { + Controller public controller; + MockGRTToken public token; + GraphPayments public payments; + GraphEscrow public escrow; + MockHorizonStaking public staking; + + address public governor = address(0x4b39f53A1084b3eC5f2dA8f3B616D11317572efE); + address public deployer = address(0x54705f06556182AeFfFe40386E85D3921c236f78); + + // GraphEscrow parameters + + uint256 public withdrawEscrowThawingPeriod = 60; + + // GraphPayments parameters + + uint256 public revokeCollectorThawingPeriod = 60; + uint256 public protocolPaymentCut = 10000; // 1% + + // Staking parameters + + address public delegatorAddress = address(0xbdC1c9f94A729E47d6877217eC995916EAD457B7); + uint256 public delegatorCut = 50000; // 5% + + // Setup + + constructor() { + setUp(); + } + + function setUp() public { + vm.prank(governor); + controller = new Controller(); + + // GraphPayments preddict address + bytes32 saltPayments = keccak256("GraphPaymentsSalt"); + bytes32 paymentsHash = keccak256(bytes.concat(vm.getCode("GraphPayments.sol:GraphPayments"), abi.encode(address(controller), revokeCollectorThawingPeriod, protocolPaymentCut))); + address predictedPaymentsAddress = vm.computeCreate2Address(saltPayments, paymentsHash, deployer); + + // GraphEscrow preddict address + bytes32 saltEscrow = keccak256("GraphEscrowSalt"); + bytes32 escrowHash = keccak256(bytes.concat(vm.getCode("GraphEscrow.sol:GraphEscrow"), abi.encode(address(controller), withdrawEscrowThawingPeriod))); + address predictedAddressEscrow = vm.computeCreate2Address(saltEscrow, escrowHash, deployer); + + // GraphToken + vm.prank(deployer); + token = new MockGRTToken(); + + // HorizonStaking + vm.prank(deployer); + staking = new MockHorizonStaking(delegatorAddress, delegatorCut); + + // Setup controller + vm.startPrank(governor); + controller.setContractProxy(keccak256("GraphToken"), address(token)); + controller.setContractProxy(keccak256("GraphEscrow"), predictedAddressEscrow); + controller.setContractProxy(keccak256("GraphPayments"), predictedPaymentsAddress); + controller.setContractProxy(keccak256("Staking"), address(staking)); + vm.stopPrank(); + + vm.startPrank(deployer); + payments = new GraphPayments{salt: saltPayments}(address(controller), revokeCollectorThawingPeriod, protocolPaymentCut); + escrow = new GraphEscrow{salt: saltEscrow}(address(controller), withdrawEscrowThawingPeriod); + vm.stopPrank(); + } + + // Tests + + function testDeployments() public view { + assertEq(address(escrow.graphPayments()), address(payments)); + assertEq(address(payments.graphEscrow()), address(escrow)); + } +} \ No newline at end of file diff --git a/packages/horizon/test/GraphEscrow.t.sol b/packages/horizon/test/GraphEscrow.t.sol index 662b30d64..9ba34820d 100644 --- a/packages/horizon/test/GraphEscrow.t.sol +++ b/packages/horizon/test/GraphEscrow.t.sol @@ -18,7 +18,6 @@ contract GraphEscrowTest is Test { address graphPayments = address(0xA1); address governor = address(0xA2); - uint256 initialSupply = 1000000 ether; uint256 withdrawEscrowThawingPeriod = 60; address sender; diff --git a/packages/horizon/test/GraphPayments.t.sol b/packages/horizon/test/GraphPayments.t.sol new file mode 100644 index 000000000..b97b3b2bf --- /dev/null +++ b/packages/horizon/test/GraphPayments.t.sol @@ -0,0 +1,133 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +import "forge-std/Test.sol"; + +import { Controller } from "@graphprotocol/contracts/contracts/governance/Controller.sol"; + +import { GraphEscrow } from "contracts/GraphEscrow.sol"; +import { GraphPayments } from "contracts/GraphPayments.sol"; +import { IGraphPayments } from "contracts/interfaces/IGraphPayments.sol"; + +import "./GraphDeployments.t.sol"; +import "./mocks/MockGRTToken.sol"; + +contract GraphPaymentsTest is Test { + GraphDeployments deployments; + GraphPayments payments; + Controller controller; + MockGRTToken token; + GraphEscrow escrow; + + address governor; + + uint256 revokeCollectorThawingPeriod; + uint256 protocolPaymentCut; + + address delegatorAddress; + uint256 delegatorCut; + + address sender; + address dataService; + + // Setup + + function setUp() public { + deployments = new GraphDeployments(); + + payments = deployments.payments(); + controller = deployments.controller(); + token = deployments.token(); + escrow = deployments.escrow(); + governor = deployments.governor(); + + revokeCollectorThawingPeriod = deployments.revokeCollectorThawingPeriod(); + protocolPaymentCut = deployments.protocolPaymentCut(); + + delegatorAddress = deployments.delegatorAddress(); + delegatorCut = deployments.delegatorCut(); + + sender = address(0xA1); + dataService = address(0xA2); + } + + // Tests + + function testApproveCollector() public { + vm.prank(sender); + payments.approveCollector(dataService); + + (bool authorized, uint256 thawEndTimestamp) = payments.authorizedCollectors(sender, dataService); + assertEq(authorized, true); + assertEq(thawEndTimestamp, 0); + } + + function testThawCollector() public { + vm.startPrank(sender); + payments.approveCollector(dataService); + payments.thawCollector(dataService); + vm.stopPrank(); + + (bool authorized, uint256 thawEndTimestamp) = payments.authorizedCollectors(sender, dataService); + assertEq(authorized, true); + assertEq(thawEndTimestamp, block.timestamp + revokeCollectorThawingPeriod); + } + + function testCancelThawCollector() public { + vm.startPrank(sender); + payments.approveCollector(dataService); + payments.thawCollector(dataService); + vm.stopPrank(); + + (bool authorized, uint256 thawEndTimestamp) = payments.authorizedCollectors(sender, dataService); + assertEq(authorized, true); + assertEq(thawEndTimestamp, block.timestamp + revokeCollectorThawingPeriod); + + vm.prank(sender); + payments.cancelThawCollector(dataService); + + (authorized, thawEndTimestamp) = payments.authorizedCollectors(sender, dataService); + assertEq(authorized, true); + assertEq(thawEndTimestamp, 0); + } + + function testRevokeCollector() public { + vm.startPrank(sender); + payments.approveCollector(dataService); + payments.thawCollector(dataService); + skip(revokeCollectorThawingPeriod + 1); + payments.revokeCollector(dataService); + vm.stopPrank(); + + (bool authorized,) = payments.authorizedCollectors(sender, dataService); + assertEq(authorized, false); + } + + function testCollect() public { + vm.prank(sender); + payments.approveCollector(dataService); + + address indexer = address(0xA3); + uint256 amount = 1000 ether; + + token.mint(sender, amount); + vm.startPrank(sender); + token.approve(address(escrow), amount); + escrow.deposit(indexer, amount); + vm.stopPrank(); + + vm.startPrank(dataService); + uint256 dataServiceCut = 30000; // 3% + payments.collect(sender, indexer, amount, IGraphPayments.PaymentType.IndexingFees, dataServiceCut); + vm.stopPrank(); + + uint256 indexerBalance = token.balanceOf(indexer); + assertEq(indexerBalance, 910 ether); + + uint256 dataServiceBalance = token.balanceOf(dataService); + assertEq(dataServiceBalance, 30 ether); + + uint256 delegatorBalance = token.balanceOf(delegatorAddress); + assertEq(delegatorBalance, 50 ether); + } +} \ No newline at end of file diff --git a/packages/horizon/test/mocks/MockHorizonStaking.sol b/packages/horizon/test/mocks/MockHorizonStaking.sol new file mode 100644 index 000000000..40eed6a4e --- /dev/null +++ b/packages/horizon/test/mocks/MockHorizonStaking.sol @@ -0,0 +1,43 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +import "forge-std/Test.sol"; + +import { IHorizonStaking } from "@graphprotocol/contracts/contracts/staking/IHorizonStaking.sol"; +import { MockGRTToken } from "./MockGRTToken.sol"; + +contract MockHorizonStaking is IHorizonStaking { + address public delegatorAddress; + uint256 public delegatorCut; + + constructor(address _delegatorAddress, uint256 _delegatorCut) { + delegatorAddress = _delegatorAddress; + delegatorCut = _delegatorCut; + } + + function allowVerifier(address verifier, bool allow) external {} + function stake(uint256 tokens) external {} + function provision(uint256 tokens, address verifier, uint256 maxVerifierCut, uint256 thawingPeriod) external {} + function thaw(bytes32 provisionId, uint256 tokens) external returns (bytes32 thawRequestId) {} + function deprovision(bytes32 thawRequestId) external {} + function reprovision(bytes32 thawRequestId, bytes32 provisionId) external {} + function withdraw(bytes32 thawRequestId) external {} + function delegate(address serviceProvider, uint256 tokens) external {} + function undelegate( + address serviceProvider, + uint256 tokens, + bytes32[] calldata provisions + ) external returns (bytes32 thawRequestId) {} + function slash(bytes32 provisionId, uint256 tokens, uint256 verifierAmount) external {} + function setForceThawProvisions(bytes32[] calldata provisions) external {} + function getStake(address serviceProvider) external view returns (uint256 tokens) {} + function getIdleStake(address serviceProvider) external view returns (uint256 tokens) {} + function getCapacity(address serviceProvider) external view returns (uint256 tokens) {} + function getTokensAvailable(bytes32 provision) external view returns (uint256 tokens) {} + function getServiceProvider(address serviceProvider) external view returns (ServiceProvider memory) {} + function getProvision(bytes32 provision) external view returns (Provision memory) {} + + function getDelegatorCut(address serviceProvider, uint paymentType) external returns (address, uint256) { + return (delegatorAddress, delegatorCut); + } +} \ No newline at end of file From 9b6ed9bcfd20b3707a518921af0dcf0e34ddf245 Mon Sep 17 00:00:00 2001 From: Miguel de Elias Date: Thu, 18 Apr 2024 16:46:54 -0300 Subject: [PATCH 07/15] chore: use GraphDeployments on GraphEscrow --- packages/horizon/contracts/GraphPayments.sol | 2 +- packages/horizon/test/GraphEscrow.t.sol | 21 ++++++++++--------- .../horizon/test/mocks/MockHorizonStaking.sol | 2 +- 3 files changed, 13 insertions(+), 12 deletions(-) diff --git a/packages/horizon/contracts/GraphPayments.sol b/packages/horizon/contracts/GraphPayments.sol index 93c80c7bc..df51dc6f7 100644 --- a/packages/horizon/contracts/GraphPayments.sol +++ b/packages/horizon/contracts/GraphPayments.sol @@ -100,7 +100,7 @@ contract GraphPayments is IGraphPayments, GraphPaymentsStorageV1Storage, GraphDi graphToken.transfer(msg.sender, dataServicePayment); // Get delegation cut - (address delegatorAddress, uint256 delegatorCut) = graphStaking.getDelegatorCut(receiver, uint256256(paymentType)); + (address delegatorAddress, uint256 delegatorCut) = graphStaking.getDelegatorCut(receiver, uint256(paymentType)); uint256 delegatorPayment = (amount * delegatorCut) / MAX_PPM; graphToken.transfer(delegatorAddress, delegatorPayment); diff --git a/packages/horizon/test/GraphEscrow.t.sol b/packages/horizon/test/GraphEscrow.t.sol index 9ba34820d..ae8814971 100644 --- a/packages/horizon/test/GraphEscrow.t.sol +++ b/packages/horizon/test/GraphEscrow.t.sol @@ -8,14 +8,15 @@ import { Controller } from "@graphprotocol/contracts/contracts/governance/Contro import { GraphEscrow } from "contracts/GraphEscrow.sol"; import { GraphPayments } from "contracts/GraphPayments.sol"; +import "./GraphDeployments.t.sol"; import "./mocks/MockGRTToken.sol"; contract GraphEscrowTest is Test { + GraphDeployments deployments; GraphEscrow escrow; - Controller controller; MockGRTToken token; - address graphPayments = address(0xA1); + GraphPayments payments; address governor = address(0xA2); uint256 withdrawEscrowThawingPeriod = 60; @@ -26,16 +27,15 @@ contract GraphEscrowTest is Test { // Setup function setUp() public { - vm.prank(governor); - controller = new Controller(); - token = new MockGRTToken(); + deployments = new GraphDeployments(); - vm.startPrank(governor); - controller.setContractProxy(keccak256("GraphToken"), address(token)); - controller.setContractProxy(keccak256("GraphPayments"), graphPayments); - vm.stopPrank(); + controller = deployments.controller(); + token = deployments.token(); + escrow = deployments.escrow(); + payments = deployments.payments(); - escrow = new GraphEscrow(address(controller), withdrawEscrowThawingPeriod); + governor = deployments.governor(); + withdrawEscrowThawingPeriod = deployments.withdrawEscrowThawingPeriod(); sender = address(0xB1); receiver = address(0xB2); @@ -112,6 +112,7 @@ contract GraphEscrowTest is Test { escrow.deposit(receiver, 1000 ether); vm.stopPrank(); + address graphPayments = address(payments); vm.prank(graphPayments); escrow.collect(sender, receiver, 100 ether); diff --git a/packages/horizon/test/mocks/MockHorizonStaking.sol b/packages/horizon/test/mocks/MockHorizonStaking.sol index 40eed6a4e..0d97d0a72 100644 --- a/packages/horizon/test/mocks/MockHorizonStaking.sol +++ b/packages/horizon/test/mocks/MockHorizonStaking.sol @@ -37,7 +37,7 @@ contract MockHorizonStaking is IHorizonStaking { function getServiceProvider(address serviceProvider) external view returns (ServiceProvider memory) {} function getProvision(bytes32 provision) external view returns (Provision memory) {} - function getDelegatorCut(address serviceProvider, uint paymentType) external returns (address, uint256) { + function getDelegatorCut(address serviceProvider, uint256 paymentType) external returns (address, uint256) { return (delegatorAddress, delegatorCut); } } \ No newline at end of file From 76539f7b20f018297bc58f5005567d73201e20fe Mon Sep 17 00:00:00 2001 From: Miguel de Elias Date: Fri, 19 Apr 2024 15:40:16 -0300 Subject: [PATCH 08/15] fix: change staking interface --- .../contracts/contracts/staking/IHorizonStaking.sol | 2 +- packages/horizon/contracts/GraphPayments.sol | 6 ++++-- .../horizon/contracts/interfaces/IGraphPayments.sol | 2 +- packages/horizon/test/GraphDeployments.t.sol | 3 +-- packages/horizon/test/GraphPayments.t.sol | 11 +++-------- packages/horizon/test/mocks/MockHorizonStaking.sol | 8 +++----- 6 files changed, 13 insertions(+), 19 deletions(-) diff --git a/packages/contracts/contracts/staking/IHorizonStaking.sol b/packages/contracts/contracts/staking/IHorizonStaking.sol index f6bf1dd87..ed9751696 100644 --- a/packages/contracts/contracts/staking/IHorizonStaking.sol +++ b/packages/contracts/contracts/staking/IHorizonStaking.sol @@ -147,5 +147,5 @@ interface IHorizonStaking { function getProvision(bytes32 provision) external view returns (Provision memory); - function getDelegatorCut(address serviceProvider, uint256256 paymentType) external returns (address, uint256); + function getDelegatorCut(address serviceProvider, uint256 paymentType) external returns (uint256 delegatorCut); } diff --git a/packages/horizon/contracts/GraphPayments.sol b/packages/horizon/contracts/GraphPayments.sol index df51dc6f7..3d9091b4a 100644 --- a/packages/horizon/contracts/GraphPayments.sol +++ b/packages/horizon/contracts/GraphPayments.sol @@ -100,9 +100,11 @@ contract GraphPayments is IGraphPayments, GraphPaymentsStorageV1Storage, GraphDi graphToken.transfer(msg.sender, dataServicePayment); // Get delegation cut - (address delegatorAddress, uint256 delegatorCut) = graphStaking.getDelegatorCut(receiver, uint256(paymentType)); + uint256 delegatorCut = graphStaking.getDelegatorCut(receiver, uint256(paymentType)); uint256 delegatorPayment = (amount * delegatorCut) / MAX_PPM; - graphToken.transfer(delegatorAddress, delegatorPayment); + // TODO: Add to delegation pool + // graphStaking.addToDelegationPool(receiver, delegatorPayment); + graphToken.burn(delegatorPayment); // Pay the rest to the receiver uint256 receiverPayment = amount - protocolCut - dataServicePayment - delegatorPayment; diff --git a/packages/horizon/contracts/interfaces/IGraphPayments.sol b/packages/horizon/contracts/interfaces/IGraphPayments.sol index 5d5fe4ca9..34dc48770 100644 --- a/packages/horizon/contracts/interfaces/IGraphPayments.sol +++ b/packages/horizon/contracts/interfaces/IGraphPayments.sol @@ -8,7 +8,7 @@ interface IGraphPayments { QueryFees } - // Authorized collector + // Collector struct Collector { bool authorized; uint256 thawEndTimestamp; diff --git a/packages/horizon/test/GraphDeployments.t.sol b/packages/horizon/test/GraphDeployments.t.sol index 2b4a1af2f..c0e1abf5a 100644 --- a/packages/horizon/test/GraphDeployments.t.sol +++ b/packages/horizon/test/GraphDeployments.t.sol @@ -33,7 +33,6 @@ contract GraphDeployments is Test { // Staking parameters - address public delegatorAddress = address(0xbdC1c9f94A729E47d6877217eC995916EAD457B7); uint256 public delegatorCut = 50000; // 5% // Setup @@ -62,7 +61,7 @@ contract GraphDeployments is Test { // HorizonStaking vm.prank(deployer); - staking = new MockHorizonStaking(delegatorAddress, delegatorCut); + staking = new MockHorizonStaking(delegatorCut); // Setup controller vm.startPrank(governor); diff --git a/packages/horizon/test/GraphPayments.t.sol b/packages/horizon/test/GraphPayments.t.sol index b97b3b2bf..b96aac1a9 100644 --- a/packages/horizon/test/GraphPayments.t.sol +++ b/packages/horizon/test/GraphPayments.t.sol @@ -24,9 +24,6 @@ contract GraphPaymentsTest is Test { uint256 revokeCollectorThawingPeriod; uint256 protocolPaymentCut; - address delegatorAddress; - uint256 delegatorCut; - address sender; address dataService; @@ -44,9 +41,6 @@ contract GraphPaymentsTest is Test { revokeCollectorThawingPeriod = deployments.revokeCollectorThawingPeriod(); protocolPaymentCut = deployments.protocolPaymentCut(); - delegatorAddress = deployments.delegatorAddress(); - delegatorCut = deployments.delegatorCut(); - sender = address(0xA1); dataService = address(0xA2); } @@ -127,7 +121,8 @@ contract GraphPaymentsTest is Test { uint256 dataServiceBalance = token.balanceOf(dataService); assertEq(dataServiceBalance, 30 ether); - uint256 delegatorBalance = token.balanceOf(delegatorAddress); - assertEq(delegatorBalance, 50 ether); + // TODO: test delegator cut payment + // uint256 delegatorBalance = token.balanceOf(delegationPool); + // assertEq(delegatorBalance, 50 ether); } } \ No newline at end of file diff --git a/packages/horizon/test/mocks/MockHorizonStaking.sol b/packages/horizon/test/mocks/MockHorizonStaking.sol index 0d97d0a72..a71beff98 100644 --- a/packages/horizon/test/mocks/MockHorizonStaking.sol +++ b/packages/horizon/test/mocks/MockHorizonStaking.sol @@ -7,11 +7,9 @@ import { IHorizonStaking } from "@graphprotocol/contracts/contracts/staking/IHor import { MockGRTToken } from "./MockGRTToken.sol"; contract MockHorizonStaking is IHorizonStaking { - address public delegatorAddress; uint256 public delegatorCut; - constructor(address _delegatorAddress, uint256 _delegatorCut) { - delegatorAddress = _delegatorAddress; + constructor(uint256 _delegatorCut) { delegatorCut = _delegatorCut; } @@ -37,7 +35,7 @@ contract MockHorizonStaking is IHorizonStaking { function getServiceProvider(address serviceProvider) external view returns (ServiceProvider memory) {} function getProvision(bytes32 provision) external view returns (Provision memory) {} - function getDelegatorCut(address serviceProvider, uint256 paymentType) external returns (address, uint256) { - return (delegatorAddress, delegatorCut); + function getDelegatorCut(address serviceProvider, uint256 paymentType) external returns (uint256) { + return delegatorCut; } } \ No newline at end of file From 914f65faf4e487475fedfc9111e13757d73005f4 Mon Sep 17 00:00:00 2001 From: Miguel de Elias Date: Fri, 19 Apr 2024 16:15:54 -0300 Subject: [PATCH 09/15] chore: use addToDelegationPool --- .../contracts/staking/IHorizonStaking.sol | 3 ++- packages/horizon/contracts/GraphPayments.sol | 6 ++---- packages/horizon/test/GraphDeployments.t.sol | 4 ++-- packages/horizon/test/GraphPayments.t.sol | 8 +++++--- .../horizon/test/mocks/MockHorizonStaking.sol | 16 ++++++++++------ 5 files changed, 21 insertions(+), 16 deletions(-) diff --git a/packages/contracts/contracts/staking/IHorizonStaking.sol b/packages/contracts/contracts/staking/IHorizonStaking.sol index ed9751696..d55520db5 100644 --- a/packages/contracts/contracts/staking/IHorizonStaking.sol +++ b/packages/contracts/contracts/staking/IHorizonStaking.sol @@ -147,5 +147,6 @@ interface IHorizonStaking { function getProvision(bytes32 provision) external view returns (Provision memory); - function getDelegatorCut(address serviceProvider, uint256 paymentType) external returns (uint256 delegatorCut); + function getDelegationCut(address serviceProvider, uint8 paymentType) external view returns (uint256 delegationCut); + function addToDelegationPool(address serviceProvider, uint256 tokens) external; } diff --git a/packages/horizon/contracts/GraphPayments.sol b/packages/horizon/contracts/GraphPayments.sol index 3d9091b4a..553f5b7ce 100644 --- a/packages/horizon/contracts/GraphPayments.sol +++ b/packages/horizon/contracts/GraphPayments.sol @@ -100,11 +100,9 @@ contract GraphPayments is IGraphPayments, GraphPaymentsStorageV1Storage, GraphDi graphToken.transfer(msg.sender, dataServicePayment); // Get delegation cut - uint256 delegatorCut = graphStaking.getDelegatorCut(receiver, uint256(paymentType)); + uint256 delegatorCut = graphStaking.getDelegationCut(receiver, uint8(paymentType)); uint256 delegatorPayment = (amount * delegatorCut) / MAX_PPM; - // TODO: Add to delegation pool - // graphStaking.addToDelegationPool(receiver, delegatorPayment); - graphToken.burn(delegatorPayment); + graphStaking.addToDelegationPool(receiver, delegatorPayment); // Pay the rest to the receiver uint256 receiverPayment = amount - protocolCut - dataServicePayment - delegatorPayment; diff --git a/packages/horizon/test/GraphDeployments.t.sol b/packages/horizon/test/GraphDeployments.t.sol index c0e1abf5a..490445f5b 100644 --- a/packages/horizon/test/GraphDeployments.t.sol +++ b/packages/horizon/test/GraphDeployments.t.sol @@ -33,7 +33,7 @@ contract GraphDeployments is Test { // Staking parameters - uint256 public delegatorCut = 50000; // 5% + uint256 public delegationCut = 50000; // 5% // Setup @@ -61,7 +61,7 @@ contract GraphDeployments is Test { // HorizonStaking vm.prank(deployer); - staking = new MockHorizonStaking(delegatorCut); + staking = new MockHorizonStaking(delegationCut); // Setup controller vm.startPrank(governor); diff --git a/packages/horizon/test/GraphPayments.t.sol b/packages/horizon/test/GraphPayments.t.sol index b96aac1a9..ea9585db0 100644 --- a/packages/horizon/test/GraphPayments.t.sol +++ b/packages/horizon/test/GraphPayments.t.sol @@ -10,6 +10,7 @@ import { GraphPayments } from "contracts/GraphPayments.sol"; import { IGraphPayments } from "contracts/interfaces/IGraphPayments.sol"; import "./GraphDeployments.t.sol"; +import "./mocks/MockHorizonStaking.sol"; import "./mocks/MockGRTToken.sol"; contract GraphPaymentsTest is Test { @@ -18,6 +19,7 @@ contract GraphPaymentsTest is Test { Controller controller; MockGRTToken token; GraphEscrow escrow; + MockHorizonStaking staking; address governor; @@ -36,6 +38,7 @@ contract GraphPaymentsTest is Test { controller = deployments.controller(); token = deployments.token(); escrow = deployments.escrow(); + staking = deployments.staking(); governor = deployments.governor(); revokeCollectorThawingPeriod = deployments.revokeCollectorThawingPeriod(); @@ -121,8 +124,7 @@ contract GraphPaymentsTest is Test { uint256 dataServiceBalance = token.balanceOf(dataService); assertEq(dataServiceBalance, 30 ether); - // TODO: test delegator cut payment - // uint256 delegatorBalance = token.balanceOf(delegationPool); - // assertEq(delegatorBalance, 50 ether); + uint256 delegatorBalance = staking.delegationPool(indexer); + assertEq(delegatorBalance, 50 ether); } } \ No newline at end of file diff --git a/packages/horizon/test/mocks/MockHorizonStaking.sol b/packages/horizon/test/mocks/MockHorizonStaking.sol index a71beff98..a31cc8798 100644 --- a/packages/horizon/test/mocks/MockHorizonStaking.sol +++ b/packages/horizon/test/mocks/MockHorizonStaking.sol @@ -4,13 +4,13 @@ pragma solidity ^0.8.24; import "forge-std/Test.sol"; import { IHorizonStaking } from "@graphprotocol/contracts/contracts/staking/IHorizonStaking.sol"; -import { MockGRTToken } from "./MockGRTToken.sol"; contract MockHorizonStaking is IHorizonStaking { - uint256 public delegatorCut; + uint256 public delegationCut; + mapping(address serviceProvider => uint256 tokens) public delegationPool; - constructor(uint256 _delegatorCut) { - delegatorCut = _delegatorCut; + constructor(uint256 _delegationCut) { + delegationCut = _delegationCut; } function allowVerifier(address verifier, bool allow) external {} @@ -35,7 +35,11 @@ contract MockHorizonStaking is IHorizonStaking { function getServiceProvider(address serviceProvider) external view returns (ServiceProvider memory) {} function getProvision(bytes32 provision) external view returns (Provision memory) {} - function getDelegatorCut(address serviceProvider, uint256 paymentType) external returns (uint256) { - return delegatorCut; + function getDelegationCut(address serviceProvider, uint8 paymentType) external view returns (uint256) { + return delegationCut; + } + + function addToDelegationPool(address serviceProvider, uint256 tokens) external { + delegationPool[serviceProvider] += tokens; } } \ No newline at end of file From 575faec3008230ccf744178550b0f64b6591cb22 Mon Sep 17 00:00:00 2001 From: Miguel de Elias Date: Fri, 19 Apr 2024 16:34:09 -0300 Subject: [PATCH 10/15] chore: move files to folders --- packages/horizon/contracts/{ => escrow}/GraphEscrow.sol | 6 +++--- .../horizon/contracts/{ => escrow}/GraphEscrowStorage.sol | 2 +- packages/horizon/contracts/{ => payments}/GraphPayments.sol | 6 +++--- .../contracts/{ => payments}/GraphPaymentsStorage.sol | 2 +- packages/horizon/test/GraphDeployments.t.sol | 4 ++-- packages/horizon/test/GraphEscrow.t.sol | 4 ++-- packages/horizon/test/GraphPayments.t.sol | 4 ++-- 7 files changed, 14 insertions(+), 14 deletions(-) rename packages/horizon/contracts/{ => escrow}/GraphEscrow.sol (96%) rename packages/horizon/contracts/{ => escrow}/GraphEscrowStorage.sol (92%) rename packages/horizon/contracts/{ => payments}/GraphPayments.sol (95%) rename packages/horizon/contracts/{ => payments}/GraphPaymentsStorage.sol (91%) diff --git a/packages/horizon/contracts/GraphEscrow.sol b/packages/horizon/contracts/escrow/GraphEscrow.sol similarity index 96% rename from packages/horizon/contracts/GraphEscrow.sol rename to packages/horizon/contracts/escrow/GraphEscrow.sol index afb23d97b..dd4d2e9c3 100644 --- a/packages/horizon/contracts/GraphEscrow.sol +++ b/packages/horizon/contracts/escrow/GraphEscrow.sol @@ -3,9 +3,9 @@ pragma solidity ^0.8.24; import { IGraphToken } from "@graphprotocol/contracts/contracts/token/IGraphToken.sol"; -import { IGraphEscrow } from "./interfaces/IGraphEscrow.sol"; -import { IGraphPayments } from "./interfaces/IGraphPayments.sol"; -import { GraphDirectory } from "./GraphDirectory.sol"; +import { IGraphEscrow } from "../interfaces/IGraphEscrow.sol"; +import { IGraphPayments } from "../interfaces/IGraphPayments.sol"; +import { GraphDirectory } from "../GraphDirectory.sol"; import { GraphEscrowStorageV1Storage } from "./GraphEscrowStorage.sol"; contract GraphEscrow is IGraphEscrow, GraphEscrowStorageV1Storage, GraphDirectory { diff --git a/packages/horizon/contracts/GraphEscrowStorage.sol b/packages/horizon/contracts/escrow/GraphEscrowStorage.sol similarity index 92% rename from packages/horizon/contracts/GraphEscrowStorage.sol rename to packages/horizon/contracts/escrow/GraphEscrowStorage.sol index 472915481..1f5657e18 100644 --- a/packages/horizon/contracts/GraphEscrowStorage.sol +++ b/packages/horizon/contracts/escrow/GraphEscrowStorage.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: GPL-3.0-or-later pragma solidity ^0.8.24; -import { IGraphEscrow } from "./interfaces/IGraphEscrow.sol"; +import { IGraphEscrow } from "../interfaces/IGraphEscrow.sol"; contract GraphEscrowStorageV1Storage { // Stores how much escrow each sender has deposited for each receiver, as well as thawing information diff --git a/packages/horizon/contracts/GraphPayments.sol b/packages/horizon/contracts/payments/GraphPayments.sol similarity index 95% rename from packages/horizon/contracts/GraphPayments.sol rename to packages/horizon/contracts/payments/GraphPayments.sol index 553f5b7ce..5213e3e17 100644 --- a/packages/horizon/contracts/GraphPayments.sol +++ b/packages/horizon/contracts/payments/GraphPayments.sol @@ -4,9 +4,9 @@ pragma solidity ^0.8.24; import { IGraphToken } from "@graphprotocol/contracts/contracts/token/IGraphToken.sol"; import { IHorizonStaking } from "@graphprotocol/contracts/contracts/staking/IHorizonStaking.sol"; -import { IGraphPayments } from "./interfaces/IGraphPayments.sol"; -import { IGraphEscrow } from "./interfaces/IGraphEscrow.sol"; -import { GraphDirectory } from "./GraphDirectory.sol"; +import { IGraphPayments } from "../interfaces/IGraphPayments.sol"; +import { IGraphEscrow } from "../interfaces/IGraphEscrow.sol"; +import { GraphDirectory } from "../GraphDirectory.sol"; import { GraphPaymentsStorageV1Storage } from "./GraphPaymentsStorage.sol"; contract GraphPayments is IGraphPayments, GraphPaymentsStorageV1Storage, GraphDirectory { diff --git a/packages/horizon/contracts/GraphPaymentsStorage.sol b/packages/horizon/contracts/payments/GraphPaymentsStorage.sol similarity index 91% rename from packages/horizon/contracts/GraphPaymentsStorage.sol rename to packages/horizon/contracts/payments/GraphPaymentsStorage.sol index e649aea0c..4c6051c43 100644 --- a/packages/horizon/contracts/GraphPaymentsStorage.sol +++ b/packages/horizon/contracts/payments/GraphPaymentsStorage.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: GPL-3.0-or-later pragma solidity ^0.8.24; -import { IGraphPayments } from "./interfaces/IGraphPayments.sol"; +import { IGraphPayments } from "../interfaces/IGraphPayments.sol"; contract GraphPaymentsStorageV1Storage { // Authorized collectors diff --git a/packages/horizon/test/GraphDeployments.t.sol b/packages/horizon/test/GraphDeployments.t.sol index 490445f5b..159035101 100644 --- a/packages/horizon/test/GraphDeployments.t.sol +++ b/packages/horizon/test/GraphDeployments.t.sol @@ -5,8 +5,8 @@ import "forge-std/Test.sol"; import { Controller } from "@graphprotocol/contracts/contracts/governance/Controller.sol"; -import { GraphEscrow } from "contracts/GraphEscrow.sol"; -import { GraphPayments } from "contracts/GraphPayments.sol"; +import { GraphEscrow } from "contracts/escrow/GraphEscrow.sol"; +import { GraphPayments } from "contracts/payments/GraphPayments.sol"; import { IGraphPayments } from "contracts/interfaces/IGraphPayments.sol"; import "./mocks/MockHorizonStaking.sol"; diff --git a/packages/horizon/test/GraphEscrow.t.sol b/packages/horizon/test/GraphEscrow.t.sol index ae8814971..5a2f66976 100644 --- a/packages/horizon/test/GraphEscrow.t.sol +++ b/packages/horizon/test/GraphEscrow.t.sol @@ -5,8 +5,8 @@ import "forge-std/Test.sol"; import { Controller } from "@graphprotocol/contracts/contracts/governance/Controller.sol"; -import { GraphEscrow } from "contracts/GraphEscrow.sol"; -import { GraphPayments } from "contracts/GraphPayments.sol"; +import { GraphEscrow } from "contracts/escrow/GraphEscrow.sol"; +import { GraphPayments } from "contracts/payments/GraphPayments.sol"; import "./GraphDeployments.t.sol"; import "./mocks/MockGRTToken.sol"; diff --git a/packages/horizon/test/GraphPayments.t.sol b/packages/horizon/test/GraphPayments.t.sol index ea9585db0..a3c77d9c0 100644 --- a/packages/horizon/test/GraphPayments.t.sol +++ b/packages/horizon/test/GraphPayments.t.sol @@ -5,8 +5,8 @@ import "forge-std/Test.sol"; import { Controller } from "@graphprotocol/contracts/contracts/governance/Controller.sol"; -import { GraphEscrow } from "contracts/GraphEscrow.sol"; -import { GraphPayments } from "contracts/GraphPayments.sol"; +import { GraphEscrow } from "contracts/escrow/GraphEscrow.sol"; +import { GraphPayments } from "contracts/payments/GraphPayments.sol"; import { IGraphPayments } from "contracts/interfaces/IGraphPayments.sol"; import "./GraphDeployments.t.sol"; From e0e93a02f00187f09c6345a9021e31680174a29b Mon Sep 17 00:00:00 2001 From: Miguel de Elias Date: Fri, 19 Apr 2024 17:14:58 -0300 Subject: [PATCH 11/15] chore: add unit tests --- .../contracts/interfaces/IGraphPayments.sol | 3 +- .../contracts/payments/GraphPayments.sol | 15 ++- packages/horizon/test/GraphEscrow.t.sol | 93 +++++++++++++++++++ packages/horizon/test/GraphPayments.t.sol | 91 +++++++++++++++--- 4 files changed, 189 insertions(+), 13 deletions(-) diff --git a/packages/horizon/contracts/interfaces/IGraphPayments.sol b/packages/horizon/contracts/interfaces/IGraphPayments.sol index 34dc48770..29802301b 100644 --- a/packages/horizon/contracts/interfaces/IGraphPayments.sol +++ b/packages/horizon/contracts/interfaces/IGraphPayments.sol @@ -11,11 +11,12 @@ interface IGraphPayments { // Collector struct Collector { bool authorized; + uint256 amount; uint256 thawEndTimestamp; } // approve a data service to collect funds - function approveCollector(address dataService) external; + function approveCollector(address dataService, uint256 amount) external; // thaw a data service's collector authorization function thawCollector(address dataService) external; diff --git a/packages/horizon/contracts/payments/GraphPayments.sol b/packages/horizon/contracts/payments/GraphPayments.sol index 5213e3e17..67e321fb2 100644 --- a/packages/horizon/contracts/payments/GraphPayments.sol +++ b/packages/horizon/contracts/payments/GraphPayments.sol @@ -15,6 +15,7 @@ contract GraphPayments is IGraphPayments, GraphPaymentsStorageV1Storage, GraphDi error GraphPaymentsNotThawing(); error GraphPaymentsStillThawing(uint256 currentTimestamp, uint256 thawEndTimestamp); error GraphPaymentsCollectorNotAuthorized(address sender, address dataService); + error GraphPaymentsCollectorInsufficientAmount(uint256 available, uint256 required); // -- Events -- @@ -41,8 +42,9 @@ contract GraphPayments is IGraphPayments, GraphPaymentsStorageV1Storage, GraphDi } // approve a data service to collect funds - function approveCollector(address dataService) external { + function approveCollector(address dataService, uint256 amount) external { authorizedCollectors[msg.sender][dataService].authorized = true; + authorizedCollectors[msg.sender][dataService].amount = amount; emit AuthorizedCollector(msg.sender, dataService); } @@ -54,6 +56,10 @@ contract GraphPayments is IGraphPayments, GraphPaymentsStorageV1Storage, GraphDi // cancel thawing a data service's collector authorization function cancelThawCollector(address dataService) external { + if (authorizedCollectors[msg.sender][dataService].thawEndTimestamp == 0) { + revert GraphPaymentsNotThawing(); + } + authorizedCollectors[msg.sender][dataService].thawEndTimestamp = 0; emit CancelThawCollector(msg.sender, dataService); } @@ -88,6 +94,13 @@ contract GraphPayments is IGraphPayments, GraphPaymentsStorageV1Storage, GraphDi revert GraphPaymentsCollectorNotAuthorized(sender, msg.sender); } + if (collector.amount < amount) { + revert GraphPaymentsCollectorInsufficientAmount(collector.amount, amount); + } + + // Reduce amount from approved collector + collector.amount -= amount; + // Collect tokens from GraphEscrow graphEscrow.collect(sender, receiver, amount); diff --git a/packages/horizon/test/GraphEscrow.t.sol b/packages/horizon/test/GraphEscrow.t.sol index 5a2f66976..356d4571c 100644 --- a/packages/horizon/test/GraphEscrow.t.sol +++ b/packages/horizon/test/GraphEscrow.t.sol @@ -41,6 +41,8 @@ contract GraphEscrowTest is Test { receiver = address(0xB2); } + // Deposit tests + function testDeposit() public { token.mint(sender, 10000 ether); vm.startPrank(sender); @@ -75,6 +77,27 @@ contract GraphEscrowTest is Test { assertEq(otherReceiverEscrowBalance, 2000 ether); } + function testDepositMany_RevertWhen_InputsLengthMismatch() public { + address otherReceiver = address(0xB3); + address[] memory receivers = new address[](2); + receivers[0] = receiver; + receivers[1] = otherReceiver; + + uint256[] memory amounts = new uint256[](1); + amounts[0] = 1000 ether; + + token.mint(sender, 1000 ether); + token.approve(address(escrow), 1000 ether); + + // revert + bytes memory expectedError = abi.encodeWithSignature("GraphEscrowInputsLengthMismatch()"); + vm.expectRevert(expectedError); + vm.prank(sender); + escrow.depositMany(receivers, amounts); + } + + // Thaw tests + function testThaw() public { token.mint(sender, 1000 ether); vm.startPrank(sender); @@ -88,6 +111,34 @@ contract GraphEscrowTest is Test { assertEq(thawEndTimestamp, block.timestamp + withdrawEscrowThawingPeriod); } + function testThaw_RevertWhen_InsufficientThawAmount() public { + token.mint(sender, 1000 ether); + vm.startPrank(sender); + token.approve(address(escrow), 1000 ether); + escrow.deposit(receiver, 1000 ether); + + // revert + bytes memory expectedError = abi.encodeWithSignature("GraphEscrowInsufficientThawAmount()"); + vm.expectRevert(expectedError); + escrow.thaw(receiver, 0); + vm.stopPrank(); + } + + function testThaw_RevertWhen_InsufficientAmount() public { + token.mint(sender, 1000 ether); + vm.startPrank(sender); + token.approve(address(escrow), 1000 ether); + escrow.deposit(receiver, 1000 ether); + + // revert + bytes memory expectedError = abi.encodeWithSignature("GraphEscrowInsufficientAmount(uint256,uint256)", 1000 ether, 2000 ether); + vm.expectRevert(expectedError); + escrow.thaw(receiver, 2000 ether); + vm.stopPrank(); + } + + // Withdraw tests + function testWithdraw() public { token.mint(sender, 1000 ether); vm.startPrank(sender); @@ -105,6 +156,35 @@ contract GraphEscrowTest is Test { assertEq(receiverEscrowBalance, 900 ether); } + function testWithdraw_RevertWhen_NotThawing() public { + token.mint(sender, 1000 ether); + vm.startPrank(sender); + token.approve(address(escrow), 1000 ether); + escrow.deposit(receiver, 1000 ether); + + // revert + bytes memory expectedError = abi.encodeWithSignature("GraphEscrowNotThawing()"); + vm.expectRevert(expectedError); + escrow.withdraw(receiver); + vm.stopPrank(); + } + + function testWithdraw_RevertWhen_StillThawing() public { + token.mint(sender, 1000 ether); + vm.startPrank(sender); + token.approve(address(escrow), 1000 ether); + escrow.deposit(receiver, 1000 ether); + escrow.thaw(receiver, 100 ether); + + // revert + bytes memory expectedError = abi.encodeWithSignature("GraphEscrowStillThawing(uint256,uint256)", block.timestamp, block.timestamp + withdrawEscrowThawingPeriod); + vm.expectRevert(expectedError); + escrow.withdraw(receiver); + vm.stopPrank(); + } + + // Collect tests + function testCollect() public { token.mint(sender, 1000 ether); vm.startPrank(sender); @@ -121,4 +201,17 @@ contract GraphEscrowTest is Test { uint256 graphPaymentsBalance = token.balanceOf(graphPayments); assertEq(graphPaymentsBalance, 100 ether); } + + function testCollect_RevertWhen_NotGraphPayments() public { + token.mint(sender, 1000 ether); + vm.startPrank(sender); + token.approve(address(escrow), 1000 ether); + escrow.deposit(receiver, 1000 ether); + vm.stopPrank(); + + // revert + bytes memory expectedError = abi.encodeWithSignature("GraphEscrowNotGraphPayments()"); + vm.expectRevert(expectedError); + escrow.collect(sender, receiver, 100 ether); + } } \ No newline at end of file diff --git a/packages/horizon/test/GraphPayments.t.sol b/packages/horizon/test/GraphPayments.t.sol index a3c77d9c0..ec85dd9c3 100644 --- a/packages/horizon/test/GraphPayments.t.sol +++ b/packages/horizon/test/GraphPayments.t.sol @@ -48,61 +48,97 @@ contract GraphPaymentsTest is Test { dataService = address(0xA2); } - // Tests + // Approve tests function testApproveCollector() public { vm.prank(sender); - payments.approveCollector(dataService); + payments.approveCollector(dataService, 1000 ether); - (bool authorized, uint256 thawEndTimestamp) = payments.authorizedCollectors(sender, dataService); + (bool authorized,, uint256 thawEndTimestamp) = payments.authorizedCollectors(sender, dataService); assertEq(authorized, true); assertEq(thawEndTimestamp, 0); } + // Thaw tests + function testThawCollector() public { vm.startPrank(sender); - payments.approveCollector(dataService); + payments.approveCollector(dataService, 1000 ether); payments.thawCollector(dataService); vm.stopPrank(); - (bool authorized, uint256 thawEndTimestamp) = payments.authorizedCollectors(sender, dataService); + (bool authorized,, uint256 thawEndTimestamp) = payments.authorizedCollectors(sender, dataService); assertEq(authorized, true); assertEq(thawEndTimestamp, block.timestamp + revokeCollectorThawingPeriod); } + // Cancel thaw tests + function testCancelThawCollector() public { vm.startPrank(sender); - payments.approveCollector(dataService); + payments.approveCollector(dataService, 1000 ether); payments.thawCollector(dataService); vm.stopPrank(); - (bool authorized, uint256 thawEndTimestamp) = payments.authorizedCollectors(sender, dataService); + (bool authorized,, uint256 thawEndTimestamp) = payments.authorizedCollectors(sender, dataService); assertEq(authorized, true); assertEq(thawEndTimestamp, block.timestamp + revokeCollectorThawingPeriod); vm.prank(sender); payments.cancelThawCollector(dataService); - (authorized, thawEndTimestamp) = payments.authorizedCollectors(sender, dataService); + (authorized,, thawEndTimestamp) = payments.authorizedCollectors(sender, dataService); assertEq(authorized, true); assertEq(thawEndTimestamp, 0); } + function testCancel_RevertWhen_CollectorIsNotThawing() public { + vm.startPrank(sender); + payments.approveCollector(dataService, 1000 ether); + bytes memory expectedError = abi.encodeWithSignature("GraphPaymentsNotThawing()"); + vm.expectRevert(expectedError); + payments.cancelThawCollector(dataService); + vm.stopPrank(); + } + + // Revoke tests + function testRevokeCollector() public { vm.startPrank(sender); - payments.approveCollector(dataService); + payments.approveCollector(dataService, 1000 ether); payments.thawCollector(dataService); skip(revokeCollectorThawingPeriod + 1); payments.revokeCollector(dataService); vm.stopPrank(); - (bool authorized,) = payments.authorizedCollectors(sender, dataService); + (bool authorized,,) = payments.authorizedCollectors(sender, dataService); assertEq(authorized, false); } + function testRevoke_RevertWhen_CollectorIsNotThawing() public { + vm.startPrank(sender); + payments.approveCollector(dataService, 1000 ether); + bytes memory expectedError = abi.encodeWithSignature("GraphPaymentsNotThawing()"); + vm.expectRevert(expectedError); + payments.revokeCollector(dataService); + vm.stopPrank(); + } + + function testRevoke_RevertWhen_CollectorIsStillThawing() public { + vm.startPrank(sender); + payments.approveCollector(dataService, 1000 ether); + payments.thawCollector(dataService); + bytes memory expectedError = abi.encodeWithSignature("GraphPaymentsStillThawing(uint256,uint256)", block.timestamp, block.timestamp + revokeCollectorThawingPeriod); + vm.expectRevert(expectedError); + payments.revokeCollector(dataService); + vm.stopPrank(); + } + + // Collect tests + function testCollect() public { vm.prank(sender); - payments.approveCollector(dataService); + payments.approveCollector(dataService, 1000 ether); address indexer = address(0xA3); uint256 amount = 1000 ether; @@ -127,4 +163,37 @@ contract GraphPaymentsTest is Test { uint256 delegatorBalance = staking.delegationPool(indexer); assertEq(delegatorBalance, 50 ether); } + + function testCollect_RevertWhen_CollectorNotAuthorized() public { + address indexer = address(0xA3); + uint256 amount = 1000 ether; + + vm.startPrank(dataService); + uint256 dataServiceCut = 30000; // 3% + bytes memory expectedError = abi.encodeWithSignature("GraphPaymentsCollectorNotAuthorized(address,address)", sender, dataService); + vm.expectRevert(expectedError); + payments.collect(sender, indexer, amount, IGraphPayments.PaymentType.IndexingFees, dataServiceCut); + vm.stopPrank(); + } + + function testCollect_RevertWhen_CollectorHasInsufficientAmount() public { + vm.prank(sender); + payments.approveCollector(dataService, 100 ether); + + address indexer = address(0xA3); + uint256 amount = 1000 ether; + + token.mint(sender, amount); + vm.startPrank(sender); + token.approve(address(escrow), amount); + escrow.deposit(indexer, amount); + vm.stopPrank(); + + vm.startPrank(dataService); + uint256 dataServiceCut = 30000; // 3% + bytes memory expectedError = abi.encodeWithSignature("GraphPaymentsCollectorInsufficientAmount(uint256,uint256)", 100 ether, 1000 ether); + vm.expectRevert(expectedError); + payments.collect(sender, indexer, 1000 ether, IGraphPayments.PaymentType.IndexingFees, dataServiceCut); + vm.stopPrank(); + } } \ No newline at end of file From 2b7cff58a264bbfd41866e2920320219bc2afe28 Mon Sep 17 00:00:00 2001 From: Miguel de Elias Date: Wed, 8 May 2024 18:41:00 -0300 Subject: [PATCH 12/15] chore: move Collector functionality to Escrow, now Escrow calls Payments --- .../horizon/contracts/escrow/GraphEscrow.sol | 107 ++++++++++--- .../contracts/escrow/GraphEscrowStorage.sol | 7 + .../contracts/interfaces/IGraphEscrow.sol | 18 ++- .../contracts/interfaces/IGraphPayments.sol | 23 +-- .../contracts/payments/GraphPayments.sol | 87 ++--------- .../payments/GraphPaymentsStorage.sol | 11 -- .../horizon/contracts/utils/TokenUtils.sol | 42 ++++++ packages/horizon/test/GraphDeployments.t.sol | 8 +- packages/horizon/test/GraphEscrow.t.sol | 139 +++++++++++++++-- packages/horizon/test/GraphPayments.t.sol | 140 +----------------- packages/subgraph-service/lib/forge-std | 1 - 11 files changed, 303 insertions(+), 280 deletions(-) create mode 100644 packages/horizon/contracts/utils/TokenUtils.sol delete mode 160000 packages/subgraph-service/lib/forge-std diff --git a/packages/horizon/contracts/escrow/GraphEscrow.sol b/packages/horizon/contracts/escrow/GraphEscrow.sol index dd4d2e9c3..a1f5c38f0 100644 --- a/packages/horizon/contracts/escrow/GraphEscrow.sol +++ b/packages/horizon/contracts/escrow/GraphEscrow.sol @@ -1,12 +1,11 @@ // SPDX-License-Identifier: GPL-3.0-or-later pragma solidity ^0.8.24; -import { IGraphToken } from "@graphprotocol/contracts/contracts/token/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 "../utils/TokenUtils.sol"; contract GraphEscrow is IGraphEscrow, GraphEscrowStorageV1Storage, GraphDirectory { // -- Errors -- @@ -17,9 +16,16 @@ contract GraphEscrow is IGraphEscrow, GraphEscrowStorageV1Storage, GraphDirector 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( @@ -32,25 +38,68 @@ contract GraphEscrow is IGraphEscrow, GraphEscrowStorageV1Storage, GraphDirector event Withdraw(address indexed sender, address indexed receiver, uint256 amount); event Collect(address indexed sender, address indexed receiver, uint256 amount); - // -- Modifier -- + // -- Constructor -- - modifier onlyGraphPayments() { - if (msg.sender != address(graphPayments)) { - revert GraphEscrowNotGraphPayments(); + constructor( + address _controller, + uint256 _revokeCollectorThawingPeriod, + uint256 _withdrawEscrowThawingPeriod + ) GraphDirectory(_controller) { + if (_revokeCollectorThawingPeriod > MAX_THAWING_PERIOD) { + revert GraphEscrowThawingPeriodTooLong(_revokeCollectorThawingPeriod, MAX_THAWING_PERIOD); } - _; - } - // -- Constructor -- + if (_withdrawEscrowThawingPeriod > MAX_THAWING_PERIOD) { + revert GraphEscrowThawingPeriodTooLong(_withdrawEscrowThawingPeriod, MAX_THAWING_PERIOD); + } - constructor(address _controller, uint256 _withdrawEscrowThawingPeriod) GraphDirectory(_controller) { + 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; - graphToken.transferFrom(msg.sender, address(this), amount); + TokenUtils.pullTokens(graphToken, msg.sender, amount); emit Deposit(msg.sender, receiver, amount); } @@ -70,7 +119,7 @@ contract GraphEscrow is IGraphEscrow, GraphEscrowStorageV1Storage, GraphDirector emit Deposit(msg.sender, receiver, amount); } - graphToken.transferFrom(msg.sender, address(this), totalAmount); + TokenUtils.pullTokens(graphToken, msg.sender, totalAmount); } // Requests to thaw a specific amount of escrow from a receiver's escrow account @@ -122,20 +171,42 @@ contract GraphEscrow is IGraphEscrow, GraphEscrowStorageV1Storage, GraphDirector account.balance -= amount; // Reduce the balance by the withdrawn amount (no underflow risk) account.amountThawing = 0; account.thawEndTimestamp = 0; - graphToken.transfer(msg.sender, amount); + TokenUtils.pushTokens(graphToken, msg.sender, amount); emit Withdraw(msg.sender, receiver, amount); } // Collect from escrow (up to amount available in escrow) for a receiver using sender's deposit - function collect(address sender, address receiver, uint256 amount) external onlyGraphPayments { + 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 available = account.balance - account.amountThawing; - - // TODO: should we revert if not enough funds are available? uint256 collectAmount = amount > available ? available : amount; - account.balance -= collectAmount; - graphToken.transfer(msg.sender, collectAmount); emit Collect(sender, receiver, collectAmount); + + // Approve tokens so GraphPayments can pull them + graphToken.approve(address(graphPayments), collectAmount); + graphPayments.collect(receiver, dataService, collectAmount, paymentType, tokensDataService); } } diff --git a/packages/horizon/contracts/escrow/GraphEscrowStorage.sol b/packages/horizon/contracts/escrow/GraphEscrowStorage.sol index 1f5657e18..158b7fae0 100644 --- a/packages/horizon/contracts/escrow/GraphEscrowStorage.sol +++ b/packages/horizon/contracts/escrow/GraphEscrowStorage.sol @@ -4,6 +4,10 @@ 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; @@ -12,6 +16,9 @@ contract GraphEscrowStorageV1Storage { // 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; } diff --git a/packages/horizon/contracts/interfaces/IGraphEscrow.sol b/packages/horizon/contracts/interfaces/IGraphEscrow.sol index 8e6ca58fd..841e18eb8 100644 --- a/packages/horizon/contracts/interfaces/IGraphEscrow.sol +++ b/packages/horizon/contracts/interfaces/IGraphEscrow.sol @@ -1,6 +1,8 @@ // 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 @@ -8,6 +10,13 @@ interface IGraphEscrow { 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; @@ -21,5 +30,12 @@ interface IGraphEscrow { function withdraw(address receiver) external; // Collect from escrow (up to amount available in escrow) for a receiver using sender's deposit - function collect(address sender, address receiver, uint256 amount) external; + function collect( + address sender, + address receiver, + address dataService, + uint256 amount, + IGraphPayments.PaymentType paymentType, + uint256 tokensDataService + ) external; } diff --git a/packages/horizon/contracts/interfaces/IGraphPayments.sol b/packages/horizon/contracts/interfaces/IGraphPayments.sol index 29802301b..b3af02b20 100644 --- a/packages/horizon/contracts/interfaces/IGraphPayments.sol +++ b/packages/horizon/contracts/interfaces/IGraphPayments.sol @@ -8,31 +8,12 @@ interface IGraphPayments { QueryFees } - // Collector - struct Collector { - bool authorized; - uint256 amount; - uint256 thawEndTimestamp; - } - - // approve a data service to collect funds - function approveCollector(address dataService, uint256 amount) external; - - // thaw a data service's collector authorization - function thawCollector(address dataService) external; - - // cancel thawing a data service's collector authorization - function cancelThawCollector(address dataService) external; - - // revoke authorized collector - function revokeCollector(address dataService) external; - // collect funds from a sender, pay cuts and forward the rest to the receiver function collect( - address sender, address receiver, + address dataService, uint256 amount, PaymentType paymentType, - uint256 dataServiceCut + uint256 tokensDataService ) external; } diff --git a/packages/horizon/contracts/payments/GraphPayments.sol b/packages/horizon/contracts/payments/GraphPayments.sol index 67e321fb2..dc8809630 100644 --- a/packages/horizon/contracts/payments/GraphPayments.sol +++ b/packages/horizon/contracts/payments/GraphPayments.sol @@ -1,13 +1,10 @@ // SPDX-License-Identifier: GPL-3.0-or-later pragma solidity ^0.8.24; -import { IGraphToken } from "@graphprotocol/contracts/contracts/token/IGraphToken.sol"; -import { IHorizonStaking } from "@graphprotocol/contracts/contracts/staking/IHorizonStaking.sol"; - import { IGraphPayments } from "../interfaces/IGraphPayments.sol"; -import { IGraphEscrow } from "../interfaces/IGraphEscrow.sol"; import { GraphDirectory } from "../GraphDirectory.sol"; import { GraphPaymentsStorageV1Storage } from "./GraphPaymentsStorage.sol"; +import { TokenUtils } from "../utils/TokenUtils.sol"; contract GraphPayments is IGraphPayments, GraphPaymentsStorageV1Storage, GraphDirectory { // -- Errors -- @@ -19,11 +16,6 @@ contract GraphPayments is IGraphPayments, GraphPaymentsStorageV1Storage, GraphDi // -- 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); - // -- Modifier -- // -- Parameters -- @@ -32,85 +24,26 @@ contract GraphPayments is IGraphPayments, GraphPaymentsStorageV1Storage, GraphDi // -- Constructor -- - constructor( - address _controller, - uint256 _revokeCollectorThawingPeriod, - uint256 _protocolPaymentCut - ) GraphDirectory(_controller) { - revokeCollectorThawingPeriod = _revokeCollectorThawingPeriod; + constructor(address _controller, uint256 _protocolPaymentCut) GraphDirectory(_controller) { protocolPaymentCut = _protocolPaymentCut; } - // 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 GraphPaymentsNotThawing(); - } - - 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 GraphPaymentsNotThawing(); - } - - if (collector.thawEndTimestamp > block.timestamp) { - revert GraphPaymentsStillThawing(block.timestamp, collector.thawEndTimestamp); - } - - delete authorizedCollectors[msg.sender][dataService]; - emit RevokeCollector(msg.sender, dataService); - } - // collect funds from a sender, pay cuts and forward the rest to the receiver function collect( - address sender, address receiver, // serviceProvider + address dataService, uint256 amount, IGraphPayments.PaymentType paymentType, - uint256 dataServiceCut + uint256 tokensDataService ) external { - Collector storage collector = authorizedCollectors[sender][msg.sender]; - - if (!collector.authorized) { - revert GraphPaymentsCollectorNotAuthorized(sender, msg.sender); - } - - if (collector.amount < amount) { - revert GraphPaymentsCollectorInsufficientAmount(collector.amount, amount); - } - - // Reduce amount from approved collector - collector.amount -= amount; - - // Collect tokens from GraphEscrow - graphEscrow.collect(sender, receiver, amount); + TokenUtils.pullTokens(graphToken, msg.sender, amount); // Pay protocol cut - uint256 protocolCut = (amount * protocolPaymentCut) / MAX_PPM; - graphToken.burn(protocolCut); + uint256 tokensProtocol = (amount * protocolPaymentCut) / MAX_PPM; + TokenUtils.burnTokens(graphToken, tokensProtocol); // Pay data service cut - uint256 dataServicePayment = (amount * dataServiceCut) / MAX_PPM; - graphToken.transfer(msg.sender, dataServicePayment); + TokenUtils.pushTokens(graphToken, dataService, tokensDataService); // Get delegation cut uint256 delegatorCut = graphStaking.getDelegationCut(receiver, uint8(paymentType)); @@ -118,7 +51,7 @@ contract GraphPayments is IGraphPayments, GraphPaymentsStorageV1Storage, GraphDi graphStaking.addToDelegationPool(receiver, delegatorPayment); // Pay the rest to the receiver - uint256 receiverPayment = amount - protocolCut - dataServicePayment - delegatorPayment; - graphToken.transfer(receiver, receiverPayment); + uint256 receiverPayment = amount - tokensProtocol - tokensDataService - delegatorPayment; + TokenUtils.pushTokens(graphToken, receiver, receiverPayment); } } diff --git a/packages/horizon/contracts/payments/GraphPaymentsStorage.sol b/packages/horizon/contracts/payments/GraphPaymentsStorage.sol index 4c6051c43..7d4c1c338 100644 --- a/packages/horizon/contracts/payments/GraphPaymentsStorage.sol +++ b/packages/horizon/contracts/payments/GraphPaymentsStorage.sol @@ -4,17 +4,6 @@ pragma solidity ^0.8.24; import { IGraphPayments } from "../interfaces/IGraphPayments.sol"; contract GraphPaymentsStorageV1Storage { - // Authorized collectors - mapping(address sender => mapping(address dataService => IGraphPayments.Collector collector)) - public authorizedCollectors; - - // The maximum thawing period (in seconds) for removing collector authorization - // This is a precautionary measure to avoid inadvertedly locking collectors for too long - uint256 public constant MAX_THAWING_PERIOD = 90 days; - - // Thawing period for authorized collectors - uint256 public immutable revokeCollectorThawingPeriod; - // The graph protocol payment cut uint256 public immutable protocolPaymentCut; } diff --git a/packages/horizon/contracts/utils/TokenUtils.sol b/packages/horizon/contracts/utils/TokenUtils.sol new file mode 100644 index 000000000..7bb86c491 --- /dev/null +++ b/packages/horizon/contracts/utils/TokenUtils.sol @@ -0,0 +1,42 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +pragma solidity 0.8.24; + +import { IGraphToken } from "@graphprotocol/contracts/contracts/token/IGraphToken.sol"; + +library TokenUtils { + /** + * @dev Pull tokens from an address to this contract. + * @param _graphToken Token to transfer + * @param _from Address sending the tokens + * @param _amount Amount of tokens to transfer + */ + function pullTokens(IGraphToken _graphToken, address _from, uint256 _amount) internal { + if (_amount > 0) { + require(_graphToken.transferFrom(_from, address(this), _amount), "!transfer"); + } + } + + /** + * @dev Push tokens from this contract to a receiving address. + * @param _graphToken Token to transfer + * @param _to Address receiving the tokens + * @param _amount Amount of tokens to transfer + */ + function pushTokens(IGraphToken _graphToken, address _to, uint256 _amount) internal { + if (_amount > 0) { + require(_graphToken.transfer(_to, _amount), "!transfer"); + } + } + + /** + * @dev Burn tokens held by this contract. + * @param _graphToken Token to burn + * @param _amount Amount of tokens to burn + */ + function burnTokens(IGraphToken _graphToken, uint256 _amount) internal { + if (_amount > 0) { + _graphToken.burn(_amount); + } + } +} diff --git a/packages/horizon/test/GraphDeployments.t.sol b/packages/horizon/test/GraphDeployments.t.sol index 159035101..e08986c62 100644 --- a/packages/horizon/test/GraphDeployments.t.sol +++ b/packages/horizon/test/GraphDeployments.t.sol @@ -47,12 +47,12 @@ contract GraphDeployments is Test { // GraphPayments preddict address bytes32 saltPayments = keccak256("GraphPaymentsSalt"); - bytes32 paymentsHash = keccak256(bytes.concat(vm.getCode("GraphPayments.sol:GraphPayments"), abi.encode(address(controller), revokeCollectorThawingPeriod, protocolPaymentCut))); + bytes32 paymentsHash = keccak256(bytes.concat(vm.getCode("GraphPayments.sol:GraphPayments"), abi.encode(address(controller), protocolPaymentCut))); address predictedPaymentsAddress = vm.computeCreate2Address(saltPayments, paymentsHash, deployer); // GraphEscrow preddict address bytes32 saltEscrow = keccak256("GraphEscrowSalt"); - bytes32 escrowHash = keccak256(bytes.concat(vm.getCode("GraphEscrow.sol:GraphEscrow"), abi.encode(address(controller), withdrawEscrowThawingPeriod))); + bytes32 escrowHash = keccak256(bytes.concat(vm.getCode("GraphEscrow.sol:GraphEscrow"), abi.encode(address(controller), revokeCollectorThawingPeriod, withdrawEscrowThawingPeriod))); address predictedAddressEscrow = vm.computeCreate2Address(saltEscrow, escrowHash, deployer); // GraphToken @@ -72,8 +72,8 @@ contract GraphDeployments is Test { vm.stopPrank(); vm.startPrank(deployer); - payments = new GraphPayments{salt: saltPayments}(address(controller), revokeCollectorThawingPeriod, protocolPaymentCut); - escrow = new GraphEscrow{salt: saltEscrow}(address(controller), withdrawEscrowThawingPeriod); + payments = new GraphPayments{salt: saltPayments}(address(controller), protocolPaymentCut); + escrow = new GraphEscrow{salt: saltEscrow}(address(controller), revokeCollectorThawingPeriod, withdrawEscrowThawingPeriod); vm.stopPrank(); } diff --git a/packages/horizon/test/GraphEscrow.t.sol b/packages/horizon/test/GraphEscrow.t.sol index 356d4571c..b7304d0ad 100644 --- a/packages/horizon/test/GraphEscrow.t.sol +++ b/packages/horizon/test/GraphEscrow.t.sol @@ -7,6 +7,7 @@ import { Controller } from "@graphprotocol/contracts/contracts/governance/Contro import { GraphEscrow } from "contracts/escrow/GraphEscrow.sol"; import { GraphPayments } from "contracts/payments/GraphPayments.sol"; +import { IGraphPayments } from "contracts/interfaces/IGraphPayments.sol"; import "./GraphDeployments.t.sol"; import "./mocks/MockGRTToken.sol"; @@ -20,9 +21,12 @@ contract GraphEscrowTest is Test { address governor = address(0xA2); uint256 withdrawEscrowThawingPeriod = 60; + uint256 revokeCollectorThawingPeriod; address sender; address receiver; + address verifier; + address dataService; // Setup @@ -36,9 +40,98 @@ contract GraphEscrowTest is Test { governor = deployments.governor(); withdrawEscrowThawingPeriod = deployments.withdrawEscrowThawingPeriod(); + revokeCollectorThawingPeriod = deployments.revokeCollectorThawingPeriod(); sender = address(0xB1); receiver = address(0xB2); + verifier = address(0xB3); + dataService = address(0xB4); + } + + // Collector approve tests + + function testApproveCollector() public { + vm.prank(sender); + escrow.approveCollector(verifier, 1000 ether); + + (bool authorized,, uint256 thawEndTimestamp) = escrow.authorizedCollectors(sender, verifier); + assertEq(authorized, true); + assertEq(thawEndTimestamp, 0); + } + + // Collector thaw tests + + function testThawCollector() public { + vm.startPrank(sender); + escrow.approveCollector(verifier, 1000 ether); + escrow.thawCollector(verifier); + vm.stopPrank(); + + (bool authorized,, uint256 thawEndTimestamp) = escrow.authorizedCollectors(sender, verifier); + assertEq(authorized, true); + assertEq(thawEndTimestamp, block.timestamp + revokeCollectorThawingPeriod); + } + + // Collector cancel thaw tests + + function testCancelThawCollector() public { + vm.startPrank(sender); + escrow.approveCollector(verifier, 1000 ether); + escrow.thawCollector(verifier); + vm.stopPrank(); + + (bool authorized,, uint256 thawEndTimestamp) = escrow.authorizedCollectors(sender, verifier); + assertEq(authorized, true); + assertEq(thawEndTimestamp, block.timestamp + revokeCollectorThawingPeriod); + + vm.prank(sender); + escrow.cancelThawCollector(verifier); + + (authorized,, thawEndTimestamp) = escrow.authorizedCollectors(sender, verifier); + assertEq(authorized, true); + assertEq(thawEndTimestamp, 0); + } + + function testCancel_RevertWhen_CollectorIsNotThawing() public { + vm.startPrank(sender); + escrow.approveCollector(verifier, 1000 ether); + bytes memory expectedError = abi.encodeWithSignature("GraphEscrowNotThawing()"); + vm.expectRevert(expectedError); + escrow.cancelThawCollector(verifier); + vm.stopPrank(); + } + + // Collector revoke tests + + function testRevokeCollector() public { + vm.startPrank(sender); + escrow.approveCollector(verifier, 1000 ether); + escrow.thawCollector(verifier); + skip(revokeCollectorThawingPeriod + 1); + escrow.revokeCollector(verifier); + vm.stopPrank(); + + (bool authorized,,) = escrow.authorizedCollectors(sender, verifier); + assertEq(authorized, false); + } + + function testRevoke_RevertWhen_CollectorIsNotThawing() public { + vm.startPrank(sender); + escrow.approveCollector(verifier, 1000 ether); + bytes memory expectedError = abi.encodeWithSignature("GraphEscrowNotThawing()"); + vm.expectRevert(expectedError); + escrow.revokeCollector(verifier); + vm.stopPrank(); + } + + function testRevoke_RevertWhen_CollectorIsStillThawing() public { + vm.startPrank(sender); + escrow.approveCollector(verifier, 1000 ether); + escrow.thawCollector(verifier); + bytes memory expectedError = abi.encodeWithSignature("GraphEscrowStillThawing(uint256,uint256)", block.timestamp, block.timestamp + revokeCollectorThawingPeriod); + vm.expectRevert(expectedError); + escrow.revokeCollector(verifier); + vm.stopPrank(); } // Deposit tests @@ -188,30 +281,48 @@ contract GraphEscrowTest is Test { function testCollect() public { token.mint(sender, 1000 ether); vm.startPrank(sender); + escrow.approveCollector(verifier, 1000 ether); token.approve(address(escrow), 1000 ether); escrow.deposit(receiver, 1000 ether); vm.stopPrank(); - address graphPayments = address(payments); - vm.prank(graphPayments); - escrow.collect(sender, receiver, 100 ether); + vm.prank(verifier); + escrow.collect(sender, receiver, dataService, 100 ether, IGraphPayments.PaymentType.IndexingFees, 3 ether); - (uint256 receiverEscrowBalance,,) = escrow.escrowAccounts(sender, receiver); - assertEq(receiverEscrowBalance, 900 ether); - uint256 graphPaymentsBalance = token.balanceOf(graphPayments); - assertEq(graphPaymentsBalance, 100 ether); + uint256 indexerBalance = token.balanceOf(receiver); + assertEq(indexerBalance, 91 ether); } - function testCollect_RevertWhen_NotGraphPayments() public { - token.mint(sender, 1000 ether); + function testCollect_RevertWhen_CollectorNotAuthorized() public { + address indexer = address(0xA3); + uint256 amount = 1000 ether; + + vm.startPrank(verifier); + uint256 dataServiceCut = 30000; // 3% + bytes memory expectedError = abi.encodeWithSignature("GraphEscrowCollectorNotAuthorized(address,address)", sender, verifier); + vm.expectRevert(expectedError); + escrow.collect(sender, indexer, dataService, amount, IGraphPayments.PaymentType.IndexingFees, dataServiceCut); + vm.stopPrank(); + } + + function testCollect_RevertWhen_CollectorHasInsufficientAmount() public { + vm.prank(sender); + escrow.approveCollector(verifier, 100 ether); + + address indexer = address(0xA3); + uint256 amount = 1000 ether; + + token.mint(sender, amount); vm.startPrank(sender); - token.approve(address(escrow), 1000 ether); - escrow.deposit(receiver, 1000 ether); + token.approve(address(escrow), amount); + escrow.deposit(indexer, amount); vm.stopPrank(); - // revert - bytes memory expectedError = abi.encodeWithSignature("GraphEscrowNotGraphPayments()"); + vm.startPrank(verifier); + uint256 dataServiceCut = 30 ether; + bytes memory expectedError = abi.encodeWithSignature("GraphEscrowCollectorInsufficientAmount(uint256,uint256)", 100 ether, 1000 ether); vm.expectRevert(expectedError); - escrow.collect(sender, receiver, 100 ether); + escrow.collect(sender, indexer, dataService, 1000 ether, IGraphPayments.PaymentType.IndexingFees, dataServiceCut); + vm.stopPrank(); } } \ No newline at end of file diff --git a/packages/horizon/test/GraphPayments.t.sol b/packages/horizon/test/GraphPayments.t.sol index ec85dd9c3..9c0f0ac56 100644 --- a/packages/horizon/test/GraphPayments.t.sol +++ b/packages/horizon/test/GraphPayments.t.sol @@ -23,7 +23,6 @@ contract GraphPaymentsTest is Test { address governor; - uint256 revokeCollectorThawingPeriod; uint256 protocolPaymentCut; address sender; @@ -41,117 +40,25 @@ contract GraphPaymentsTest is Test { staking = deployments.staking(); governor = deployments.governor(); - revokeCollectorThawingPeriod = deployments.revokeCollectorThawingPeriod(); protocolPaymentCut = deployments.protocolPaymentCut(); sender = address(0xA1); dataService = address(0xA2); } - // Approve tests - - function testApproveCollector() public { - vm.prank(sender); - payments.approveCollector(dataService, 1000 ether); - - (bool authorized,, uint256 thawEndTimestamp) = payments.authorizedCollectors(sender, dataService); - assertEq(authorized, true); - assertEq(thawEndTimestamp, 0); - } - - // Thaw tests - - function testThawCollector() public { - vm.startPrank(sender); - payments.approveCollector(dataService, 1000 ether); - payments.thawCollector(dataService); - vm.stopPrank(); - - (bool authorized,, uint256 thawEndTimestamp) = payments.authorizedCollectors(sender, dataService); - assertEq(authorized, true); - assertEq(thawEndTimestamp, block.timestamp + revokeCollectorThawingPeriod); - } - - // Cancel thaw tests - - function testCancelThawCollector() public { - vm.startPrank(sender); - payments.approveCollector(dataService, 1000 ether); - payments.thawCollector(dataService); - vm.stopPrank(); - - (bool authorized,, uint256 thawEndTimestamp) = payments.authorizedCollectors(sender, dataService); - assertEq(authorized, true); - assertEq(thawEndTimestamp, block.timestamp + revokeCollectorThawingPeriod); - - vm.prank(sender); - payments.cancelThawCollector(dataService); - - (authorized,, thawEndTimestamp) = payments.authorizedCollectors(sender, dataService); - assertEq(authorized, true); - assertEq(thawEndTimestamp, 0); - } - - function testCancel_RevertWhen_CollectorIsNotThawing() public { - vm.startPrank(sender); - payments.approveCollector(dataService, 1000 ether); - bytes memory expectedError = abi.encodeWithSignature("GraphPaymentsNotThawing()"); - vm.expectRevert(expectedError); - payments.cancelThawCollector(dataService); - vm.stopPrank(); - } - - // Revoke tests - - function testRevokeCollector() public { - vm.startPrank(sender); - payments.approveCollector(dataService, 1000 ether); - payments.thawCollector(dataService); - skip(revokeCollectorThawingPeriod + 1); - payments.revokeCollector(dataService); - vm.stopPrank(); - - (bool authorized,,) = payments.authorizedCollectors(sender, dataService); - assertEq(authorized, false); - } - - function testRevoke_RevertWhen_CollectorIsNotThawing() public { - vm.startPrank(sender); - payments.approveCollector(dataService, 1000 ether); - bytes memory expectedError = abi.encodeWithSignature("GraphPaymentsNotThawing()"); - vm.expectRevert(expectedError); - payments.revokeCollector(dataService); - vm.stopPrank(); - } - - function testRevoke_RevertWhen_CollectorIsStillThawing() public { - vm.startPrank(sender); - payments.approveCollector(dataService, 1000 ether); - payments.thawCollector(dataService); - bytes memory expectedError = abi.encodeWithSignature("GraphPaymentsStillThawing(uint256,uint256)", block.timestamp, block.timestamp + revokeCollectorThawingPeriod); - vm.expectRevert(expectedError); - payments.revokeCollector(dataService); - vm.stopPrank(); - } - // Collect tests function testCollect() public { - vm.prank(sender); - payments.approveCollector(dataService, 1000 ether); - address indexer = address(0xA3); uint256 amount = 1000 ether; + address escrowAddress = address(escrow); - token.mint(sender, amount); - vm.startPrank(sender); - token.approve(address(escrow), amount); - escrow.deposit(indexer, amount); - vm.stopPrank(); + vm.startPrank(escrowAddress); + token.mint(escrowAddress, amount); + token.approve(address(payments), amount); - vm.startPrank(dataService); - uint256 dataServiceCut = 30000; // 3% - payments.collect(sender, indexer, amount, IGraphPayments.PaymentType.IndexingFees, dataServiceCut); + uint256 dataServiceCut = 30 ether; // 3% + payments.collect(indexer, dataService, amount, IGraphPayments.PaymentType.IndexingFees, dataServiceCut); vm.stopPrank(); uint256 indexerBalance = token.balanceOf(indexer); @@ -163,37 +70,4 @@ contract GraphPaymentsTest is Test { uint256 delegatorBalance = staking.delegationPool(indexer); assertEq(delegatorBalance, 50 ether); } - - function testCollect_RevertWhen_CollectorNotAuthorized() public { - address indexer = address(0xA3); - uint256 amount = 1000 ether; - - vm.startPrank(dataService); - uint256 dataServiceCut = 30000; // 3% - bytes memory expectedError = abi.encodeWithSignature("GraphPaymentsCollectorNotAuthorized(address,address)", sender, dataService); - vm.expectRevert(expectedError); - payments.collect(sender, indexer, amount, IGraphPayments.PaymentType.IndexingFees, dataServiceCut); - vm.stopPrank(); - } - - function testCollect_RevertWhen_CollectorHasInsufficientAmount() public { - vm.prank(sender); - payments.approveCollector(dataService, 100 ether); - - address indexer = address(0xA3); - uint256 amount = 1000 ether; - - token.mint(sender, amount); - vm.startPrank(sender); - token.approve(address(escrow), amount); - escrow.deposit(indexer, amount); - vm.stopPrank(); - - vm.startPrank(dataService); - uint256 dataServiceCut = 30000; // 3% - bytes memory expectedError = abi.encodeWithSignature("GraphPaymentsCollectorInsufficientAmount(uint256,uint256)", 100 ether, 1000 ether); - vm.expectRevert(expectedError); - payments.collect(sender, indexer, 1000 ether, IGraphPayments.PaymentType.IndexingFees, dataServiceCut); - vm.stopPrank(); - } -} \ No newline at end of file +} diff --git a/packages/subgraph-service/lib/forge-std b/packages/subgraph-service/lib/forge-std deleted file mode 160000 index bb4ceea94..000000000 --- a/packages/subgraph-service/lib/forge-std +++ /dev/null @@ -1 +0,0 @@ -Subproject commit bb4ceea94d6f10eeb5b41dc2391c6c8bf8e734ef From 9ab94292be001e97f1acb7791f205fc8c9049680 Mon Sep 17 00:00:00 2001 From: Miguel de Elias Date: Fri, 10 May 2024 14:51:56 -0300 Subject: [PATCH 13/15] chore: revert when there is insufficient amount in deposit --- .../horizon/contracts/escrow/GraphEscrow.sol | 23 +++++++++++++------ .../contracts/interfaces/IGraphEscrow.sol | 4 +++- packages/horizon/test/GraphEscrow.t.sol | 15 ++++++++++++ 3 files changed, 34 insertions(+), 8 deletions(-) diff --git a/packages/horizon/contracts/escrow/GraphEscrow.sol b/packages/horizon/contracts/escrow/GraphEscrow.sol index a1f5c38f0..2c3e63cc8 100644 --- a/packages/horizon/contracts/escrow/GraphEscrow.sol +++ b/packages/horizon/contracts/escrow/GraphEscrow.sol @@ -175,7 +175,7 @@ contract GraphEscrow is IGraphEscrow, GraphEscrowStorageV1Storage, GraphDirector emit Withdraw(msg.sender, receiver, amount); } - // Collect from escrow (up to amount available in escrow) for a receiver using sender's deposit + // Collect from escrow for a receiver using sender's deposit function collect( address sender, address receiver, // serviceProvider @@ -200,13 +200,22 @@ contract GraphEscrow is IGraphEscrow, GraphEscrowStorageV1Storage, GraphDirector // Collect tokens from GraphEscrow up to amount available EscrowAccount storage account = escrowAccounts[sender][receiver]; - uint256 available = account.balance - account.amountThawing; - uint256 collectAmount = amount > available ? available : amount; - account.balance -= collectAmount; - emit Collect(sender, receiver, collectAmount); + 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), collectAmount); - graphPayments.collect(receiver, dataService, collectAmount, paymentType, tokensDataService); + 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; } } diff --git a/packages/horizon/contracts/interfaces/IGraphEscrow.sol b/packages/horizon/contracts/interfaces/IGraphEscrow.sol index 841e18eb8..f9d3f6140 100644 --- a/packages/horizon/contracts/interfaces/IGraphEscrow.sol +++ b/packages/horizon/contracts/interfaces/IGraphEscrow.sol @@ -29,7 +29,7 @@ interface IGraphEscrow { // Withdraws all thawed escrow from a receiver's escrow account function withdraw(address receiver) external; - // Collect from escrow (up to amount available in escrow) for a receiver using sender's deposit + // Collect from escrow for a receiver using sender's deposit function collect( address sender, address receiver, @@ -38,4 +38,6 @@ interface IGraphEscrow { IGraphPayments.PaymentType paymentType, uint256 tokensDataService ) external; + + function getBalance(address sender, address receiver) external view returns (uint256); } diff --git a/packages/horizon/test/GraphEscrow.t.sol b/packages/horizon/test/GraphEscrow.t.sol index b7304d0ad..221b0f62d 100644 --- a/packages/horizon/test/GraphEscrow.t.sol +++ b/packages/horizon/test/GraphEscrow.t.sol @@ -325,4 +325,19 @@ contract GraphEscrowTest is Test { escrow.collect(sender, indexer, dataService, 1000 ether, IGraphPayments.PaymentType.IndexingFees, dataServiceCut); vm.stopPrank(); } + + function testCollect_RevertWhen_SenderHasInsufficientAmountInEscrow() public { + token.mint(sender, 1000 ether); + vm.startPrank(sender); + escrow.approveCollector(verifier, 1000 ether); + token.approve(address(escrow), 1000 ether); + escrow.deposit(receiver, 100 ether); + vm.stopPrank(); + + vm.prank(verifier); + bytes memory expectedError = abi.encodeWithSignature("GraphEscrowInsufficientAmount(uint256,uint256)", 100 ether, 200 ether); + vm.expectRevert(expectedError); + escrow.collect(sender, receiver, dataService, 200 ether, IGraphPayments.PaymentType.IndexingFees, 3 ether); + vm.stopPrank(); + } } \ No newline at end of file From 16eabd820382321e180309bc56d7f781159a663c Mon Sep 17 00:00:00 2001 From: Miguel de Elias Date: Fri, 17 May 2024 22:55:47 -0300 Subject: [PATCH 14/15] fix: merge conflicts errors --- .../contracts/staking/IHorizonStaking.sol | 152 ------------------ .../contracts/HorizonStakingExtension.sol | 3 +- .../contracts/payments/GraphPayments.sol | 2 +- 3 files changed, 3 insertions(+), 154 deletions(-) delete mode 100644 packages/contracts/contracts/staking/IHorizonStaking.sol diff --git a/packages/contracts/contracts/staking/IHorizonStaking.sol b/packages/contracts/contracts/staking/IHorizonStaking.sol deleted file mode 100644 index d55520db5..000000000 --- a/packages/contracts/contracts/staking/IHorizonStaking.sol +++ /dev/null @@ -1,152 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-or-later - -pragma solidity >=0.7.6 <0.9.0; -pragma abicoder v2; - -interface Test { - function test() external returns (uint256); -} - -interface IHorizonStaking { - struct Provision { - // Service provider that created the provision - address serviceProvider; - // tokens in the provision - uint256 tokens; - // tokens that are being thawed (and will stop being slashable soon) - uint256 tokensThawing; - // timestamp of provision creation - uint64 createdAt; - // authority to slash the provision - address verifier; - // max amount that can be taken by the verifier when slashing, expressed in parts-per-million of the amount slashed - uint32 maxVerifierCut; - // time, in seconds, tokens must thaw before being withdrawn - uint64 thawingPeriod; - } - - // the new "Indexer" struct - struct ServiceProviderInternal { - // Tokens on the Service Provider stake (staked by the provider) - uint256 tokensStaked; - // Tokens used in allocations - uint256 __DEPRECATED_tokensAllocated; - // Tokens locked for withdrawal subject to thawing period - uint256 __DEPRECATED_tokensLocked; - // Block when locked tokens can be withdrawn - uint256 __DEPRECATED_tokensLockedUntil; - // tokens used in a provision - uint256 tokensProvisioned; - // tokens that initiated a thawing in any one of the provider's provisions - uint256 tokensRequestedThaw; - // tokens that have been removed from any one of the provider's provisions after thawing - uint256 tokensFulfilledThaw; - // provisions that take priority for undelegation force thawing - bytes32[] forceThawProvisions; - } - - struct ServiceProvider { - // Tokens on the provider stake (staked by the provider) - uint256 tokensStaked; - // tokens used in a provision - uint256 tokensProvisioned; - // tokens that initiated a thawing in any one of the provider's provisions - uint256 tokensRequestedThaw; - // tokens that have been removed from any one of the provider's provisions after thawing - uint256 tokensFulfilledThaw; - // provisions that take priority for undelegation force thawing - bytes32[] forceThawProvisions; - } - - struct DelegationPool { - uint32 __DEPRECATED_cooldownBlocks; // solhint-disable-line var-name-mixedcase - uint32 __DEPRECATED_indexingRewardCut; // in PPM - uint32 __DEPRECATED_queryFeeCut; // in PPM - uint256 __DEPRECATED_updatedAtBlock; // Block when the pool was last updated - uint256 tokens; // Total tokens as pool reserves - uint256 shares; // Total shares minted in the pool - mapping(address => Delegation) delegators; // Mapping of delegator => Delegation - } - - struct Delegation { - // shares owned by the delegator in the pool - uint256 shares; - // tokens delegated to the pool - uint256 tokens; - // Timestamp when locked tokens can be undelegated (after the timelock) - uint256 tokensLockedUntil; - } - - struct ThawRequest { - // tokens that are being thawed by this request - uint256 tokens; - // the provision id to which this request corresponds to - bytes32 provisionId; - // the address that initiated the thaw request, allowed to remove the funds once thawed - address owner; - // the timestamp when the thawed funds can be removed from the provision - uint64 thawingUntil; - // the value of `ServiceProvider.tokensRequestedThaw` the moment the thaw request is created - uint256 tokensRequestedThawSnapshot; - } - - // whitelist/deny a verifier - function allowVerifier(address verifier, bool allow) external; - - // deposit stake - function stake(uint256 tokens) external; - - // create a provision - function provision(uint256 tokens, address verifier, uint256 maxVerifierCut, uint256 thawingPeriod) external; - - // initiate a thawing to remove tokens from a provision - function thaw(bytes32 provisionId, uint256 tokens) external returns (bytes32 thawRequestId); - - // moves thawed stake from a provision back into the provider's available stake - function deprovision(bytes32 thawRequestId) external; - - // moves thawed stake from one provision into another provision - function reprovision(bytes32 thawRequestId, bytes32 provisionId) external; - - // moves thawed stake back to the owner's account - stake is removed from the protocol - function withdraw(bytes32 thawRequestId) external; - - // delegate tokens to a provider - function delegate(address serviceProvider, uint256 tokens) external; - - // undelegate tokens - function undelegate( - address serviceProvider, - uint256 tokens, - bytes32[] calldata provisions - ) external returns (bytes32 thawRequestId); - - // slash a service provider - function slash(bytes32 provisionId, uint256 tokens, uint256 verifierAmount) external; - - // set the Service Provider's preferred provisions to be force thawed - function setForceThawProvisions(bytes32[] calldata provisions) external; - - // total staked tokens to the provider - // `ServiceProvider.tokensStaked + DelegationPool.serviceProvider.tokens` - function getStake(address serviceProvider) external view returns (uint256 tokens); - - // staked tokens that are currently not provisioned, aka idle stake - // `getStake(serviceProvider) - ServiceProvider.tokensProvisioned` - function getIdleStake(address serviceProvider) external view returns (uint256 tokens); - - // staked tokens the provider can provision before hitting the delegation cap - // `ServiceProvider.tokensStaked * Staking.delegationRatio - Provision.tokensProvisioned` - function getCapacity(address serviceProvider) external view returns (uint256 tokens); - - // provisioned tokens that are not being used - // `Provision.tokens - Provision.tokensThawing` - function getTokensAvailable(bytes32 provision) external view returns (uint256 tokens); - - 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; -} diff --git a/packages/horizon/contracts/HorizonStakingExtension.sol b/packages/horizon/contracts/HorizonStakingExtension.sol index 890e34cd8..1ec8b2c45 100644 --- a/packages/horizon/contracts/HorizonStakingExtension.sol +++ b/packages/horizon/contracts/HorizonStakingExtension.sol @@ -8,7 +8,8 @@ import { IL2StakingTypes } from "@graphprotocol/contracts/contracts/l2/staking/I import { IHorizonStakingExtension } from "./IHorizonStakingExtension.sol"; import { MathUtils } from "./utils/MathUtils.sol"; -act +/** + * @title L2Staking contract * @dev This contract is the L2 variant of the Staking contract. It adds a function * to receive an indexer's stake or delegation from L1. Note that this contract inherits Staking, * which uses a StakingExtension contract to implement the full IStaking interface through delegatecalls. diff --git a/packages/horizon/contracts/payments/GraphPayments.sol b/packages/horizon/contracts/payments/GraphPayments.sol index f47ac1414..652183fd3 100644 --- a/packages/horizon/contracts/payments/GraphPayments.sol +++ b/packages/horizon/contracts/payments/GraphPayments.sol @@ -8,7 +8,7 @@ import { GraphDirectory } from "../GraphDirectory.sol"; import { GraphPaymentsStorageV1Storage } from "./GraphPaymentsStorage.sol"; import { TokenUtils } from "../utils/TokenUtils.sol"; -aphPayments, GraphPaymentsStorageV1Storage, GraphDirectory { +contract GraphPayments is IGraphPayments, GraphPaymentsStorageV1Storage, GraphDirectory { // -- Errors -- error GraphPaymentsNotThawing(); From c052943e1f35cb97ecc6ce6e2cc96ea3fb78140b Mon Sep 17 00:00:00 2001 From: Miguel de Elias Date: Mon, 20 May 2024 14:30:08 -0300 Subject: [PATCH 15/15] chore: uint tests refactor --- packages/horizon/test/GraphBase.t.sol | 12 +- .../horizon/test/escrow/GraphEscrow.t.sol | 304 ++---------------- packages/horizon/test/escrow/collect.t.sol | 75 +++++ packages/horizon/test/escrow/collector.t.sol | 87 +++++ packages/horizon/test/escrow/deposit.t.sol | 53 +++ packages/horizon/test/escrow/thaw.t.sol | 34 ++ packages/horizon/test/escrow/withdraw.t.sol | 48 +++ .../horizon/test/payments/GraphPayments.t.sol | 4 +- .../horizon-staking/HorizonStaking.t.sol | 10 +- 9 files changed, 332 insertions(+), 295 deletions(-) create mode 100644 packages/horizon/test/escrow/collect.t.sol create mode 100644 packages/horizon/test/escrow/collector.t.sol create mode 100644 packages/horizon/test/escrow/deposit.t.sol create mode 100644 packages/horizon/test/escrow/thaw.t.sol create mode 100644 packages/horizon/test/escrow/withdraw.t.sol diff --git a/packages/horizon/test/GraphBase.t.sol b/packages/horizon/test/GraphBase.t.sol index 5367dc0c1..b26bc14e1 100644 --- a/packages/horizon/test/GraphBase.t.sol +++ b/packages/horizon/test/GraphBase.t.sol @@ -57,6 +57,14 @@ abstract contract GraphBaseTest is Test, Constants { // Deploy protocol contracts deployProtocolContracts(); unpauseProtocol(); + + // Label contracts + vm.label({ account: address(controller), newLabel: "Controller" }); + vm.label({ account: address(token), newLabel: "GraphToken" }); + vm.label({ account: address(payments), newLabel: "GraphPayments" }); + vm.label({ account: address(escrow), newLabel: "GraphEscrow" }); + vm.label({ account: address(staking), newLabel: "HorizonStaking" }); + vm.label({ account: address(stakingExtension), newLabel: "HorizonStakingExtension" }); } function deployProtocolContracts() private { @@ -158,7 +166,9 @@ abstract contract GraphBaseTest is Test, Constants { function createUser(string memory name) private returns (address) { address user = makeAddr(name); - deal({ token: address(token), to: user, give: 10000 ether }); + vm.deal({ account: user, newBalance: 100 ether }); + deal({ token: address(token), to: user, give: type(uint256).max }); + vm.label({ account: user, newLabel: name }); return user; } diff --git a/packages/horizon/test/escrow/GraphEscrow.t.sol b/packages/horizon/test/escrow/GraphEscrow.t.sol index ced2cbd18..f420c1964 100644 --- a/packages/horizon/test/escrow/GraphEscrow.t.sol +++ b/packages/horizon/test/escrow/GraphEscrow.t.sol @@ -3,306 +3,38 @@ pragma solidity ^0.8.24; import "forge-std/Test.sol"; -import { IGraphPayments } from "../../contracts/interfaces/IGraphPayments.sol"; +import { HorizonStakingSharedTest } from "../shared/horizon-staking/HorizonStaking.t.sol"; -import { HorizonStaking_Shared_Test } from "../shared/horizon-staking/HorizonStaking.t.sol"; +contract GraphEscrowTest is HorizonStakingSharedTest { -contract GraphEscrowTest is HorizonStaking_Shared_Test { - - // Collector approve tests - - function testCollector_Approve() public { - vm.prank(users.gateway); - escrow.approveCollector(users.verifier, 1000 ether); - - (bool authorized,, uint256 thawEndTimestamp) = escrow.authorizedCollectors(users.gateway, users.verifier); - assertEq(authorized, true); - assertEq(thawEndTimestamp, 0); - } - - // Collector thaw tests - - function testCollector_Thaw() public { - vm.startPrank(users.gateway); - escrow.approveCollector(users.verifier, 1000 ether); - escrow.thawCollector(users.verifier); - vm.stopPrank(); - - (bool authorized,, uint256 thawEndTimestamp) = escrow.authorizedCollectors(users.gateway, users.verifier); - assertEq(authorized, true); - assertEq(thawEndTimestamp, block.timestamp + revokeCollectorThawingPeriod); - } - - // Collector cancel thaw tests - - function testCollector_CancelThaw() public { - vm.startPrank(users.gateway); - escrow.approveCollector(users.verifier, 1000 ether); - escrow.thawCollector(users.verifier); - vm.stopPrank(); - - (bool authorized,, uint256 thawEndTimestamp) = escrow.authorizedCollectors(users.gateway, users.verifier); - assertEq(authorized, true); - assertEq(thawEndTimestamp, block.timestamp + revokeCollectorThawingPeriod); - - vm.prank(users.gateway); - escrow.cancelThawCollector(users.verifier); - - (authorized,, thawEndTimestamp) = escrow.authorizedCollectors(users.gateway, users.verifier); - assertEq(authorized, true); - assertEq(thawEndTimestamp, 0); - } - - function testCollector_RevertWhen_CancelThawIsNotThawing() public { - vm.startPrank(users.gateway); - escrow.approveCollector(users.verifier, 1000 ether); - bytes memory expectedError = abi.encodeWithSignature("GraphEscrowNotThawing()"); - vm.expectRevert(expectedError); - escrow.cancelThawCollector(users.verifier); - vm.stopPrank(); - } - - // Collector revoke tests - - function testCollector_Revoke() public { - vm.startPrank(users.gateway); - escrow.approveCollector(users.verifier, 1000 ether); - escrow.thawCollector(users.verifier); - skip(revokeCollectorThawingPeriod + 1); - escrow.revokeCollector(users.verifier); - vm.stopPrank(); - - (bool authorized,,) = escrow.authorizedCollectors(users.gateway, users.verifier); - assertEq(authorized, false); - } - - function testCollector_RevertWhen_RevokeIsNotThawing() public { - vm.startPrank(users.gateway); - escrow.approveCollector(users.verifier, 1000 ether); - bytes memory expectedError = abi.encodeWithSignature("GraphEscrowNotThawing()"); - vm.expectRevert(expectedError); - escrow.revokeCollector(users.verifier); - vm.stopPrank(); - } - - function testCollector_RevertWhen_RevokeIsStillThawing() public { + modifier useGateway() { vm.startPrank(users.gateway); - escrow.approveCollector(users.verifier, 1000 ether); - escrow.thawCollector(users.verifier); - bytes memory expectedError = abi.encodeWithSignature("GraphEscrowStillThawing(uint256,uint256)", block.timestamp, block.timestamp + revokeCollectorThawingPeriod); - vm.expectRevert(expectedError); - escrow.revokeCollector(users.verifier); + _; vm.stopPrank(); } - // Deposit tests - - function testDeposit_Tokens() public { - mint(users.gateway, 10000 ether); - vm.startPrank(users.gateway); - token.approve(address(escrow), 1000 ether); - escrow.deposit(users.indexer, 1000 ether); - vm.stopPrank(); - - (uint256 indexerEscrowBalance,,) = escrow.escrowAccounts(users.gateway, users.indexer); - assertEq(indexerEscrowBalance, 1000 ether); - } - - function testDeposit_ManyDeposits() public { - address otherIndexer = address(0xB3); - address[] memory indexers = new address[](2); - indexers[0] = users.indexer; - indexers[1] = otherIndexer; - - uint256[] memory amounts = new uint256[](2); - amounts[0] = 1000 ether; - amounts[1] = 2000 ether; - - mint(users.gateway, 3000 ether); - vm.startPrank(users.gateway); - token.approve(address(escrow), 3000 ether); - escrow.depositMany(indexers, amounts); - vm.stopPrank(); - - (uint256 indexerEscrowBalance,,) = escrow.escrowAccounts(users.gateway, users.indexer); - assertEq(indexerEscrowBalance, 1000 ether); - - (uint256 otherIndexerEscrowBalance,,) = escrow.escrowAccounts(users.gateway, otherIndexer); - assertEq(otherIndexerEscrowBalance, 2000 ether); + modifier approveEscrow(uint256 amount) { + _approveEscrow(amount); + _; } - function testDeposit_RevertWhen_ManyDepositsInputsLengthMismatch() public { - address otherIndexer = address(0xB3); - address[] memory indexers = new address[](2); - indexers[0] = users.indexer; - indexers[1] = otherIndexer; - - uint256[] memory amounts = new uint256[](1); - amounts[0] = 1000 ether; - - mint(users.gateway, 1000 ether); - token.approve(address(escrow), 1000 ether); - - // revert - bytes memory expectedError = abi.encodeWithSignature("GraphEscrowInputsLengthMismatch()"); - vm.expectRevert(expectedError); - vm.prank(users.gateway); - escrow.depositMany(indexers, amounts); + modifier depositTokens(uint256 amount) { + vm.assume(amount > 0); + vm.assume(amount <= 10000 ether); + _depositTokens(amount); + _; } - // Thaw tests - - function testThaw_Tokens() public { - mint(users.gateway, 1000 ether); - vm.startPrank(users.gateway); - token.approve(address(escrow), 1000 ether); - escrow.deposit(users.indexer, 1000 ether); - escrow.thaw(users.indexer, 100 ether); - vm.stopPrank(); - - (, uint256 amountThawing,uint256 thawEndTimestamp) = escrow.escrowAccounts(users.gateway, users.indexer); - assertEq(amountThawing, 100 ether); - assertEq(thawEndTimestamp, block.timestamp + withdrawEscrowThawingPeriod); + function setUp() public virtual override { + HorizonStakingSharedTest.setUp(); } - function testThaw_RevertWhen_InsufficientThawAmount() public { - mint(users.gateway, 1000 ether); - vm.startPrank(users.gateway); - token.approve(address(escrow), 1000 ether); - escrow.deposit(users.indexer, 1000 ether); - - // revert - bytes memory expectedError = abi.encodeWithSignature("GraphEscrowInsufficientThawAmount()"); - vm.expectRevert(expectedError); - escrow.thaw(users.indexer, 0); - vm.stopPrank(); - } - - function testThaw_RevertWhen_InsufficientAmount() public { - mint(users.gateway, 1000 ether); - vm.startPrank(users.gateway); - token.approve(address(escrow), 1000 ether); - escrow.deposit(users.indexer, 1000 ether); - - // revert - bytes memory expectedError = abi.encodeWithSignature("GraphEscrowInsufficientAmount(uint256,uint256)", 1000 ether, 2000 ether); - vm.expectRevert(expectedError); - escrow.thaw(users.indexer, 2000 ether); - vm.stopPrank(); - } - - // Withdraw tests - - function testWithdraw_Tokens() public { - mint(users.gateway, 1000 ether); - vm.startPrank(users.gateway); - token.approve(address(escrow), 1000 ether); - escrow.deposit(users.indexer, 1000 ether); - escrow.thaw(users.indexer, 100 ether); - - // advance time - skip(withdrawEscrowThawingPeriod + 1); - - escrow.withdraw(users.indexer); - vm.stopPrank(); - - (uint256 indexerEscrowBalance,,) = escrow.escrowAccounts(users.gateway, users.indexer); - assertEq(indexerEscrowBalance, 900 ether); - } - - function testWithdraw_RevertWhen_NotThawing() public { - mint(users.gateway, 1000 ether); - vm.startPrank(users.gateway); - token.approve(address(escrow), 1000 ether); - escrow.deposit(users.indexer, 1000 ether); - - // revert - bytes memory expectedError = abi.encodeWithSignature("GraphEscrowNotThawing()"); - vm.expectRevert(expectedError); - escrow.withdraw(users.indexer); - vm.stopPrank(); - } - - function testWithdraw_RevertWhen_StillThawing() public { - mint(users.gateway, 1000 ether); - vm.startPrank(users.gateway); - token.approve(address(escrow), 1000 ether); - escrow.deposit(users.indexer, 1000 ether); - escrow.thaw(users.indexer, 100 ether); - - // revert - bytes memory expectedError = abi.encodeWithSignature("GraphEscrowStillThawing(uint256,uint256)", block.timestamp, block.timestamp + withdrawEscrowThawingPeriod); - vm.expectRevert(expectedError); - escrow.withdraw(users.indexer); - vm.stopPrank(); - } - - // Collect tests - - function testCollect() public { - uint256 amount = 1000 ether; - createProvision(amount); - setDelegationFeeCut(0, 100000); - - vm.startPrank(users.gateway); - escrow.approveCollector(users.verifier, 1000 ether); - token.approve(address(escrow), 1000 ether); - escrow.deposit(users.indexer, 1000 ether); - vm.stopPrank(); - - uint256 indexerPreviousBalance = token.balanceOf(users.indexer); - vm.prank(users.verifier); - escrow.collect(users.gateway, users.indexer, subgraphDataServiceAddress, 100 ether, IGraphPayments.PaymentType.IndexingFees, 3 ether); - - uint256 indexerBalance = token.balanceOf(users.indexer); - assertEq(indexerBalance - indexerPreviousBalance, 86 ether); - } - - function testCollect_RevertWhen_CollectorNotAuthorized() public { - address indexer = address(0xA3); - uint256 amount = 1000 ether; - - vm.startPrank(users.verifier); - uint256 dataServiceCut = 30000; // 3% - bytes memory expectedError = abi.encodeWithSignature("GraphEscrowCollectorNotAuthorized(address,address)", users.gateway, users.verifier); - vm.expectRevert(expectedError); - escrow.collect(users.gateway, indexer, subgraphDataServiceAddress, amount, IGraphPayments.PaymentType.IndexingFees, dataServiceCut); - vm.stopPrank(); - } - - function testCollect_RevertWhen_CollectorHasInsufficientAmount() public { - vm.prank(users.gateway); - escrow.approveCollector(users.verifier, 100 ether); - - address indexer = address(0xA3); - uint256 amount = 1000 ether; - - mint(users.gateway, amount); - vm.startPrank(users.gateway); + function _depositTokens(uint256 amount) internal { token.approve(address(escrow), amount); - escrow.deposit(indexer, amount); - vm.stopPrank(); - - vm.startPrank(users.verifier); - uint256 dataServiceCut = 30 ether; - bytes memory expectedError = abi.encodeWithSignature("GraphEscrowCollectorInsufficientAmount(uint256,uint256)", 100 ether, 1000 ether); - vm.expectRevert(expectedError); - escrow.collect(users.gateway, indexer, subgraphDataServiceAddress, 1000 ether, IGraphPayments.PaymentType.IndexingFees, dataServiceCut); - vm.stopPrank(); + escrow.deposit(users.indexer, amount); } - function testCollect_RevertWhen_SenderHasInsufficientAmountInEscrow() public { - mint(users.gateway, 1000 ether); - vm.startPrank(users.gateway); - escrow.approveCollector(users.verifier, 1000 ether); - token.approve(address(escrow), 1000 ether); - escrow.deposit(users.indexer, 100 ether); - vm.stopPrank(); - - vm.prank(users.verifier); - bytes memory expectedError = abi.encodeWithSignature("GraphEscrowInsufficientAmount(uint256,uint256)", 100 ether, 200 ether); - vm.expectRevert(expectedError); - escrow.collect(users.gateway, users.indexer, subgraphDataServiceAddress, 200 ether, IGraphPayments.PaymentType.IndexingFees, 3 ether); - vm.stopPrank(); + function _approveEscrow(uint256 amount) internal { + token.approve(address(escrow), amount); } } \ No newline at end of file diff --git a/packages/horizon/test/escrow/collect.t.sol b/packages/horizon/test/escrow/collect.t.sol new file mode 100644 index 000000000..a1d7a3a5f --- /dev/null +++ b/packages/horizon/test/escrow/collect.t.sol @@ -0,0 +1,75 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +import "forge-std/Test.sol"; + +import { GraphEscrowTest } from "./GraphEscrow.t.sol"; +import { IGraphPayments } from "../../contracts/interfaces/IGraphPayments.sol"; + +contract GraphEscrowCollectTest is GraphEscrowTest { + + function testCollect() public { + uint256 amount = 1000 ether; + createProvision(amount); + setDelegationFeeCut(0, 100000); + + vm.startPrank(users.gateway); + escrow.approveCollector(users.verifier, 1000 ether); + token.approve(address(escrow), 1000 ether); + escrow.deposit(users.indexer, 1000 ether); + vm.stopPrank(); + + uint256 indexerPreviousBalance = token.balanceOf(users.indexer); + vm.prank(users.verifier); + escrow.collect(users.gateway, users.indexer, subgraphDataServiceAddress, 100 ether, IGraphPayments.PaymentType.IndexingFees, 3 ether); + + uint256 indexerBalance = token.balanceOf(users.indexer); + assertEq(indexerBalance - indexerPreviousBalance, 86 ether); + } + + function testCollect_RevertWhen_CollectorNotAuthorized() public { + address indexer = address(0xA3); + uint256 amount = 1000 ether; + + vm.startPrank(users.verifier); + uint256 dataServiceCut = 30000; // 3% + bytes memory expectedError = abi.encodeWithSignature("GraphEscrowCollectorNotAuthorized(address,address)", users.gateway, users.verifier); + vm.expectRevert(expectedError); + escrow.collect(users.gateway, indexer, subgraphDataServiceAddress, amount, IGraphPayments.PaymentType.IndexingFees, dataServiceCut); + vm.stopPrank(); + } + + function testCollect_RevertWhen_CollectorHasInsufficientAmount() public { + vm.prank(users.gateway); + escrow.approveCollector(users.verifier, 100 ether); + + address indexer = address(0xA3); + uint256 amount = 1000 ether; + + vm.startPrank(users.gateway); + token.approve(address(escrow), amount); + escrow.deposit(indexer, amount); + vm.stopPrank(); + + vm.startPrank(users.verifier); + uint256 dataServiceCut = 30 ether; + bytes memory expectedError = abi.encodeWithSignature("GraphEscrowCollectorInsufficientAmount(uint256,uint256)", 100 ether, 1000 ether); + vm.expectRevert(expectedError); + escrow.collect(users.gateway, indexer, subgraphDataServiceAddress, 1000 ether, IGraphPayments.PaymentType.IndexingFees, dataServiceCut); + vm.stopPrank(); + } + + function testCollect_RevertWhen_SenderHasInsufficientAmountInEscrow() public { + vm.startPrank(users.gateway); + escrow.approveCollector(users.verifier, 1000 ether); + token.approve(address(escrow), 1000 ether); + escrow.deposit(users.indexer, 100 ether); + vm.stopPrank(); + + vm.prank(users.verifier); + bytes memory expectedError = abi.encodeWithSignature("GraphEscrowInsufficientAmount(uint256,uint256)", 100 ether, 200 ether); + vm.expectRevert(expectedError); + escrow.collect(users.gateway, users.indexer, subgraphDataServiceAddress, 200 ether, IGraphPayments.PaymentType.IndexingFees, 3 ether); + vm.stopPrank(); + } +} \ No newline at end of file diff --git a/packages/horizon/test/escrow/collector.t.sol b/packages/horizon/test/escrow/collector.t.sol new file mode 100644 index 000000000..f4ef2b11c --- /dev/null +++ b/packages/horizon/test/escrow/collector.t.sol @@ -0,0 +1,87 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +import "forge-std/Test.sol"; + +import { GraphEscrowTest } from "./GraphEscrow.t.sol"; + +contract GraphEscrowCollectorTest is GraphEscrowTest { + function setUp() public virtual override { + GraphEscrowTest.setUp(); + vm.prank(users.gateway); + escrow.approveCollector(users.verifier, 1000 ether); + } + + // Collector approve tests + + function testCollector_Approve() public view { + (bool authorized,, uint256 thawEndTimestamp) = escrow.authorizedCollectors(users.gateway, users.verifier); + assertEq(authorized, true); + assertEq(thawEndTimestamp, 0); + } + + // Collector thaw tests + + function testCollector_Thaw() public { + vm.prank(users.gateway); + escrow.thawCollector(users.verifier); + + (bool authorized,, uint256 thawEndTimestamp) = escrow.authorizedCollectors(users.gateway, users.verifier); + assertEq(authorized, true); + assertEq(thawEndTimestamp, block.timestamp + revokeCollectorThawingPeriod); + } + + // Collector cancel thaw tests + + function testCollector_CancelThaw() public { + vm.prank(users.gateway); + escrow.thawCollector(users.verifier); + + (bool authorized,, uint256 thawEndTimestamp) = escrow.authorizedCollectors(users.gateway, users.verifier); + assertEq(authorized, true); + assertEq(thawEndTimestamp, block.timestamp + revokeCollectorThawingPeriod); + + vm.prank(users.gateway); + escrow.cancelThawCollector(users.verifier); + + (authorized,, thawEndTimestamp) = escrow.authorizedCollectors(users.gateway, users.verifier); + assertEq(authorized, true); + assertEq(thawEndTimestamp, 0); + } + + function testCollector_RevertWhen_CancelThawIsNotThawing() public { + bytes memory expectedError = abi.encodeWithSignature("GraphEscrowNotThawing()"); + vm.expectRevert(expectedError); + escrow.cancelThawCollector(users.verifier); + vm.stopPrank(); + } + + // Collector revoke tests + + function testCollector_Revoke() public { + vm.startPrank(users.gateway); + escrow.thawCollector(users.verifier); + skip(revokeCollectorThawingPeriod + 1); + escrow.revokeCollector(users.verifier); + vm.stopPrank(); + + (bool authorized,,) = escrow.authorizedCollectors(users.gateway, users.verifier); + assertEq(authorized, false); + } + + function testCollector_RevertWhen_RevokeIsNotThawing() public { + bytes memory expectedError = abi.encodeWithSignature("GraphEscrowNotThawing()"); + vm.expectRevert(expectedError); + vm.prank(users.gateway); + escrow.revokeCollector(users.verifier); + } + + function testCollector_RevertWhen_RevokeIsStillThawing() public { + vm.startPrank(users.gateway); + escrow.thawCollector(users.verifier); + bytes memory expectedError = abi.encodeWithSignature("GraphEscrowStillThawing(uint256,uint256)", block.timestamp, block.timestamp + revokeCollectorThawingPeriod); + vm.expectRevert(expectedError); + escrow.revokeCollector(users.verifier); + vm.stopPrank(); + } +} \ No newline at end of file diff --git a/packages/horizon/test/escrow/deposit.t.sol b/packages/horizon/test/escrow/deposit.t.sol new file mode 100644 index 000000000..d7898bd37 --- /dev/null +++ b/packages/horizon/test/escrow/deposit.t.sol @@ -0,0 +1,53 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +import "forge-std/Test.sol"; + +import { GraphEscrowTest } from "./GraphEscrow.t.sol"; + +contract GraphEscrowDepositTest is GraphEscrowTest { + + function testDeposit_Tokens(uint256 amount) public useGateway depositTokens(amount) { + (uint256 indexerEscrowBalance,,) = escrow.escrowAccounts(users.gateway, users.indexer); + assertEq(indexerEscrowBalance, amount); + } + + function testDeposit_ManyDeposits(uint256 amount) public useGateway approveEscrow(amount) { + uint256 amountOne = amount / 2; + uint256 amountTwo = amount - amountOne; + + address otherIndexer = address(0xB3); + address[] memory indexers = new address[](2); + indexers[0] = users.indexer; + indexers[1] = otherIndexer; + + uint256[] memory amounts = new uint256[](2); + amounts[0] = amountOne; + amounts[1] = amountTwo; + + escrow.depositMany(indexers, amounts); + + (uint256 indexerEscrowBalance,,) = escrow.escrowAccounts(users.gateway, users.indexer); + assertEq(indexerEscrowBalance, amountOne); + + (uint256 otherIndexerEscrowBalance,,) = escrow.escrowAccounts(users.gateway, otherIndexer); + assertEq(otherIndexerEscrowBalance, amountTwo); + } + + function testDeposit_RevertWhen_ManyDepositsInputsLengthMismatch( + uint256 amount + ) public useGateway approveEscrow(amount) { + address otherIndexer = address(0xB3); + address[] memory indexers = new address[](2); + indexers[0] = users.indexer; + indexers[1] = otherIndexer; + + uint256[] memory amounts = new uint256[](1); + amounts[0] = 1000 ether; + + // revert + bytes memory expectedError = abi.encodeWithSignature("GraphEscrowInputsLengthMismatch()"); + vm.expectRevert(expectedError); + escrow.depositMany(indexers, amounts); + } +} \ No newline at end of file diff --git a/packages/horizon/test/escrow/thaw.t.sol b/packages/horizon/test/escrow/thaw.t.sol new file mode 100644 index 000000000..51400feac --- /dev/null +++ b/packages/horizon/test/escrow/thaw.t.sol @@ -0,0 +1,34 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +import "forge-std/Test.sol"; + +import { GraphEscrowTest } from "./GraphEscrow.t.sol"; + +contract GraphEscrowThawTest is GraphEscrowTest { + + function testThaw_Tokens(uint256 amount) public useGateway depositTokens(amount) { + escrow.thaw(users.indexer, amount); + + (, uint256 amountThawing,uint256 thawEndTimestamp) = escrow.escrowAccounts(users.gateway, users.indexer); + assertEq(amountThawing, amount); + assertEq(thawEndTimestamp, block.timestamp + withdrawEscrowThawingPeriod); + } + + function testThaw_RevertWhen_InsufficientThawAmount( + uint256 amount + ) public useGateway depositTokens(amount) { + bytes memory expectedError = abi.encodeWithSignature("GraphEscrowInsufficientThawAmount()"); + vm.expectRevert(expectedError); + escrow.thaw(users.indexer, 0); + } + + function testThaw_RevertWhen_InsufficientAmount( + uint256 amount + ) public useGateway depositTokens(amount) { + uint256 overAmount = amount + 1; + bytes memory expectedError = abi.encodeWithSignature("GraphEscrowInsufficientAmount(uint256,uint256)", amount, overAmount); + vm.expectRevert(expectedError); + escrow.thaw(users.indexer, overAmount); + } +} \ No newline at end of file diff --git a/packages/horizon/test/escrow/withdraw.t.sol b/packages/horizon/test/escrow/withdraw.t.sol new file mode 100644 index 000000000..075a16a74 --- /dev/null +++ b/packages/horizon/test/escrow/withdraw.t.sol @@ -0,0 +1,48 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +import "forge-std/Test.sol"; + +import { GraphEscrowTest } from "./GraphEscrow.t.sol"; + +contract GraphEscrowWithdrawTest is GraphEscrowTest { + + modifier depositAndThawTokens(uint256 amount, uint256 thawAmount) { + vm.assume(thawAmount > 0); + vm.assume(amount > thawAmount); + _depositTokens(amount); + escrow.thaw(users.indexer, thawAmount); + _; + } + + function testWithdraw_Tokens( + uint256 amount, + uint256 thawAmount + ) public useGateway depositAndThawTokens(amount, thawAmount) { + // advance time + skip(withdrawEscrowThawingPeriod + 1); + + escrow.withdraw(users.indexer); + vm.stopPrank(); + + (uint256 indexerEscrowBalance,,) = escrow.escrowAccounts(users.gateway, users.indexer); + assertEq(indexerEscrowBalance, amount - thawAmount); + } + + function testWithdraw_RevertWhen_NotThawing(uint256 amount) public useGateway depositTokens(amount) { + bytes memory expectedError = abi.encodeWithSignature("GraphEscrowNotThawing()"); + vm.expectRevert(expectedError); + escrow.withdraw(users.indexer); + vm.stopPrank(); + } + + function testWithdraw_RevertWhen_StillThawing( + uint256 amount, + uint256 thawAmount + ) public useGateway depositAndThawTokens(amount, thawAmount) { + bytes memory expectedError = abi.encodeWithSignature("GraphEscrowStillThawing(uint256,uint256)", block.timestamp, block.timestamp + withdrawEscrowThawingPeriod); + vm.expectRevert(expectedError); + escrow.withdraw(users.indexer); + vm.stopPrank(); + } +} \ No newline at end of file diff --git a/packages/horizon/test/payments/GraphPayments.t.sol b/packages/horizon/test/payments/GraphPayments.t.sol index e7889101f..afcecd803 100644 --- a/packages/horizon/test/payments/GraphPayments.t.sol +++ b/packages/horizon/test/payments/GraphPayments.t.sol @@ -5,9 +5,9 @@ import "forge-std/Test.sol"; import { IGraphPayments } from "../../contracts/interfaces/IGraphPayments.sol"; -import { HorizonStaking_Shared_Test } from "../shared/horizon-staking/HorizonStaking.t.sol"; +import { HorizonStakingSharedTest } from "../shared/horizon-staking/HorizonStaking.t.sol"; -contract GraphPaymentsTest is HorizonStaking_Shared_Test { +contract GraphPaymentsTest is HorizonStakingSharedTest { function testCollect() public { // Setup Staking diff --git a/packages/horizon/test/shared/horizon-staking/HorizonStaking.t.sol b/packages/horizon/test/shared/horizon-staking/HorizonStaking.t.sol index 62a8db3ac..c5aec8aa2 100644 --- a/packages/horizon/test/shared/horizon-staking/HorizonStaking.t.sol +++ b/packages/horizon/test/shared/horizon-staking/HorizonStaking.t.sol @@ -5,15 +5,13 @@ import "forge-std/Test.sol"; import { GraphBaseTest } from "../../GraphBase.t.sol"; -abstract contract HorizonStaking_Shared_Test is GraphBaseTest { +abstract contract HorizonStakingSharedTest is GraphBaseTest { /* Set Up */ - // function setUp() public virtual override { - // GraphBaseTest.setUp(); - - - // } + function setUp() public virtual override { + GraphBaseTest.setUp(); + } /* Helpers */