Skip to content

Commit

Permalink
Merge pull request #2 from edgeandnode/dave/finish-contract
Browse files Browse the repository at this point in the history
Finish the contracts with tests
  • Loading branch information
davekaj authored Jun 4, 2021
2 parents 5a54061 + 500a16d commit ecf69dc
Show file tree
Hide file tree
Showing 17 changed files with 10,015 additions and 39 deletions.
35 changes: 33 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,33 @@
# billing-contracts
Billing project contracts for matic deployment
# Billing Contract

This repository contains a Billing contract to be deployed on the Polygon (previously Matic)
network. It allows Users to add tokens (GRT) into the contract for the Gateway to pull when
a billing period is up.

## Contract Design

The contract is designed with the following requirements:

- The contract is owned by the `governor`
- There is a privileged used named `gateway`
- Users add tokens into the contract by calling `add()`. It is important to note that the contract
is designed so that the users are trusting the Gateway. Once the user adds tokens, the Gateway can
pull the funds whenever it wants. The Gateway is running it's own logic, not on the blockchain, that
records what Users owe.
- The trust risk for the User is that the Gateway would pull funds before the User spent their funds, which are query fees in The Graph Network.
- The trust risk for the Gateway is that the user adds funds, spends them on queries, and then
removes their tokens before the gateway pulled the tokens.
- These combined trust risks make for a situation that we expect both parties to act responsibly.
It will always be recommended to keep the amount of GRT in the contract low for each user, and for
the Gateway to pull regularly. This way, if one side does not play nice, the funds lost won't be
that large.

## Using the Polygon Bridge

The Billing contract will be deployed on Polygon. This is how users will have to use it:

- Move GRT from Ethereum Mainnet through the Polgon POS bridge. After a short 5-7 minute period, they
will get Polygon GRT on the Polygon chain.
- Users will have to get MATIC to pay for transaction fees on the Polygon network.
- If the User ever wants to move their Polygon GRT back to Ethereum, they must use the reverse bridge,
which has about a 3 hour waiting time.
153 changes: 153 additions & 0 deletions contracts/Billing.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
// SPDX-License-Identifier: MIT

pragma solidity ^0.8.4;

import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/utils/math/Math.sol";
import "./IBilling.sol";
import "./Governed.sol";

/**
* @title Billing Contract
* @dev The billing contract allows for Graph Tokens to be added by a user. The token can then
* be pulled by a permissoned user named 'gateway'. It is owned and controlled by the 'governor'.
*/

contract Billing is IBilling, Governed {
IERC20 private immutable graphToken;
address public gateway;

// user address --> user tokens
mapping(address => uint256) public userBalances;

/**
* @dev Constructor function
* @param _gateway Gateway address
* @param _token Graph Token address
* @param _governor Governor address
*/
constructor(
address _gateway,
IERC20 _token,
address _governor
) {
Governed._initialize(_governor);
_setGateway(_gateway);
graphToken = _token;
}

/**
* @dev Check if the caller is the gateway.
*/
modifier onlyGateway() {
require(msg.sender == gateway, "!gateway");
_;
}

/**
* @dev Set the new gateway address
* @param _newGateway New gateway address
*/
function setGateway(address _newGateway) external override onlyGovernor {
_setGateway(_newGateway);
}

/**
* @dev Set the new gateway address
* @param _newGateway New gateway address
*/
function _setGateway(address _newGateway) internal {
require(_newGateway != address(0), "gateway != 0");
gateway = _newGateway;
emit GatewayUpdated(gateway);
}

/**
* @dev Add tokens into the billing contract
* @param _amount Amount of tokens to add
*/
function add(uint256 _amount) external override {
_add(msg.sender, msg.sender, _amount);
}

/**
* @dev Add tokens into the billing contract for any user
* @param _to Address that tokens are being added to
* @param _amount Amount of tokens to add
*/
function addTo(address _to, uint256 _amount) external override {
_add(msg.sender, _to, _amount);
}

/**
* @dev Add tokens into the billing contract
* @param _from Address that is sending tokens
* @param _user User that is adding tokens
* @param _amount Amount of tokens to add
*/
function _add(
address _from,
address _user,
uint256 _amount
) private {
require(graphToken.transferFrom(_from, address(this), _amount), "Add transfer failed");
userBalances[_user] = userBalances[_user] + _amount;
emit TokensAdded(_user, _amount);
}

/**
* @dev Remove tokens from the billing contract
* @param _user Address that tokens are being removed from
* @param _amount Amount of tokens to remove
*/
function remove(address _user, uint256 _amount) external override {
require(userBalances[msg.sender] >= _amount, "Too much removed");
userBalances[msg.sender] = userBalances[msg.sender] - _amount;
require(graphToken.transfer(_user, _amount), "Remove transfer failed");
emit TokensRemoved(msg.sender, _user, _amount);
}

/**
* @dev Gateway pulls tokens from the billing contract
* @param _user Address that tokens are being pulled from
* @param _amount Amount of tokens to pull
*/
function pull(address _user, uint256 _amount) external override onlyGateway {
uint256 maxAmount = _pull(_user, _amount);
if (maxAmount > 0) {
require(graphToken.transfer(gateway, maxAmount), "Pull transfer failed");
}
}

/**
* @dev Gateway pulls tokens from many users in the billing contract
* @param _users Addresses that tokens are being pulled from
* @param _amounts Amounts of tokens to pull from each user
*/
function pullMany(address[] calldata _users, uint256[] calldata _amounts) external override onlyGateway {
require(_users.length == _amounts.length, "Lengths not equal");
uint256 totalPulled;
for (uint256 i = 0; i < _users.length; i++) {
uint256 userMax = _pull(_users[i], _amounts[i]);
totalPulled = totalPulled + userMax;
}
if (totalPulled > 0) {
require(graphToken.transfer(gateway, totalPulled), "Pull Many transfer failed");
}
}

/**
* @dev Gateway pulls tokens from the billing contract. Uses Math.min() so that it won't fail
* in the event that a user removes in front of the gateway pulling
* @param _user Address that tokens are being pulled from
* @param _amount Amount of tokens to pull
*/
function _pull(address _user, uint256 _amount) internal returns (uint256) {
uint256 maxAmount = Math.min(_amount, userBalances[_user]);
if (maxAmount > 0) {
userBalances[_user] = userBalances[_user] - _amount;
emit TokensPulled(_user, _amount);
}
return maxAmount;
}
}
68 changes: 68 additions & 0 deletions contracts/Governed.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
// SPDX-License-Identifier: MIT

