Skip to content
Permalink

Comparing changes

This is a direct comparison between two commits made in this repository or its related repositories. View the default comparison for this range or learn more about diff comparisons.

Open a pull request

Create a new pull request by comparing changes across two branches. If you need to, you can also . Learn more about diff comparisons here.
base repository: graphprotocol/contracts
Failed to load repositories. Confirm that selected base ref is valid, then try again.
Loading
base: fb4ff7aa50caf809790c1c23ded2230a9ec6dc6b
Choose a base ref
..
head repository: graphprotocol/contracts
Failed to load repositories. Confirm that selected head ref is valid, then try again.
Loading
compare: 6e6e95f9d09f31b132eef60cfc432bc5f5d75b3d
Choose a head ref
2 changes: 2 additions & 0 deletions packages/contracts/contracts/curation/Curation.sol
Original file line number Diff line number Diff line change
@@ -340,6 +340,8 @@ contract Curation is CurationV2Storage, GraphUpgradeable {
override
returns (uint256, uint256)
{
// NOTE: We're aware that this function rounds down and tax can be 0 for small amounts
// of tokens but since minimumCurationDeposit is 1 GRT tax will always be greater than 0.
uint256 curationTax = _tokensIn.mul(uint256(curationTaxPercentage)).div(MAX_PPM);
uint256 signalOut = _tokensToSignal(_subgraphDeploymentID, _tokensIn.sub(curationTax));
return (signalOut, curationTax);
10 changes: 8 additions & 2 deletions packages/contracts/contracts/l2/curation/L2Curation.sol
Original file line number Diff line number Diff line change
@@ -400,8 +400,14 @@ contract L2Curation is CurationV2Storage, GraphUpgradeable, IL2Curation {
override
returns (uint256, uint256)
{
uint256 curationTax = _tokensIn.mul(uint256(curationTaxPercentage)).div(MAX_PPM);
uint256 signalOut = _tokensToSignal(_subgraphDeploymentID, _tokensIn.sub(curationTax));
// Calculate tokens after tax first, subtract that from the tokens in
// to get the curation tax to avoid rounding down to zero.
uint256 tokensAfterCurationTax = uint256(MAX_PPM)
.sub(curationTaxPercentage)
.mul(_tokensIn)
.div(MAX_PPM);
uint256 curationTax = _tokensIn.sub(tokensAfterCurationTax);
uint256 signalOut = _tokensToSignal(_subgraphDeploymentID, tokensAfterCurationTax);
return (signalOut, curationTax);
}

10 changes: 7 additions & 3 deletions packages/contracts/contracts/staking/libs/MathUtils.sol
Original file line number Diff line number Diff line change
@@ -14,19 +14,23 @@ library MathUtils {
/**
* @dev Calculates the weighted average of two values pondering each of these
* values based on configured weights. The contribution of each value N is
* weightN/(weightA + weightB).
* weightN/(weightA + weightB). The calculation rounds up to ensure the result
* is always greater than the smallest of the two values.
* @param valueA The amount for value A
* @param weightA The weight to use for value A
* @param valueB The amount for value B
* @param weightB The weight to use for value B
*/
function weightedAverage(
function weightedAverageRoundingUp(
uint256 valueA,
uint256 weightA,
uint256 valueB,
uint256 weightB
) internal pure returns (uint256) {
return valueA.mul(weightA).add(valueB.mul(weightB)).div(weightA.add(weightB));
return
valueA.mul(weightA).add(valueB.mul(weightB)).add(weightA.add(weightB).sub(1)).div(
weightA.add(weightB)
);
}

/**
2 changes: 1 addition & 1 deletion packages/contracts/contracts/staking/libs/Stakes.sol
Original file line number Diff line number Diff line change
@@ -73,7 +73,7 @@ library Stakes {
// Take into account period averaging for multiple unstake requests
uint256 lockingPeriod = _period;
if (stake.tokensLocked > 0) {
lockingPeriod = MathUtils.weightedAverage(
lockingPeriod = MathUtils.weightedAverageRoundingUp(
MathUtils.diffOrZero(stake.tokensLockedUntil, block.number), // Remaining thawing period
stake.tokensLocked, // Weighted by remaining unstaked tokens
_period, // Thawing period
41 changes: 41 additions & 0 deletions packages/contracts/test/unit/l2/l2Curation.test.ts
Original file line number Diff line number Diff line change
@@ -469,6 +469,47 @@ describe('L2Curation', () => {
.mint(subgraphDeploymentID, tokensToDeposit, expectedSignal.add(1))
await expect(tx).revertedWith('Slippage protection')
})

it('should pay a minimum of 1 wei GRT in tax when depositing small amounts', async function () {
// Set minimum curation deposit
await contracts.Curation.connect(governor).setMinimumCurationDeposit('1')

// Set curation tax to 1%
await contracts.Curation.connect(governor).setCurationTaxPercentage(10000)

// Deposit a small amount where tax would be less than 1 wei
const tokensToDeposit = '99'

const expectedTokens = '98'
const expectedSignal = '98'
const expectedTax = 1

const tx = contracts.Curation.connect(curator).mint(
subgraphDeploymentID,
tokensToDeposit,
expectedSignal,
)

await expect(tx)
.emit(contracts.Curation, 'Signalled')
.withArgs(
curator.address,
subgraphDeploymentID,
tokensToDeposit,
expectedSignal,
expectedTax,
)

const burnTx = contracts.Curation.connect(curator).burn(
subgraphDeploymentID,
expectedSignal,
expectedTokens,
)

await expect(burnTx)
.emit(contracts.Curation, 'Burned')
.withArgs(curator.address, subgraphDeploymentID, expectedTokens, expectedSignal)
})
})

describe('curate tax free (from GNS)', async function () {
34 changes: 34 additions & 0 deletions packages/contracts/test/unit/staking/staking.test.ts
Original file line number Diff line number Diff line change
@@ -239,6 +239,40 @@ describe('Staking:Stakes', () => {
expect(afterIndexerStake.tokensLockedUntil).eq(expectedLockedUntil)
})

it('should always increase the thawing period on subsequent unstakes', async function () {
const tokensToUnstake = toGRT('10')
const tokensToUnstakeSecondTime = toGRT('0.000001')
const thawingPeriod = toBN(await staking.thawingPeriod())

// Unstake (1)
const tx1 = await staking.connect(indexer).unstake(tokensToUnstake)
const receipt1 = await tx1.wait()
const event1: Event = receipt1.events.pop()
const tokensLockedUntil1 = event1.args['until']

// Move forward before the tokens are unlocked for withdrawal
await helpers.mineUpTo(tokensLockedUntil1.sub(5))

// Calculate locking time for tokens taking into account the previous unstake request
const currentBlock = await helpers.latestBlock()

// Ensure at least 1 block is added (i.e. the weighted average rounds up)
const expectedLockedUntil = tokensLockedUntil1.add(1)

// Unstake (2)
const tx2 = await staking.connect(indexer).unstake(tokensToUnstakeSecondTime)
const receipt2 = await tx2.wait()

// Verify events
const event2: Event = receipt2.events.pop()
expect(event2.args['until']).eq(expectedLockedUntil)

// Verify state
const afterIndexerStake = await staking.stakes(indexer.address)
expect(afterIndexerStake.tokensLocked).eq(tokensToUnstake.add(tokensToUnstakeSecondTime)) // we unstaked two times
expect(afterIndexerStake.tokensLockedUntil).eq(expectedLockedUntil)
})

it('should unstake and withdraw if some tokens are unthawed', async function () {
const tokensToUnstake = toGRT('10')
const thawingPeriod = toBN(await staking.thawingPeriod())