Skip to content

Commit 44be3f0

Browse files
authored
curation: signal as an ERC20 token (#252)
A signal token (GST) for each curated subgraph deployment
1 parent 7408e5c commit 44be3f0

File tree

8 files changed

+161
-69
lines changed

8 files changed

+161
-69
lines changed

contracts/bancor/BancorFormula.sol

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -189,7 +189,8 @@ contract BancorFormula {
189189
) public view returns (uint256) {
190190
// validate input
191191
require(
192-
_supply > 0 && _reserveBalance > 0 && _reserveRatio > 0 && _reserveRatio <= MAX_RATIO
192+
_supply > 0 && _reserveBalance > 0 && _reserveRatio > 0 && _reserveRatio <= MAX_RATIO,
193+
"invalid parameters"
193194
);
194195

195196
// special case for 0 deposit amount

contracts/curation/Curation.sol

Lines changed: 69 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,14 @@ import "./ICuration.sol";
1111
/**
1212
* @title Curation contract
1313
* @dev Allows curators to signal on subgraph deployments that might be relevant to indexers by
14-
* staking Graph Tokens. Additionally, curators earn fees from the Query Market related to the
14+
* staking Graph Tokens (GRT). Additionally, curators earn fees from the Query Market related to the
1515
* subgraph deployment they curate.
1616
* A curators stake goes to a curation pool along with the stakes of other curators,
17-
* only one pool exists for each subgraph deployment.
17+
* only one such pool exists for each subgraph deployment.
18+
* The contract mints Graph Signal Tokens (GST) according to a bonding curve for each individual
19+
* curation pool where GRT is deposited.
20+
* Holders can burn GST tokens using this contract to get GRT tokens back according to the
21+
* bonding curve.
1822
*/
1923
contract Curation is CurationV1Storage, ICuration, Governed {
2024
using SafeMath for uint256;
@@ -204,19 +208,18 @@ contract Curation is CurationV1Storage, ICuration, Governed {
204208
*/
205209
function burn(bytes32 _subgraphDeploymentID, uint256 _signal) external override {
206210
address curator = msg.sender;
207-
CurationPool storage curationPool = pools[_subgraphDeploymentID];
208211

209212
require(_signal > 0, "Cannot burn zero signal");
210213
require(
211-
curationPool.curatorSignal[curator] >= _signal,
214+
getCuratorSignal(curator, _subgraphDeploymentID) >= _signal,
212215
"Cannot burn more signal than you own"
213216
);
214217

215218
// Update balance and get the amount of tokens to refund based on returned signal
216219
uint256 tokens = _burnSignal(curator, _subgraphDeploymentID, _signal);
217220

218221
// If all signal burnt delete the curation pool
219-
if (curationPool.signal == 0) {
222+
if (getCurationPoolSignal(_subgraphDeploymentID) == 0) {
220223
delete pools[_subgraphDeploymentID];
221224
}
222225

@@ -238,31 +241,43 @@ contract Curation is CurationV1Storage, ICuration, Governed {
238241
* @param _subgraphDeploymentID SubgraphDeployment to check if curated
239242
* @return True if curated
240243
*/
241-
function isCurated(bytes32 _subgraphDeploymentID) external override view returns (bool) {
242-
return _isCurated(_subgraphDeploymentID);
244+
function isCurated(bytes32 _subgraphDeploymentID) public override view returns (bool) {
245+
return pools[_subgraphDeploymentID].tokens > 0;
243246
}
244247

245248
/**
246-
* @dev Check if any Graph tokens are staked for a SubgraphDeployment.
247-
* @param _subgraphDeploymentID SubgraphDeployment to check if curated
248-
* @return True if curated
249+
* @dev Get the amount of signal a curator has in a curation pool.
250+
* @param _curator Curator owning the signal tokens
251+
* @param _subgraphDeploymentID Subgraph deployment curation pool
252+
* @return Amount of signal owned by a curator for the subgraph deployment
249253
*/
250-
function _isCurated(bytes32 _subgraphDeploymentID) private view returns (bool) {
251-
return pools[_subgraphDeploymentID].tokens > 0;
254+
function getCuratorSignal(address _curator, bytes32 _subgraphDeploymentID)
255+
public
256+
override
257+
view
258+
returns (uint256)
259+
{
260+
if (address(pools[_subgraphDeploymentID].gst) == address(0)) {
261+
return 0;
262+
}
263+
return pools[_subgraphDeploymentID].gst.balanceOf(_curator);
252264
}
253265

254266
/**
255-
* @dev Get the amount of signal a curator has on a curation pool.
256-
* @param _curator Curator owning signal
257-
* @param _subgraphDeploymentID SubgraphDeployment of issued signal
258-
* @return Amount of signal owned by a curator for the SubgraphDeployment
267+
* @dev Get the amount of signal in a curation pool.
268+
* @param _subgraphDeploymentID Subgraph deployment curation poool
269+
* @return Amount of signal owned by a curator for the subgraph deployment
259270
*/
260-
function getCuratorSignal(address _curator, bytes32 _subgraphDeploymentID)
271+
function getCurationPoolSignal(bytes32 _subgraphDeploymentID)
261272
public
273+
override
262274
view
263275
returns (uint256)
264276
{
265-
return pools[_subgraphDeploymentID].curatorSignal[_curator];
277+
if (address(pools[_subgraphDeploymentID].gst) == address(0)) {
278+
return 0;
279+
}
280+
return pools[_subgraphDeploymentID].gst.totalSupply();
266281
}
267282

268283
/**
@@ -273,30 +288,28 @@ contract Curation is CurationV1Storage, ICuration, Governed {
273288
*/
274289
function tokensToSignal(bytes32 _subgraphDeploymentID, uint256 _tokens)
275290
public
291+
override
276292
view
277293
returns (uint256)
278294
{
279-
// Handle initialization of bonding curve
280-
uint256 tokens = _tokens;
281-
uint256 signal = 0;
295+
// Get current tokens and signal
282296
CurationPool memory curationPool = pools[_subgraphDeploymentID];
297+
uint256 newTokens = _tokens;
298+
uint256 curTokens = curationPool.tokens;
299+
uint256 curSignal = getCurationPoolSignal(_subgraphDeploymentID);
300+
uint32 reserveRatio = curationPool.reserveRatio;
301+
302+
// Init curation pool
283303
if (curationPool.tokens == 0) {
284-
curationPool = CurationPool(
285-
minimumCurationStake,
286-
SIGNAL_PER_MINIMUM_STAKE,
287-
defaultReserveRatio
288-
);
289-
tokens = tokens.sub(curationPool.tokens);
290-
signal = curationPool.signal;
304+
newTokens = newTokens.sub(minimumCurationStake);
305+
curTokens = minimumCurationStake;
306+
curSignal = SIGNAL_PER_MINIMUM_STAKE;
307+
reserveRatio = defaultReserveRatio;
291308
}
292309

293-
return
294-
calculatePurchaseReturn(
295-
curationPool.signal,
296-
curationPool.tokens,
297-
uint32(curationPool.reserveRatio),
298-
tokens
299-
) + signal;
310+
// Calculate new signal
311+
uint256 newSignal = calculatePurchaseReturn(curSignal, curTokens, reserveRatio, newTokens);
312+
return newSignal.add(curSignal);
300313
}
301314

302315
/**
@@ -307,21 +320,23 @@ contract Curation is CurationV1Storage, ICuration, Governed {
307320
*/
308321
function signalToTokens(bytes32 _subgraphDeploymentID, uint256 _signal)
309322
public
323+
override
310324
view
311325
returns (uint256)
312326
{
313327
CurationPool memory curationPool = pools[_subgraphDeploymentID];
328+
uint256 curationPoolSignal = getCurationPoolSignal(_subgraphDeploymentID);
314329
require(
315330
curationPool.tokens > 0,
316331
"Subgraph deployment must be curated to perform calculations"
317332
);
318333
require(
319-
curationPool.signal >= _signal,
334+
curationPoolSignal >= _signal,
320335
"Signal must be above or equal to signal issued in the curation pool"
321336
);
322337
return
323338
calculateSaleReturn(
324-
curationPool.signal,
339+
curationPoolSignal,
325340
curationPool.tokens,
326341
uint32(curationPool.reserveRatio),
327342
_signal
@@ -332,8 +347,8 @@ contract Curation is CurationV1Storage, ICuration, Governed {
332347
* @dev Update balances after mint of signal and deposit of tokens.
333348
* @param _curator Curator address
334349
* @param _subgraphDeploymentID Subgraph deployment from where to mint signal
335-
* @param _tokens Amount of tokens
336-
* @return Amount of signal bought
350+
* @param _tokens Amount of tokens to deposit
351+
* @return Amount of signal minted
337352
*/
338353
function _mintSignal(
339354
address _curator,
@@ -343,12 +358,11 @@ contract Curation is CurationV1Storage, ICuration, Governed {
343358
CurationPool storage curationPool = pools[_subgraphDeploymentID];
344359
uint256 signal = tokensToSignal(_subgraphDeploymentID, _tokens);
345360

346-
// Update tokens
361+
// Update GRT tokens held as reserves
347362
curationPool.tokens = curationPool.tokens.add(_tokens);
348363

349-
// Update signal
350-
curationPool.signal = curationPool.signal.add(signal);
351-
curationPool.curatorSignal[_curator] = curationPool.curatorSignal[_curator].add(signal);
364+
// Mint signal to the curator
365+
curationPool.gst.mint(_curator, signal);
352366

353367
return signal;
354368
}
@@ -357,7 +371,7 @@ contract Curation is CurationV1Storage, ICuration, Governed {
357371
* @dev Update balances after burn of signal and return of tokens.
358372
* @param _curator Curator address
359373
* @param _subgraphDeploymentID Subgraph deployment pool to burn signal
360-
* @param _signal Amount of signal
374+
* @param _signal Amount of signal to burn
361375
* @return Number of tokens received
362376
*/
363377
function _burnSignal(
@@ -368,12 +382,11 @@ contract Curation is CurationV1Storage, ICuration, Governed {
368382
CurationPool storage curationPool = pools[_subgraphDeploymentID];
369383
uint256 tokens = signalToTokens(_subgraphDeploymentID, _signal);
370384

371-
// Update tokens
385+
// Update GRT tokens held as reserves
372386
curationPool.tokens = curationPool.tokens.sub(tokens);
373387

374-
// Update signal
375-
curationPool.signal = curationPool.signal.sub(_signal);
376-
curationPool.curatorSignal[_curator] = curationPool.curatorSignal[_curator].sub(_signal);
388+
// Burn signal from curator
389+
curationPool.gst.burnFrom(_curator, _signal);
377390

378391
return tokens;
379392
}
@@ -385,7 +398,7 @@ contract Curation is CurationV1Storage, ICuration, Governed {
385398
*/
386399
function _collect(bytes32 _subgraphDeploymentID, uint256 _tokens) private {
387400
require(
388-
_isCurated(_subgraphDeploymentID),
401+
isCurated(_subgraphDeploymentID),
389402
"Subgraph deployment must be curated to collect fees"
390403
);
391404

@@ -410,11 +423,17 @@ contract Curation is CurationV1Storage, ICuration, Governed {
410423
CurationPool storage curationPool = pools[_subgraphDeploymentID];
411424

412425
// If it hasn't been curated before then initialize the curve
413-
if (!_isCurated(_subgraphDeploymentID)) {
426+
if (!isCurated(_subgraphDeploymentID)) {
414427
require(_tokens >= minimumCurationStake, "Curation stake is below minimum required");
415428

416429
// Initialize
417430
curationPool.reserveRatio = defaultReserveRatio;
431+
432+
// If no signal token for the pool - create one
433+
if (address(curationPool.gst) == address(0)) {
434+
string memory symbol = string(abi.encodePacked("GST-", _subgraphDeploymentID));
435+
curationPool.gst = new GraphSignalToken(symbol, address(this));
436+
}
418437
}
419438

420439
// Update balances

contracts/curation/CurationStorage.sol

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,15 @@ import "../staking/IStaking.sol";
55
import "../token/IGraphToken.sol";
66
import "../upgrades/GraphProxyStorage.sol";
77

8+
import "./GraphSignalToken.sol";
9+
810
contract CurationV1Storage is GraphProxyStorage, BancorFormula {
9-
// -- Curation --
11+
// -- Pool --
1012

1113
struct CurationPool {
12-
uint256 tokens; // Tokens stored as reserves for the SubgraphDeployment
13-
uint256 signal; // Signal issued for the SubgraphDeployment
14+
uint256 tokens; // GRT Tokens stored as reserves for the subgraph deployment
1415
uint32 reserveRatio; // Ratio for the bonding curve
15-
mapping(address => uint256) curatorSignal; // Mapping of curator => signal
16+
GraphSignalToken gst; // Signal token contract for this curation pool
1617
}
1718

1819
// -- State --
@@ -29,13 +30,15 @@ contract CurationV1Storage is GraphProxyStorage, BancorFormula {
2930
// This is the `startPoolBalance` for the bonding curve
3031
uint256 public minimumCurationStake;
3132

33+
// Mapping of subgraphDeploymentID => CurationPool
34+
// There is only one CurationPool per SubgraphDeploymentID
35+
mapping(bytes32 => CurationPool) public pools;
36+
37+
// -- Related contracts --
38+
3239
// Address of the staking contract that will distribute fees to reserves
3340
IStaking public staking;
3441

3542
// Token used for staking
3643
IGraphToken public token;
37-
38-
// Mapping of subgraphDeploymentID => CurationPool
39-
// There is only one CurationPool per SubgraphDeployment
40-
mapping(bytes32 => CurationPool) public pools;
4144
}
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
pragma solidity ^0.6.4;
2+
3+
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
4+
5+
/**
6+
* @title GraphSignalToken contract
7+
* @dev This is the implementation of the Curation Signal ERC20 token (GST).
8+
* GST tokens are created for each subgraph deployment curated in the Curation contract.
9+
* The Curation contract is the owner of GST tokens and the only one allowed to mint or
10+
* burn them. GST tokens are transferrable and their holders can do any action allowed
11+
* in a standard ERC20 token implementation except for burning them.
12+
*/
13+
contract GraphSignalToken is ERC20 {
14+
address public owner;
15+
16+
modifier onlyOwner {
17+
require(msg.sender == owner, "Caller must be owner");
18+
_;
19+
}
20+
21+
/**
22+
* @dev Graph Token Contract Constructor.
23+
* @param _symbol Token symbol
24+
* @param _owner Address of the contract issuing this token
25+
*/
26+
constructor(string memory _symbol, address _owner) public ERC20("Graph Signal Token", _symbol) {
27+
owner = _owner;
28+
}
29+
30+
/**
31+
* @dev Mint new tokens.
32+
* @param _to Address to send the newly minted tokens
33+
* @param _amount Amount of tokens to mint
34+
*/
35+
function mint(address _to, uint256 _amount) public onlyOwner {
36+
_mint(_to, _amount);
37+
}
38+
39+
/**
40+
* @dev Burn tokens from an address.
41+
* @param _account Address from where tokens will be burned
42+
* @param _amount Amount of tokens to burn
43+
*/
44+
function burnFrom(address _account, uint256 _amount) public onlyOwner {
45+
_burn(_account, _amount);
46+
}
47+
}

contracts/curation/ICuration.sol

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,4 +22,21 @@ interface ICuration {
2222
// -- Getters --
2323

2424
function isCurated(bytes32 _subgraphDeploymentID) external view returns (bool);
25+
26+
function getCuratorSignal(address _curator, bytes32 _subgraphDeploymentID)
27+
external
28+
view
29+
returns (uint256);
30+
31+
function getCurationPoolSignal(bytes32 _subgraphDeploymentID) external view returns (uint256);
32+
33+
function tokensToSignal(bytes32 _subgraphDeploymentID, uint256 _tokens)
34+
external
35+
view
36+
returns (uint256);
37+
38+
function signalToTokens(bytes32 _subgraphDeploymentID, uint256 _signal)
39+
external
40+
view
41+
returns (uint256);
2542
}

contracts/staking/Staking.sol

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1022,7 +1022,7 @@ contract Staking is StakingV1Storage, IStaking, Governed {
10221022
// Collect funds in the allocated channel
10231023
alloc.collectedFees = alloc.collectedFees.add(rebateFees);
10241024

1025-
// When channel allocation is settling redirect funds to the rebate pool
1025+
// When channel allocation is settled redirect funds to the rebate pool
10261026
if (allocState == AllocationState.Settled) {
10271027
Rebates.Pool storage rebatePool = rebates[alloc.settledAtEpoch];
10281028
rebatePool.fees = rebatePool.fees.add(rebateFees);

contracts/staking/StakingStorage.sol

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ contract StakingV1Storage is GraphProxyStorage {
6060
// Delegation pools : indexer => DelegationPool
6161
mapping(address => IStaking.DelegationPool) public delegationPools;
6262

63-
// -- Operators
63+
// -- Operators --
6464

6565
// Operator auth : indexer => operator
6666
mapping(address => mapping(address => bool)) public operatorAuth;

0 commit comments

Comments
 (0)