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

feat: remove asset holder allowlist, with additional fixes #864

Merged
merged 21 commits into from
Dec 7, 2023
Merged
Changes from 17 commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
bcf31e9
feat: remove asset holder allowlist
pcarranzav Nov 15, 2022
2e8c499
fix: remove setAssetHolder call from config files
tmigone Nov 16, 2022
36308c3
fix: a few details for asset holder deprecation
pcarranzav Nov 24, 2022
b56af5d
fix: improvements from Trust audit (TRST-L-1 and recommendations)
pcarranzav Feb 17, 2023
225be78
test: fix the case when collecting zero fees
pcarranzav Feb 17, 2023
c2fdebb
Merge pull request #791 from graphprotocol/pcv/asset-holder-trust-fixes
pcarranzav Feb 23, 2023
765d43f
chore: add Trust audit report
pcarranzav Feb 23, 2023
194fb4f
Merge branch 'dev' into pcv/remove-asset-holder-check
pcarranzav Aug 2, 2023
04a42bc
fix: remove assetHolders getter
pcarranzav Aug 3, 2023
4b334cb
test: remove a leftover setAssetHolder call
pcarranzav Aug 3, 2023
d3b626c
fix: use a bracket scope to avoid stack too deep
pcarranzav Sep 5, 2023
c090c63
fix: typo
pcarranzav Sep 5, 2023
abe55a1
test: fix a case where it still expected an event
pcarranzav Sep 6, 2023
bf1aa37
fix: require that an allocation is open for an epoch before collecting
pcarranzav Oct 11, 2023
22e838f
fix: enforce a minimum of 1 GRT when delegating to an indexer for the…
pcarranzav Oct 11, 2023
ef55d45
fix: enforce minimum delegation when receiving from L1
pcarranzav Oct 11, 2023
3882b35
fix: enforce minimum delegation in all cases, including undelegation
pcarranzav Oct 12, 2023
f9af7eb
fix: round up when calculating curation fees and the protocol tax (OZ…
pcarranzav Nov 9, 2023
821c93a
fix: clean up the check for remaining delegation (OZ N-01)
pcarranzav Nov 10, 2023
842445d
fix: add a rounding error protection when receiving subgraphs or sign…
pcarranzav Nov 22, 2023
afe7f9f
fix: add comment on not being able to revert
pcarranzav Nov 24, 2023
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
Binary file not shown.
3 changes: 0 additions & 3 deletions config/graph.arbitrum-goerli.yml
Original file line number Diff line number Diff line change
@@ -123,9 +123,6 @@ contracts:
- fn: "setSlasher"
slasher: "${{DisputeManager.address}}"
allowed: true
- fn: "setAssetHolder"
assetHolder: "${{AllocationExchange.address}}"
allowed: true
- fn: "syncAllContracts"
RewardsManager:
proxy: true
3 changes: 0 additions & 3 deletions config/graph.arbitrum-localhost.yml
Original file line number Diff line number Diff line change
@@ -123,9 +123,6 @@ contracts:
- fn: "setSlasher"
slasher: "${{DisputeManager.address}}"
allowed: true
- fn: "setAssetHolder"
assetHolder: "${{AllocationExchange.address}}"
allowed: true
- fn: "syncAllContracts"
RewardsManager:
proxy: true
3 changes: 0 additions & 3 deletions config/graph.arbitrum-one.yml
Original file line number Diff line number Diff line change
@@ -123,9 +123,6 @@ contracts:
- fn: "setSlasher"
slasher: "${{DisputeManager.address}}"
allowed: true
- fn: "setAssetHolder"
assetHolder: "${{AllocationExchange.address}}"
allowed: true
- fn: "syncAllContracts"
RewardsManager:
proxy: true
3 changes: 0 additions & 3 deletions config/graph.goerli.yml
Original file line number Diff line number Diff line change
@@ -126,9 +126,6 @@ contracts:
- fn: "setSlasher"
slasher: "${{DisputeManager.address}}"
allowed: true
- fn: "setAssetHolder"
assetHolder: "${{AllocationExchange.address}}"
allowed: true
- fn: "syncAllContracts"
RewardsManager:
proxy: true
3 changes: 0 additions & 3 deletions config/graph.localhost.yml
Original file line number Diff line number Diff line change
@@ -131,9 +131,6 @@ contracts:
- fn: "setSlasher"
slasher: "${{DisputeManager.address}}"
allowed: true
- fn: "setAssetHolder"
assetHolder: "${{AllocationExchange.address}}"
allowed: true
- fn: "syncAllContracts"
RewardsManager:
proxy: true
3 changes: 0 additions & 3 deletions config/graph.mainnet.yml
Original file line number Diff line number Diff line change
@@ -126,9 +126,6 @@ contracts:
- fn: "setSlasher"
slasher: "${{DisputeManager.address}}"
allowed: true
- fn: "setAssetHolder"
assetHolder: "${{AllocationExchange.address}}"
allowed: true
- fn: "syncAllContracts"
RewardsManager:
proxy: true
9 changes: 7 additions & 2 deletions contracts/l2/staking/L2Staking.sol
Original file line number Diff line number Diff line change
@@ -19,6 +19,9 @@ contract L2Staking is Staking, IL2StakingBase {
using SafeMath for uint256;
using Stakes for Stakes.Indexer;

/// @dev Minimum amount of tokens that can be delegated
uint256 private constant MINIMUM_DELEGATION = 1e18;

/**
* @dev Emitted when `delegator` delegated `tokens` to the `indexer`, the delegator
* gets `shares` for the delegation pool proportionally to the tokens staked.
@@ -124,8 +127,10 @@ contract L2Staking is Staking, IL2StakingBase {
// Calculate shares to issue (without applying any delegation tax)
uint256 shares = (pool.tokens == 0) ? _amount : _amount.mul(pool.shares).div(pool.tokens);

if (shares == 0) {
// If no shares would be issued (probably a rounding issue or attack), return the tokens to the delegator
if (shares == 0 || _amount < MINIMUM_DELEGATION) {
// If no shares would be issued (probably a rounding issue or attack),
// or if the amount is under the minimum delegation (which could be part of a rounding attack),
// return the tokens to the delegator
graphToken().transfer(_delegationData.delegator, _amount);
emit TransferredDelegationReturnedToDelegator(
_delegationData.indexer,
14 changes: 0 additions & 14 deletions contracts/staking/IStakingBase.sol
Original file line number Diff line number Diff line change
@@ -93,12 +93,6 @@ interface IStakingBase is IStakingData {
uint32 cooldownBlocks
);

/**
* @dev Emitted when `caller` set `assetHolder` address as `allowed` to send funds
* to staking contract.
*/
event AssetHolderUpdate(address indexed caller, address indexed assetHolder, bool allowed);

/**
* @dev Emitted when `indexer` set `operator` access.
*/
@@ -219,14 +213,6 @@ interface IStakingBase is IStakingData {
uint32 _lambdaDenominator
) external;

/**
* @notice Set an address as allowed asset holder.
* @dev This function can only be called by the governor.
* @param _assetHolder Address of allowed source for state channel funds
* @param _allowed True if asset holder is allowed
*/
function setAssetHolder(address _assetHolder, bool _allowed) external;

/**
* @notice Authorize or unauthorize an address to be an operator for the caller.
* @param _operator Address to authorize or unauthorize
9 changes: 0 additions & 9 deletions contracts/staking/IStakingExtension.sol
Original file line number Diff line number Diff line change
@@ -237,15 +237,6 @@ interface IStakingExtension is IStakingData {
*/
function rewardsDestination(address _indexer) external view returns (address);

/**
* @notice Getter for assetHolders[_maybeAssetHolder]:
* returns true if the address is an asset holder, i.e. an entity that can collect
* query fees into the Staking contract.
* @param _maybeAssetHolder The address that may or may not be an asset holder
* @return True if the address is an asset holder
*/
function assetHolders(address _maybeAssetHolder) external view returns (bool);

/**
* @notice Getter for subgraphAllocations[_subgraphDeploymentId]:
* returns the amount of tokens allocated to a subgraph deployment.
33 changes: 14 additions & 19 deletions contracts/staking/Staking.sol
Original file line number Diff line number Diff line change
@@ -220,17 +220,6 @@ abstract contract Staking is StakingV4Storage, GraphUpgradeable, IStakingBase, M
);
}

/**
* @notice Set an address as allowed asset holder.
* @param _assetHolder Address of allowed source for state channel funds
* @param _allowed True if asset holder is allowed
*/
function setAssetHolder(address _assetHolder, bool _allowed) external override onlyGovernor {
require(_assetHolder != address(0), "!assetHolder");
__assetHolders[_assetHolder] = _allowed;
emit AssetHolderUpdate(msg.sender, _assetHolder, _allowed);
}

/**
* @notice Authorize or unauthorize an address to be an operator for the caller.
* @param _operator Address to authorize or unauthorize
@@ -354,7 +343,6 @@ abstract contract Staking is StakingV4Storage, GraphUpgradeable, IStakingBase, M

/**
* @dev Collect and rebate query fees from state channels to the indexer
* Funds received are only accepted from a valid sender.
* To avoid reverting on the withdrawal from channel flow this function will accept calls with zero tokens.
* We use an exponential rebate formula to calculate the amount of tokens to rebate to the indexer.
* This implementation allows collecting multiple times on the same allocation, keeping track of the
@@ -366,14 +354,22 @@ abstract contract Staking is StakingV4Storage, GraphUpgradeable, IStakingBase, M
// Allocation identifier validation
require(_allocationID != address(0), "!alloc");

// The contract caller must be an authorized asset holder
require(__assetHolders[msg.sender] == true, "!assetHolder");

// Allocation must exist
AllocationState allocState = _getAllocationState(_allocationID);
require(allocState != AllocationState.Null, "!collect");

// Get allocation
Allocation storage alloc = __allocations[_allocationID];
// The allocation must've been opened at least 1 epoch ago,
// to prevent manipulation of the curation or delegation pools
require(alloc.createdAtEpoch < epochManager().currentEpoch(), "!epoch");

// If the query fees are zero, we don't want to revert
// but we also don't need to do anything, so just return
if (_tokens == 0) {
return;
}

bytes32 subgraphDeploymentID = alloc.subgraphDeploymentID;

uint256 queryFees = _tokens; // Tokens collected from the channel
@@ -382,9 +378,8 @@ abstract contract Staking is StakingV4Storage, GraphUpgradeable, IStakingBase, M
uint256 queryRebates = 0; // Tokens to distribute to indexer
uint256 delegationRewards = 0; // Tokens to distribute to delegators

// Process query fees only if non-zero amount
if (queryFees > 0) {
// -- Pull tokens from the authorized sender --
{
// -- Pull tokens from the sender --
IGraphToken graphToken = graphToken();
TokenUtils.pullTokens(graphToken, msg.sender, queryFees);

@@ -789,7 +784,7 @@ abstract contract Staking is StakingV4Storage, GraphUpgradeable, IStakingBase, M

// Creates an allocation
// Allocation identifiers are not reused
// The assetHolder address can send collected funds to the allocation
// Anyone can send collected funds to the allocation using collect()
Allocation memory alloc = Allocation(
_indexer,
_subgraphDeploymentID,
24 changes: 11 additions & 13 deletions contracts/staking/StakingExtension.sol
Original file line number Diff line number Diff line change
@@ -26,6 +26,8 @@ contract StakingExtension is StakingV4Storage, GraphUpgradeable, IStakingExtensi

/// @dev 100% in parts per million
uint32 private constant MAX_PPM = 1000000;
/// @dev Minimum amount of tokens that can be delegated
uint256 private constant MINIMUM_DELEGATION = 1e18;

/**
* @dev Check if the caller is the slasher.
@@ -301,17 +303,6 @@ contract StakingExtension is StakingV4Storage, GraphUpgradeable, IStakingExtensi
return __rewardsDestination[_indexer];
}

/**
* @notice Getter for assetHolders[_maybeAssetHolder]:
* returns true if the address is an asset holder, i.e. an entity that can collect
* query fees into the Staking contract.
* @param _maybeAssetHolder The address that may or may not be an asset holder
* @return True if the address is an asset holder
*/
function assetHolders(address _maybeAssetHolder) external view override returns (bool) {
return __assetHolders[_maybeAssetHolder];
}

/**
* @notice Getter for operatorAuth[_indexer][_maybeOperator]:
* returns true if the operator is authorized to operate on behalf of the indexer.
@@ -539,8 +530,8 @@ contract StakingExtension is StakingV4Storage, GraphUpgradeable, IStakingExtensi
address _indexer,
uint256 _tokens
) private returns (uint256) {
// Only delegate a non-zero amount of tokens
require(_tokens > 0, "!tokens");
// Only allow delegations over a minimum, to prevent rounding attacks
require(_tokens >= MINIMUM_DELEGATION, "!minimum-delegation");
// Only delegate to non-empty address
require(_indexer != address(0), "!indexer");
// Only delegate to staked indexer
@@ -608,6 +599,13 @@ contract StakingExtension is StakingV4Storage, GraphUpgradeable, IStakingExtensi

// Update the delegation
delegation.shares = delegation.shares.sub(_shares);
// Enforce more than the minimum delegation is left,
// to prevent rounding attacks
require(
delegation.shares == 0 ||
delegation.shares.mul(pool.tokens).div(pool.shares) >= MINIMUM_DELEGATION,
"!minimum-delegation"
);
delegation.tokensLocked = delegation.tokensLocked.add(tokens);
delegation.tokensLockedUntil = epochManager().currentEpoch().add(
__delegationUnbondingPeriod
4 changes: 2 additions & 2 deletions contracts/staking/StakingStorage.sol
Original file line number Diff line number Diff line change
@@ -89,8 +89,8 @@ contract StakingV1Storage is Managed {

// -- Asset Holders --

/// @dev Allowed AssetHolders that can collect query fees: assetHolder => is allowed
mapping(address => bool) internal __assetHolders;
/// @dev DEPRECATED: Allowed AssetHolders: assetHolder => is allowed
mapping(address => bool) private __DEPRECATED_assetHolders; // solhint-disable-line var-name-mixedcase
}

/**
5 changes: 0 additions & 5 deletions e2e/deployment/config/staking.test.ts
Original file line number Diff line number Diff line change
@@ -26,11 +26,6 @@ describe('Staking configuration', () => {
expect(isSlasher).eq(true)
})

it('should allow AllocationExchange to collect query fees', async function () {
const allowed = await Staking.assetHolders(AllocationExchange.address)
expect(allowed).eq(true)
})

it('minimumIndexerStake should match "minimumIndexerStake" in the config file', async function () {
const value = await Staking.minimumIndexerStake()
const expected = getItemValue(graphConfig, `contracts/${contractName}/init/minimumIndexerStake`)
3 changes: 0 additions & 3 deletions test/disputes/poi.test.ts
Original file line number Diff line number Diff line change
@@ -105,9 +105,6 @@ describe('DisputeManager:POI', async () => {
// Give some funds to the fisherman
await grt.connect(governor.signer).mint(fisherman.address, fishermanTokens)
await grt.connect(fisherman.signer).approve(disputeManager.address, fishermanTokens)

// Allow the asset holder
await staking.connect(governor.signer).setAssetHolder(assetHolder.address, true)
})

beforeEach(async function () {
3 changes: 0 additions & 3 deletions test/disputes/query.test.ts
Original file line number Diff line number Diff line change
@@ -153,9 +153,6 @@ describe('DisputeManager:Query', async () => {
await grt.connect(dst.signer).approve(disputeManager.address, fishermanTokens)
}

// Allow the asset holder
await staking.connect(governor.signer).setAssetHolder(assetHolder.address, true)

// Create an attestation
const attestation = await buildAttestation(receipt, indexer1ChannelKey.privKey)

85 changes: 84 additions & 1 deletion test/l2/l2Staking.test.ts
Original file line number Diff line number Diff line change
@@ -295,7 +295,8 @@ describe('L2Staking', () => {
.setIssuancePerBlock(toGRT('114'))

await staking.connect(me.signer).stake(tokens100k)
await staking.connect(me.signer).delegate(me.address, toBN(1)) // 1 weiGRT == 1 share
// Initialize the delegation pool to allow delegating less than 1 GRT
await staking.connect(me.signer).delegate(me.address, tokens10k)

await staking.connect(me.signer).setDelegationParameters(1000, 1000, 1000)
await grt.connect(me.signer).approve(fixtureContracts.curation.address, tokens10k)
@@ -336,6 +337,88 @@ describe('L2Staking', () => {
const delegatorGRTBalanceAfter = await grt.balanceOf(other.address)
expect(delegatorGRTBalanceAfter.sub(delegatorGRTBalanceBefore)).to.equal(toBN(1))
})
it('returns delegation to the delegator if it initializes the pool with less than the minimum delegation', async function () {
await fixtureContracts.rewardsManager
.connect(governor.signer)
.setIssuancePerBlock(toGRT('114'))

await staking.connect(me.signer).stake(tokens100k)

await staking.connect(me.signer).setDelegationParameters(1000, 1000, 1000)
await grt.connect(me.signer).approve(fixtureContracts.curation.address, tokens10k)
await fixtureContracts.curation.connect(me.signer).mint(subgraphDeploymentID, tokens10k, 0)

await allocate(tokens100k)
await advanceToNextEpoch(fixtureContracts.epochManager)
await advanceToNextEpoch(fixtureContracts.epochManager)
await staking.connect(me.signer).closeAllocation(allocationID, randomHexBytes(32))
// Now there are some rewards sent to delegation pool, so 1 weiGRT is less than 1 share

const functionData = defaultAbiCoder.encode(
['tuple(address,address)'],
[[me.address, other.address]],
)

const callhookData = defaultAbiCoder.encode(
['uint8', 'bytes'],
[toBN(1), functionData], // code = 1 means RECEIVE_DELEGATION_CODE
)
const delegatorGRTBalanceBefore = await grt.balanceOf(other.address)
const tx = gatewayFinalizeTransfer(
mockL1Staking.address,
staking.address,
toGRT('0.1'), // Less than 1 GRT!
callhookData,
)

await expect(tx)
.emit(l2GraphTokenGateway, 'DepositFinalized')
.withArgs(mockL1GRT.address, mockL1Staking.address, staking.address, toGRT('0.1'))
const delegation = await staking.getDelegation(me.address, other.address)
await expect(tx)
.emit(staking, 'TransferredDelegationReturnedToDelegator')
.withArgs(me.address, other.address, toGRT('0.1'))

expect(delegation.shares).to.equal(0)
const delegatorGRTBalanceAfter = await grt.balanceOf(other.address)
expect(delegatorGRTBalanceAfter.sub(delegatorGRTBalanceBefore)).to.equal(toGRT('0.1'))
})
it('returns delegation under the minimum if the pool is initialized', async function () {
await staking.connect(me.signer).stake(tokens100k)

// Initialize the delegation pool to allow delegating less than 1 GRT
await staking.connect(me.signer).delegate(me.address, tokens10k)

const functionData = defaultAbiCoder.encode(
['tuple(address,address)'],
[[me.address, other.address]],
)

const callhookData = defaultAbiCoder.encode(
['uint8', 'bytes'],
[toBN(1), functionData], // code = 1 means RECEIVE_DELEGATION_CODE
)
const delegatorGRTBalanceBefore = await grt.balanceOf(other.address)
const tx = gatewayFinalizeTransfer(
mockL1Staking.address,
staking.address,
toGRT('0.1'),
callhookData,
)

await expect(tx)
.emit(l2GraphTokenGateway, 'DepositFinalized')
.withArgs(mockL1GRT.address, mockL1Staking.address, staking.address, toGRT('0.1'))

const delegation = await staking.getDelegation(me.address, other.address)
await expect(tx)
.emit(staking, 'TransferredDelegationReturnedToDelegator')
.withArgs(me.address, other.address, toGRT('0.1'))

expect(delegation.shares).to.equal(0)
const delegatorGRTBalanceAfter = await grt.balanceOf(other.address)
expect(delegatorGRTBalanceAfter.sub(delegatorGRTBalanceBefore)).to.equal(toGRT('0.1'))
})
})
describe('onTokenTransfer with invalid messages', function () {
it('reverts if the code is invalid', async function () {
8 changes: 6 additions & 2 deletions test/payments/allocationExchange.test.ts
Original file line number Diff line number Diff line change
@@ -14,8 +14,10 @@ import {
randomHexBytes,
toGRT,
Account,
advanceToNextEpoch,
} from '../lib/testHelpers'
import { arrayify, joinSignature, SigningKey, solidityKeccak256 } from 'ethers/lib/utils'
import { EpochManager } from '../../build/types/EpochManager'

const { AddressZero, MaxUint256 } = constants

@@ -35,6 +37,7 @@ describe('AllocationExchange', () => {
let grt: GraphToken
let staking: IStaking
let allocationExchange: AllocationExchange
let epochManager: EpochManager

async function createVoucher(
allocationID: string,
@@ -57,7 +60,7 @@ describe('AllocationExchange', () => {
authority = Wallet.createRandom()

fixture = new NetworkFixture()
;({ grt, staking } = await fixture.load(governor.signer))
;({ grt, staking, epochManager } = await fixture.load(governor.signer))
allocationExchange = (await deployment.deployContract(
'AllocationExchange',
governor.signer,
@@ -77,7 +80,6 @@ describe('AllocationExchange', () => {
await grt.connect(governor.signer).mint(allocationExchange.address, exchangeTokens)

// Ensure the exchange is correctly setup
await staking.connect(governor.signer).setAssetHolder(allocationExchange.address, true)
await allocationExchange.connect(governor.signer).setAuthority(authority.address, true)
await allocationExchange.approveAll()
})
@@ -192,6 +194,7 @@ describe('AllocationExchange', () => {

// Setup an active allocation
const allocationID = await setupAllocation()
await advanceToNextEpoch(epochManager)

// Initiate a withdrawal
const actualAmount = toGRT('2000') // <- withdraw amount
@@ -213,6 +216,7 @@ describe('AllocationExchange', () => {
it('reject double spending of a voucher', async function () {
// Setup an active allocation
const allocationID = await setupAllocation()
await advanceToNextEpoch(epochManager)

// Initiate a withdrawal
const actualAmount = toGRT('2000') // <- withdraw amount
3 changes: 1 addition & 2 deletions test/rewards/rewards.test.ts
Original file line number Diff line number Diff line change
@@ -1013,7 +1013,6 @@ describe('Rewards', () => {

// allow the asset holder
const tokensToCollect = toGRT('10000')
await staking.connect(governor.signer).setAssetHolder(assetHolder.address, true)

// signal in two subgraphs in the same block
const subgraphs = [subgraphDeploymentID1, subgraphDeploymentID2]
@@ -1043,7 +1042,7 @@ describe('Rewards', () => {
])

// move time fwd
await advanceBlock()
await advanceToNextEpoch(epochManager)

// collect funds into staking for that sub
await staking.connect(assetHolder.signer).collect(tokensToCollect, allocationID1)
97 changes: 65 additions & 32 deletions test/staking/allocation.test.ts
Original file line number Diff line number Diff line change
@@ -117,9 +117,13 @@ describe('Staking:Allocation', () => {
// This function tests collect with state updates
const shouldCollect = async (
tokensToCollect: BigNumber,
_allocationID?: string,
options: {
allocationID?: string
expectEvent?: boolean
} = {},
): Promise<{ queryRebates: BigNumber; queryFeesBurnt: BigNumber }> => {
const alloID = _allocationID ?? allocationID
const expectEvent = options.expectEvent ?? true
const alloID = options.allocationID ?? allocationID
const alloStateBefore = await staking.getAllocationState(alloID)
// Should have a particular state before collecting
expect(alloStateBefore).to.be.oneOf([AllocationState.Active, AllocationState.Closed])
@@ -174,21 +178,26 @@ describe('Staking:Allocation', () => {

// Collect tokens from allocation
const tx = staking.connect(assetHolder.signer).collect(tokensToCollect, alloID)
await expect(tx)
.emit(staking, 'RebateCollected')
.withArgs(
assetHolder.address,
indexer.address,
subgraphDeploymentID,
alloID,
await epochManager.currentEpoch(),
tokensToCollect,
protocolFees,
curationFees,
queryFees,
queryRebates,
delegationRewards,
)
if (expectEvent) {
await expect(tx)
.emit(staking, 'RebateCollected')
.withArgs(
assetHolder.address,
indexer.address,
subgraphDeploymentID,
alloID,
await epochManager.currentEpoch(),
tokensToCollect,
protocolFees,
curationFees,
queryFees,
queryRebates,
delegationRewards,
)
} else {
await expect(tx).to.not.be.reverted
await expect(tx).to.not.emit(staking, 'RebateCollected')
}

// After state
const afterTokenSupply = await grt.totalSupply()
@@ -255,8 +264,9 @@ describe('Staking:Allocation', () => {
)

// Collect `tokensToCollect` with a single voucher
const rebatedAmountFull = (await shouldCollect(totalTokensToCollect, anotherAllocationID))
.queryRebates
const rebatedAmountFull = (
await shouldCollect(totalTokensToCollect, { allocationID: anotherAllocationID })
).queryRebates

// Check rebated amounts match, allow a small error margin of 5 wei
// Due to rounding it's not possible to guarantee an exact match in case of multiple collections
@@ -322,9 +332,6 @@ describe('Staking:Allocation', () => {
// Give some funds to the delegator and approve staking contract to use funds on delegator behalf
await grt.connect(governor.signer).mint(delegator.address, tokensToDelegate)
await grt.connect(delegator.signer).approve(staking.address, tokensToDelegate)

// Allow the asset holder
await staking.connect(governor.signer).setAssetHolder(assetHolder.address, true)
})

beforeEach(async function () {
@@ -526,6 +533,7 @@ describe('Staking:Allocation', () => {
beforeEach(async function () {
// Create the allocation
await staking.connect(indexer.signer).stake(tokensToStake)
await advanceToNextEpoch(epochManager)
await allocate(tokensToAllocate)

// Add some signal to the subgraph to enable curation fees
@@ -593,7 +601,7 @@ describe('Staking:Allocation', () => {
})

it('should collect zero tokens', async function () {
await shouldCollect(toGRT('0'))
await shouldCollect(toGRT('0'), { expectEvent: false })
})

it('should allow multiple collections on the same allocation', async function () {
@@ -625,6 +633,11 @@ describe('Staking:Allocation', () => {
await expect(tx).revertedWith('!alloc')
})

it('reject collect if allocation has been open for less than 1 epoch', async function () {
const tx = staking.connect(indexer.signer).collect(tokensToCollect, allocationID)
await expect(tx).revertedWith('!epoch')
})

it('reject collect if allocation does not exist', async function () {
const invalidAllocationID = randomHexBytes(20)
const tx = staking.connect(assetHolder.signer).collect(tokensToCollect, invalidAllocationID)
@@ -641,7 +654,7 @@ describe('Staking:Allocation', () => {
)

// Collect from closed allocation, should get no rebates
const rebates = await shouldCollect(tokensToCollect, anotherAllocationID)
const rebates = await shouldCollect(tokensToCollect, { allocationID: anotherAllocationID })
expect(rebates.queryRebates).eq(BigNumber.from(0))
expect(rebates.queryFeesBurnt).eq(tokensToCollect)
})
@@ -665,7 +678,9 @@ describe('Staking:Allocation', () => {

// First collection
// Indexer gets 100% of the query fees due to α = 0
const firstRebates = await shouldCollect(firstTokensToCollect, anotherAllocationID)
const firstRebates = await shouldCollect(firstTokensToCollect, {
allocationID: anotherAllocationID,
})
expect(firstRebates.queryRebates).eq(firstTokensToCollect)
expect(firstRebates.queryFeesBurnt).eq(BigNumber.from(0))

@@ -675,14 +690,18 @@ describe('Staking:Allocation', () => {
// Second collection
// Indexer gets 0% of the query fees
// Parameters changed so now they are over-rebated and should get "negative rebates", instead they get 0
const secondRebates = await shouldCollect(secondTokensToCollect, anotherAllocationID)
const secondRebates = await shouldCollect(secondTokensToCollect, {
allocationID: anotherAllocationID,
})
expect(secondRebates.queryRebates).eq(BigNumber.from(0))
expect(secondRebates.queryFeesBurnt).eq(secondTokensToCollect)

// Third collection
// Previous collection plus this new one tip the balance and indexer is no longer over-rebated
// They get rebates and burn again
const thirdRebates = await shouldCollect(thirdTokensToCollect, anotherAllocationID)
const thirdRebates = await shouldCollect(thirdTokensToCollect, {
allocationID: anotherAllocationID,
})
expect(thirdRebates.queryRebates).gt(BigNumber.from(0))
expect(thirdRebates.queryFeesBurnt).gt(BigNumber.from(0))
})
@@ -706,7 +725,9 @@ describe('Staking:Allocation', () => {

// First collection
// Indexer gets rebates and burn
const firstRebates = await shouldCollect(firstTokensToCollect, anotherAllocationID)
const firstRebates = await shouldCollect(firstTokensToCollect, {
allocationID: anotherAllocationID,
})
expect(firstRebates.queryRebates).gt(BigNumber.from(0))
expect(firstRebates.queryFeesBurnt).gt(BigNumber.from(0))

@@ -716,18 +737,26 @@ describe('Staking:Allocation', () => {
// Second collection
// Indexer gets 100% of the query fees
// Parameters changed so now they are under-rebated and should get more than the available amount but we cap it
const secondRebates = await shouldCollect(secondTokensToCollect, anotherAllocationID)
const secondRebates = await shouldCollect(secondTokensToCollect, {
allocationID: anotherAllocationID,
})
expect(secondRebates.queryRebates).eq(secondTokensToCollect)
expect(secondRebates.queryFeesBurnt).eq(BigNumber.from(0))

// Third collection
// Previous collection plus this new one tip the balance and indexer is no longer under-rebated
// They get rebates and burn again
const thirdRebates = await shouldCollect(thirdTokensToCollect, anotherAllocationID)
const thirdRebates = await shouldCollect(thirdTokensToCollect, {
allocationID: anotherAllocationID,
})
expect(thirdRebates.queryRebates).gt(BigNumber.from(0))
expect(thirdRebates.queryFeesBurnt).gt(BigNumber.from(0))
})

it('should collect zero tokens', async function () {
await shouldCollect(toGRT('0'), { expectEvent: false })
})

it('should get stuck under-rebated if alpha is changed to zero', async function () {
// Set up a new allocation with `tokensToAllocate` staked
await staking.connect(indexer.signer).stake(tokensToStake)
@@ -742,7 +771,9 @@ describe('Staking:Allocation', () => {

// First collection
// Indexer gets rebates and burn
const firstRebates = await shouldCollect(tokensToCollect, anotherAllocationID)
const firstRebates = await shouldCollect(tokensToCollect, {
allocationID: anotherAllocationID,
})
expect(firstRebates.queryRebates).gt(BigNumber.from(0))
expect(firstRebates.queryFeesBurnt).gt(BigNumber.from(0))

@@ -754,7 +785,9 @@ describe('Staking:Allocation', () => {
// Parameters changed so now they are under-rebated and should get more than the available amount but we cap it
// Distributed amount will never catch up due to the initial collection which was less than 100%
for (const _i of [...Array(10).keys()]) {
const succesiveRebates = await shouldCollect(tokensToCollect, anotherAllocationID)
const succesiveRebates = await shouldCollect(tokensToCollect, {
allocationID: anotherAllocationID,
})
expect(succesiveRebates.queryRebates).eq(tokensToCollect)
expect(succesiveRebates.queryFeesBurnt).eq(BigNumber.from(0))
}
29 changes: 0 additions & 29 deletions test/staking/configuration.test.ts
Original file line number Diff line number Diff line change
@@ -88,35 +88,6 @@ describe('Staking:Config', () => {
})
})

describe('setAssetHolder', function () {
it('should set `assetHolder`', async function () {
expect(await staking.assetHolders(me.address)).eq(false)

const tx1 = staking.connect(governor.signer).setAssetHolder(me.address, true)
await expect(tx1)
.emit(staking, 'AssetHolderUpdate')
.withArgs(governor.address, me.address, true)
expect(await staking.assetHolders(me.address)).eq(true)

const tx2 = staking.connect(governor.signer).setAssetHolder(me.address, false)
await expect(tx2)
.emit(staking, 'AssetHolderUpdate')
.withArgs(governor.address, me.address, false)
await staking.connect(governor.signer).setAssetHolder(me.address, false)
expect(await staking.assetHolders(me.address)).eq(false)
})

it('reject set `assetHolder` if not allowed', async function () {
const tx = staking.connect(other.signer).setAssetHolder(me.address, true)
await expect(tx).revertedWith('Only Controller governor')
})

it('reject set `assetHolder` to address zero', async function () {
const tx = staking.connect(governor.signer).setAssetHolder(AddressZero, true)
await expect(tx).revertedWith('!assetHolder')
})
})

describe('curationPercentage', function () {
it('should set `curationPercentage`', async function () {
const newValue = toBN('5')
45 changes: 34 additions & 11 deletions test/staking/delegation.test.ts
Original file line number Diff line number Diff line change
@@ -20,6 +20,7 @@ import {

const { AddressZero, HashZero } = constants
const MAX_PPM = toBN('1000000')
const tokensToCollect = toGRT('50000000000000000000')

describe('Staking::Delegation', () => {
let me: Account
@@ -190,13 +191,12 @@ describe('Staking::Delegation', () => {
}

// Distribute test funds
for (const wallet of [me, indexer, indexer2, assetHolder]) {
for (const wallet of [me, indexer, indexer2]) {
await grt.connect(governor.signer).mint(wallet.address, toGRT('1000000'))
await grt.connect(wallet.signer).approve(staking.address, toGRT('1000000'))
}

// Allow the asset holder
await staking.connect(governor.signer).setAssetHolder(assetHolder.address, true)
await grt.connect(governor.signer).mint(assetHolder.address, tokensToCollect)
await grt.connect(assetHolder.signer).approve(staking.address, tokensToCollect)
})

beforeEach(async function () {
@@ -372,7 +372,20 @@ describe('Staking::Delegation', () => {
it('reject delegate with zero tokens', async function () {
const tokensToDelegate = toGRT('0')
const tx = staking.connect(delegator.signer).delegate(indexer.address, tokensToDelegate)
await expect(tx).revertedWith('!tokens')
await expect(tx).revertedWith('!minimum-delegation')
})

it('reject delegate with less than 1 GRT when the pool is not initialized', async function () {
const tokensToDelegate = toGRT('0.5')
const tx = staking.connect(delegator.signer).delegate(indexer.address, tokensToDelegate)
await expect(tx).revertedWith('!minimum-delegation')
})

it('reject delegating under 1 GRT when the pool is initialized', async function () {
await shouldDelegate(delegator, toGRT('1'))
const tokensToDelegate = toGRT('0.5')
const tx = staking.connect(delegator.signer).delegate(indexer.address, tokensToDelegate)
await expect(tx).revertedWith('!minimum-delegation')
})

it('reject delegate to empty address', async function () {
@@ -471,6 +484,12 @@ describe('Staking::Delegation', () => {
await advanceToNextEpoch(epochManager) // epoch 2
await shouldUndelegate(delegator, toGRT('10'))
})

it('reject undelegate if remaining tokens are less than the minimum', async function () {
await shouldDelegate(delegator, toGRT('100'))
const tx = staking.connect(delegator.signer).undelegate(indexer.address, toGRT('99.5'))
await expect(tx).revertedWith('!minimum-delegation')
})
})

describe('withdraw', function () {
@@ -526,7 +545,6 @@ describe('Staking::Delegation', () => {
// Test values
const tokensToStake = toGRT('200')
const tokensToAllocate = toGRT('2000')
const tokensToCollect = toGRT('500')
const tokensToDelegate = toGRT('1800')
const subgraphDeploymentID = randomHexBytes()
const channelKey = deriveChannelKey()
@@ -596,19 +614,24 @@ describe('Staking::Delegation', () => {

it('revert if it cannot assign the smallest amount of shares', async function () {
// Init the delegation pool
await shouldDelegate(delegator, tokensToDelegate)

await shouldDelegate(delegator, toGRT('1'))
const tokensToAllocate = toGRT('1')
// Collect funds thru full allocation cycle
// Set rebate alpha to 0 to ensure all fees are collected
await staking.connect(governor.signer).setRebateParameters(0, 1, 1, 1)
await staking.connect(governor.signer).setDelegationRatio(10)
await staking.connect(indexer.signer).setDelegationParameters(0, 0, 0)
await setupAllocation(tokensToAllocate)
await advanceToNextEpoch(epochManager)
await staking.connect(assetHolder.signer).collect(tokensToCollect, allocationID)
await advanceToNextEpoch(epochManager)
await staking.connect(indexer.signer).closeAllocation(allocationID, poi)
// We've callected 5e18 GRT (a ridiculous amount),
// which means the price of a share is now 5 GRT

// Delegate with such small amount of tokens (1 wei) that we do not have enough precision
// to even assign 1 wei of shares
const tx = staking.connect(delegator.signer).delegate(indexer.address, toBN(1))
// Delegate with such small amount of tokens (1 GRT) that we do not have enough precision
// to even assign 1 share
const tx = staking.connect(delegator.signer).delegate(indexer.address, toGRT('1'))
await expect(tx).revertedWith('!shares')
})
})