pragma solidity ^0.8.4;

/**
* @title Graph Governance contract
* @dev Allows a contract to be owned and controlled by the 'governor'
*/
contract Governed {
// -- State --

address public governor;
address public pendingGovernor;

// -- Events --

event NewPendingOwnership(address indexed from, address indexed to);
event NewOwnership(address indexed from, address indexed to);

/**
* @dev Check if the caller is the governor.
*/
modifier onlyGovernor {
require(msg.sender == governor, "Only Governor can call");
_;
}

/**
* @dev Initialize the governor to the contract caller.
*/
function _initialize(address _initGovernor) internal {
governor = _initGovernor;
}

/**
* @dev Admin function to begin change of governor. The `_newGovernor` must call
* `acceptOwnership` to finalize the transfer.
* @param _newGovernor Address of new `governor`
*/
function transferOwnership(address _newGovernor) external onlyGovernor {
require(_newGovernor != address(0), "Governor must be set");

address oldPendingGovernor = pendingGovernor;
pendingGovernor = _newGovernor;

emit NewPendingOwnership(oldPendingGovernor, pendingGovernor);
}

/**
* @dev Admin function for pending governor to accept role and update governor.
* This function must called by the pending governor.
*/
function acceptOwnership() external {
require(
pendingGovernor != address(0) && msg.sender == pendingGovernor,
"Caller must be pending governor"
);

address oldGovernor = governor;
address oldPendingGovernor = pendingGovernor;

governor = pendingGovernor;
pendingGovernor = address(0);

emit NewOwnership(oldGovernor, governor);
emit NewPendingOwnership(oldPendingGovernor, pendingGovernor);
}
}
64 changes: 64 additions & 0 deletions contracts/IBilling.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
// SPDX-License-Identifier: MIT

pragma solidity ^0.8.4;

interface IBilling {
/**
* @dev User adds tokens
*/
event TokensAdded(address indexed user, uint256 amount);
/**
* @dev User removes tokens
*/
event TokensRemoved(address indexed user, address indexed to, uint256 amount);

/**
* @dev Gateway pulled tokens from a user
*/
event TokensPulled(address indexed user, uint256 amount);

/**
* @dev Gateway address updated
*/
event GatewayUpdated(address indexed newGateway);

/**
* @dev Set the new gateway address
* @param _newGateway New gateway address
*/
function setGateway(address _newGateway) external; // onlyGateway or onlyGovernor, or something

/**
* @dev Add tokens into the billing contract
* @param _amount Amount of tokens to add
*/
function add(uint256 _amount) external;

/**
* @dev Add tokens into the billing contract for any user
* @param _to Address that tokens are being added to
* @param _amount Amount of tokens to add
*/
function addTo(address _to, uint256 _amount) external;

/**
* @dev Remove tokens from the billing contract
* @param _to Address that tokens are being removed from
* @param _amount Amount of tokens to remove
*/
function remove(address _to, uint256 _amount) external;

/**
* @dev Gateway pulls tokens from the billing contract
* @param _user Address that tokens are being pulled from
* @param _amount Amount of tokens to pull
*/
function pull(address _user, uint256 _amount) external;

/**
* @dev Gateway pulls tokens from many users in the billing contract
* @param _users Addresses that tokens are being pulled from
* @param _amounts Amounts of tokens to pull from each user
*/
function pullMany(address[] calldata _users, uint256[] calldata _amounts) external;
}
14 changes: 14 additions & 0 deletions contracts/tests/GovernedMock.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
// SPDX-License-Identifier: MIT

pragma solidity ^0.8.4;

import "../Governed.sol";

/**
* @title GovernedMock contract
*/
contract GovernedMock is Governed {
constructor() {
Governed._initialize(msg.sender);
}
}
21 changes: 21 additions & 0 deletions contracts/tests/Token.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
// SPDX-License-Identifier: MIT

pragma solidity ^0.8.4;

import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/access/Ownable.sol";

/**
* @title Token contract
* @dev Used for testing purposes
*
*/
contract Token is ERC20, Ownable {
/**
* @dev Token Contract Constructor.
* @param _initialSupply Initial supply of GRT
*/
constructor(uint256 _initialSupply) ERC20("Graph Token", "GRT") {
_mint(msg.sender, _initialSupply);
}
}
Loading

0 comments on commit ecf69dc

Please sign in to comment.