Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use a minimal proxy for the curation shares ERC20 #505

Merged
merged 5 commits into from
Nov 29, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions cli/commands/migrate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ const allContracts = [
'Controller',
'EpochManager',
'GraphToken',
'GraphCurationToken',
'ServiceRegistry',
'Curation',
'GNS',
Expand Down
86 changes: 46 additions & 40 deletions contracts/curation/Curation.sol
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,13 @@

pragma solidity ^0.7.6;

import "@openzeppelin/contracts/utils/Address.sol";
import "@openzeppelin/contracts/math/SafeMath.sol";
import "@openzeppelin/contracts/proxy/Clones.sol";

import "../bancor/BancorFormula.sol";
import "../upgrades/GraphUpgradeable.sol";
import "../utils/TokenUtils.sol";

import "./CurationStorage.sol";
import "./ICuration.sol";
Expand All @@ -23,7 +26,7 @@ import "./GraphCurationToken.sol";
* Holders can burn GCS using this contract to get GRT tokens back according to the
* bonding curve.
*/
contract Curation is CurationV1Storage, GraphUpgradeable, ICuration {
contract Curation is CurationV1Storage, GraphUpgradeable {
using SafeMath for uint256;

// 100% in parts per million
Expand Down Expand Up @@ -70,6 +73,7 @@ contract Curation is CurationV1Storage, GraphUpgradeable, ICuration {
function initialize(
address _controller,
address _bondingCurve,
address _curationTokenMaster,
uint32 _defaultReserveRatio,
uint32 _curationTaxPercentage,
uint256 _minimumCurationDeposit
Expand All @@ -83,6 +87,7 @@ contract Curation is CurationV1Storage, GraphUpgradeable, ICuration {
_setDefaultReserveRatio(_defaultReserveRatio);
_setCurationTaxPercentage(_curationTaxPercentage);
_setMinimumCurationDeposit(_minimumCurationDeposit);
_setCurationTokenMaster(_curationTokenMaster);
}

/**
Expand Down Expand Up @@ -154,10 +159,30 @@ contract Curation is CurationV1Storage, GraphUpgradeable, ICuration {
"Curation tax percentage must be below or equal to MAX_PPM"
);

_curationTaxPercentage = _percentage;
curationTaxPercentage = _percentage;
emit ParameterUpdated("curationTaxPercentage");
}

/**
* @dev Set the master copy to use as clones for the curation token.
* @param _curationTokenMaster Address of implementation contract to use for curation tokens
*/
function setCurationTokenMaster(address _curationTokenMaster) external override onlyGovernor {
_setCurationTokenMaster(_curationTokenMaster);
}

/**
* @dev Internal: Set the master copy to use as clones for the curation token.
* @param _curationTokenMaster Address of implementation contract to use for curation tokens
*/
function _setCurationTokenMaster(address _curationTokenMaster) private {
require(_curationTokenMaster != address(0), "Token master must be non-empty");
require(Address.isContract(_curationTokenMaster), "Token master must be a contract");

curationTokenMaster = _curationTokenMaster;
emit ParameterUpdated("curationTokenMaster");
}

/**
* @dev Assign Graph Tokens collected as curation fees to the curation pool reserve.
* This function can only be called by the Staking contract and will do the bookeeping of
Expand Down Expand Up @@ -208,36 +233,27 @@ contract Curation is CurationV1Storage, GraphUpgradeable, ICuration {

// If it hasn't been curated before then initialize the curve
if (!isCurated(_subgraphDeploymentID)) {
// Initialize
curationPool.reserveRatio = defaultReserveRatio;

// If no signal token for the pool - create one
if (address(curationPool.gcs) == address(0)) {
// TODO: Use a minimal proxy to reduce gas cost
// https://github.com/graphprotocol/contracts/issues/405
// --abarmat-- 20201113
curationPool.gcs = IGraphCurationToken(
address(new GraphCurationToken(address(this)))
);
// Use a minimal proxy to reduce gas cost
IGraphCurationToken gcs = IGraphCurationToken(Clones.clone(curationTokenMaster));
gcs.initialize(address(this));
curationPool.gcs = gcs;
}
}

// Trigger update rewards calculation snapshot
_updateRewards(_subgraphDeploymentID);

// Transfer tokens from the curator to this contract
// This needs to happen after _updateRewards snapshot as that function
// Burn the curation tax
// NOTE: This needs to happen after _updateRewards snapshot as that function
// is using balanceOf(curation)
IGraphToken graphToken = graphToken();
require(
graphToken.transferFrom(curator, address(this), _tokensIn),
"Cannot transfer tokens to deposit"
);

// Burn withdrawal fees
if (curationTax > 0) {
graphToken.burn(curationTax);
}
IGraphToken _graphToken = graphToken();
TokenUtils.pullTokens(_graphToken, curator, _tokensIn);
TokenUtils.burnTokens(_graphToken, curationTax);

// Update curation pool
curationPool.tokens = curationPool.tokens.add(_tokensIn.sub(curationTax));
Expand Down Expand Up @@ -284,13 +300,15 @@ contract Curation is CurationV1Storage, GraphUpgradeable, ICuration {
curationPool.tokens = curationPool.tokens.sub(tokensOut);
curationPool.gcs.burnFrom(curator, _signalIn);

// If all signal burnt delete the curation pool
// If all signal burnt delete the curation pool except for the
// curation token contract to avoid recreating it on a new mint
if (getCurationPoolSignal(_subgraphDeploymentID) == 0) {
delete pools[_subgraphDeploymentID];
curationPool.tokens = 0;
curationPool.reserveRatio = 0;
}

// Return the tokens to the curator
require(graphToken().transfer(curator, tokensOut), "Error sending curator tokens");
TokenUtils.pushTokens(graphToken(), curator, tokensOut);

emit Burned(curator, _subgraphDeploymentID, tokensOut, _signalIn);

Expand Down Expand Up @@ -318,10 +336,8 @@ contract Curation is CurationV1Storage, GraphUpgradeable, ICuration {
override
returns (uint256)
{
if (address(pools[_subgraphDeploymentID].gcs) == address(0)) {
return 0;
}
return pools[_subgraphDeploymentID].gcs.balanceOf(_curator);
IGraphCurationToken gcs = pools[_subgraphDeploymentID].gcs;
return (address(gcs) == address(0)) ? 0 : gcs.balanceOf(_curator);
}

/**
Expand All @@ -335,10 +351,8 @@ contract Curation is CurationV1Storage, GraphUpgradeable, ICuration {
override
returns (uint256)
{
if (address(pools[_subgraphDeploymentID].gcs) == address(0)) {
return 0;
}
return pools[_subgraphDeploymentID].gcs.totalSupply();
IGraphCurationToken gcs = pools[_subgraphDeploymentID].gcs;
return (address(gcs) == address(0)) ? 0 : gcs.totalSupply();
}

/**
Expand All @@ -355,14 +369,6 @@ contract Curation is CurationV1Storage, GraphUpgradeable, ICuration {
return pools[_subgraphDeploymentID].tokens;
}

/**
* @dev Get curation tax percentage
* @return Amount the curation tax percentage in PPM
*/
function curationTaxPercentage() external view override returns (uint32) {
return _curationTaxPercentage;
}

/**
* @dev Calculate amount of signal that can be bought with tokens in a curation pool.
* This function considers and excludes the deposit tax.
Expand All @@ -376,7 +382,7 @@ contract Curation is CurationV1Storage, GraphUpgradeable, ICuration {
override
returns (uint256, uint256)
{
uint256 curationTax = _tokensIn.mul(uint256(_curationTaxPercentage)).div(MAX_PPM);
uint256 curationTax = _tokensIn.mul(uint256(curationTaxPercentage)).div(MAX_PPM);
uint256 signalOut = _tokensToSignal(_subgraphDeploymentID, _tokensIn.sub(curationTax));
return (signalOut, curationTax);
}
Expand Down
21 changes: 16 additions & 5 deletions contracts/curation/CurationStorage.sol
Original file line number Diff line number Diff line change
Expand Up @@ -2,28 +2,39 @@

pragma solidity ^0.7.6;

import "./ICuration.sol";
import "../governance/Managed.sol";

contract CurationV1Storage is Managed {
abstract contract CurationV1Storage is Managed, ICuration {
// -- Pool --

struct CurationPool {
uint256 tokens; // GRT Tokens stored as reserves for the subgraph deployment
uint32 reserveRatio; // Ratio for the bonding curve
IGraphCurationToken gcs; // Curation token contract for this curation pool
}

// -- State --

// Tax charged when curator deposit funds
// Parts per million. (Allows for 4 decimal points, 999,999 = 99.9999%)
uint32 internal _curationTaxPercentage;
uint32 public override curationTaxPercentage;

// Default reserve ratio to configure curator shares bonding curve
// Parts per million. (Allows for 4 decimal points, 999,999 = 99.9999%)
uint32 public defaultReserveRatio;

// Master copy address that holds implementation of curation token
// This is used as the target for GraphCurationToken clones
address public curationTokenMaster;

// Minimum amount allowed to be deposited by curators to initialize a pool
// This is the `startPoolBalance` for the bonding curve
uint256 public minimumCurationDeposit;

// Bonding curve formula
// Bonding curve library
address public bondingCurve;

// Mapping of subgraphDeploymentID => CurationPool
// There is only one CurationPool per SubgraphDeploymentID
mapping(bytes32 => ICuration.CurationPool) public pools;
mapping(bytes32 => CurationPool) public pools;
}
13 changes: 9 additions & 4 deletions contracts/curation/GraphCurationToken.sol
Original file line number Diff line number Diff line change
Expand Up @@ -2,25 +2,30 @@

pragma solidity ^0.7.6;

import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol";

import "../governance/Governed.sol";

/**
* @title GraphCurationToken contract
* @dev This is the implementation of the Curation ERC20 token (GCS).
*
* GCS are created for each subgraph deployment curated in the Curation contract.
* The Curation contract is the owner of GCS tokens and the only one allowed to mint or
* burn them. GCS tokens are transferrable and their holders can do any action allowed
* in a standard ERC20 token implementation except for burning them.
*
* This contract is meant to be used as the implementation for Minimal Proxy clones for
* gas-saving purposes.
*/
contract GraphCurationToken is ERC20, Governed {
contract GraphCurationToken is ERC20Upgradeable, Governed {
/**
* @dev Graph Curation Token Contract Constructor.
* @dev Graph Curation Token Contract initializer.
* @param _owner Address of the contract issuing this token
*/
constructor(address _owner) ERC20("Graph Curation Share", "GCS") {
function initialize(address _owner) external initializer {
Governed._initialize(_owner);
ERC20Upgradeable.__ERC20_init("Graph Curation Share", "GCS");
}

/**
Expand Down
10 changes: 2 additions & 8 deletions contracts/curation/ICuration.sol
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,6 @@ pragma solidity ^0.7.6;
import "./IGraphCurationToken.sol";

interface ICuration {
// -- Pool --

struct CurationPool {
uint256 tokens; // GRT Tokens stored as reserves for the subgraph deployment
uint32 reserveRatio; // Ratio for the bonding curve
IGraphCurationToken gcs; // Curation token contract for this curation pool
}

// -- Configuration --

function setDefaultReserveRatio(uint32 _defaultReserveRatio) external;
Expand All @@ -21,6 +13,8 @@ interface ICuration {

function setCurationTaxPercentage(uint32 _percentage) external;

function setCurationTokenMaster(address _curationTokenMaster) external;

// -- Curation --

function mint(
Expand Down
6 changes: 4 additions & 2 deletions contracts/curation/IGraphCurationToken.sol
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,11 @@

pragma solidity ^0.7.6;

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

interface IGraphCurationToken is IERC20Upgradeable {
function initialize(address _owner) external;

interface IGraphCurationToken is IERC20 {
function burnFrom(address _account, uint256 _amount) external;

function mint(address _to, uint256 _amount) external;
Expand Down
3 changes: 2 additions & 1 deletion graph.config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -39,12 +39,13 @@ contracts:
initialSupply: "10000000000000000000000000000" # 10,000,000,000 GRT
calls:
- fn: "addMinter"
minter: "${{RewardsManager.address}}"
minter: "${{RewardsManager.address}}"
Curation:
proxy: true
init:
controller: "${{Controller.address}}"
bondingCurve: "${{BancorFormula.address}}"
curationTokenMaster: "${{GraphCurationToken.address}}"
reserveRatio: 500000 # 50% (parts per million)
curationTaxPercentage: 25000 # 2.5% (parts per million)
minimumCurationDeposit: "1000000000000000000" # 1 GRT
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
"@nomiclabs/hardhat-etherscan": "^2.1.1",
"@nomiclabs/hardhat-waffle": "^2.0.1",
"@openzeppelin/contracts": "^3.4.1",
"@openzeppelin/contracts-upgradeable": "3.4.2",
"@openzeppelin/hardhat-upgrades": "^1.6.0",
"@tenderly/hardhat-tenderly": "^1.0.11",
"@typechain/ethers-v5": "^7.0.0",
Expand Down
30 changes: 29 additions & 1 deletion test/curation/configuration.test.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
import { expect } from 'chai'
import { constants } from 'ethers'

import { Curation } from '../../build/types/Curation'

import { defaults } from '../lib/deployment'
import { NetworkFixture } from '../lib/fixtures'
import { getAccounts, toBN, Account } from '../lib/testHelpers'
import { getAccounts, toBN, Account, randomAddress } from '../lib/testHelpers'

const { AddressZero } = constants

const MAX_PPM = 1000000

Expand Down Expand Up @@ -99,4 +102,29 @@ describe('Curation:Config', () => {
await expect(tx).revertedWith('Caller must be Controller governor')
})
})

describe('curationTokenMaster', function () {
it('should set `curationTokenMaster`', async function () {
const newCurationTokenMaster = curation.address
await curation.connect(governor.signer).setCurationTokenMaster(newCurationTokenMaster)
})

it('reject set `curationTokenMaster` to empty value', async function () {
const newCurationTokenMaster = AddressZero
const tx = curation.connect(governor.signer).setCurationTokenMaster(newCurationTokenMaster)
await expect(tx).revertedWith('Token master must be non-empty')
})

it('reject set `curationTokenMaster` to non-contract', async function () {
const newCurationTokenMaster = randomAddress()
const tx = curation.connect(governor.signer).setCurationTokenMaster(newCurationTokenMaster)
await expect(tx).revertedWith('Token master must be a contract')
})

it('reject set `curationTokenMaster` if not allowed', async function () {
const newCurationTokenMaster = curation.address
const tx = curation.connect(me.signer).setCurationTokenMaster(newCurationTokenMaster)
await expect(tx).revertedWith('Caller must be Controller governor')
})
})
})
Loading