Skip to content

Commit

Permalink
fix: calculation of shares when receiving delegation from l1 (OZ H-03)
Browse files Browse the repository at this point in the history
Signed-off-by: Tomás Migone <[email protected]>
  • Loading branch information
tmigone committed Aug 23, 2024
1 parent 1650a91 commit afe8bbc
Show file tree
Hide file tree
Showing 3 changed files with 159 additions and 47 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -337,6 +337,8 @@ contract HorizonStakingExtension is HorizonStakingBase, IL2StakingBase, IHorizon
* The specified amount is added to the delegator's delegation; the delegator's
* address and the indexer's address are specified in the _delegationData struct.
* Note that no delegation tax is applied here.
* @dev Note that L1 staking contract only allows delegation transfer if the indexer has already transferred,
* this means the corresponding delegation pool exists.
* @param _tokens Amount of tokens that were transferred
* @param _delegationData struct containing the delegator's address and the indexer's address
*/
Expand All @@ -356,7 +358,7 @@ contract HorizonStakingExtension is HorizonStakingBase, IL2StakingBase, IHorizon
}

// Calculate shares to issue (without applying any delegation tax)
uint256 shares = (pool.tokens == 0) ? _tokens : ((_tokens * pool.shares) / pool.tokens);
uint256 shares = (pool.tokens == 0) ? _tokens : ((_tokens * pool.shares) / (pool.tokens - pool.tokensThawing));

if (shares == 0 || _tokens < MINIMUM_DELEGATION) {
// If no shares would be issued (probably a rounding issue or attack),
Expand Down
4 changes: 3 additions & 1 deletion packages/horizon/test/staking/delegation/undelegate.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,12 @@ contract HorizonStakingUndelegateTest is HorizonStakingTest {
uint256 amount,
uint256 delegationAmount,
uint256 undelegateSteps
) public useIndexer useProvision(amount, 0, 0) useDelegation(delegationAmount) {
) public useIndexer useProvision(amount, 0, 0) {
undelegateSteps = bound(undelegateSteps, 1, 10);
delegationAmount = bound(delegationAmount, MIN_DELEGATION + 10 wei, MAX_STAKING_TOKENS);

resetPrank(users.delegator);
_delegate(users.indexer, subgraphDataServiceAddress, delegationAmount, 0);
Delegation memory delegation = _getDelegation(subgraphDataServiceAddress);

// there is a min delegation amount of 1 ether after undelegating
Expand Down
198 changes: 153 additions & 45 deletions packages/horizon/test/staking/transfer-tools/ttools.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -5,72 +5,180 @@ import "forge-std/Test.sol";

import { HorizonStakingTest } from "../HorizonStaking.t.sol";
import { IL2StakingTypes } from "@graphprotocol/contracts/contracts/l2/staking/IL2StakingTypes.sol";
import { IL2StakingBase } from "@graphprotocol/contracts/contracts/l2/staking/IL2StakingBase.sol";
import { IHorizonStakingExtension } from "../../../contracts/interfaces/internal/IHorizonStakingExtension.sol";

contract HorizonStakingTransferToolsTest is HorizonStakingTest {
event Transfer(address indexed from, address indexed to, uint tokens);

/*
* TESTS
*/

function testOnTransfer_RevertWhen_InvalidCaller(
uint256 amount,
uint256 delegationAmount
) public useIndexer useProvision(amount, 0, 0) useDelegation(delegationAmount) {
function testOnTransfer_RevertWhen_InvalidCaller() public {
bytes memory data = abi.encode(uint8(0), new bytes(0)); // Valid codes are 0 and 1
vm.expectRevert(bytes("ONLY_GATEWAY"));
staking.onTokenTransfer(counterpartStaking, 0, data);
}

function testOnTransfer_RevertWhen_InvalidCounterpart() public {
resetPrank(graphTokenGatewayAddress);

bytes memory data = abi.encode(uint8(0), new bytes(0)); // Valid codes are 0 and 1
vm.expectRevert(bytes("ONLY_GATEWAY"));
staking.onTokenTransfer(counterpartStaking, 0, data);
bytes memory data = abi.encode(uint8(0), new bytes(0)); // Valid codes are 0 and 1
vm.expectRevert(bytes("ONLY_L1_STAKING_THROUGH_BRIDGE"));
staking.onTokenTransfer(address(staking), 0, data);
}

function testOnTransfer_RevertWhen_InvalidCounterpart(
uint256 amount,
uint256 delegationAmount
) public useIndexer useProvision(amount, 0, 0) useDelegation(delegationAmount) {
resetPrank(graphTokenGatewayAddress);
function testOnTransfer_RevertWhen_InvalidData() public {
resetPrank(graphTokenGatewayAddress);

bytes memory data = abi.encode(uint8(0), new bytes(0)); // Valid codes are 0 and 1
vm.expectRevert(bytes("ONLY_L1_STAKING_THROUGH_BRIDGE"));
staking.onTokenTransfer(address(staking), 0, data);
vm.expectRevert();
staking.onTokenTransfer(counterpartStaking, 0, new bytes(0));
}

function testOnTransfer_RevertWhen_InvalidData(
uint256 amount,
uint256 delegationAmount
) public useIndexer useProvision(amount, 0, 0) useDelegation(delegationAmount) {
resetPrank(graphTokenGatewayAddress);
function testOnTransfer_RevertWhen_InvalidCode() public {
resetPrank(graphTokenGatewayAddress);

vm.expectRevert();
staking.onTokenTransfer(counterpartStaking, 0, new bytes(0));
bytes memory data = abi.encode(uint8(2), new bytes(0)); // Valid codes are 0 and 1
vm.expectRevert(bytes("INVALID_CODE"));
staking.onTokenTransfer(counterpartStaking, 0, data);
}

function testOnTransfer_RevertWhen_InvalidCode(
uint256 amount,
uint256 delegationAmount
) public useIndexer useProvision(amount, 0, 0) useDelegation(delegationAmount) {
resetPrank(graphTokenGatewayAddress);
function testOnTransfer_ReceiveDelegation_RevertWhen_InvalidData() public {
resetPrank(graphTokenGatewayAddress);

bytes memory data = abi.encode(uint8(2), new bytes(0)); // Valid codes are 0 and 1
vm.expectRevert(bytes("INVALID_CODE"));
staking.onTokenTransfer(counterpartStaking, 0, data);
bytes memory data = abi.encode(uint8(IL2StakingTypes.L1MessageCodes.RECEIVE_DELEGATION_CODE), new bytes(0));
vm.expectRevert();
staking.onTokenTransfer(counterpartStaking, 0, data);
}

function testOnTransfer_ReceiveDelegation_RevertWhen_InvalidData(
uint256 amount,
uint256 delegationAmount
) public useIndexer useProvision(amount, 0, 0) useDelegation(delegationAmount) {
resetPrank(graphTokenGatewayAddress);
function testOnTransfer_ReceiveDelegation(uint256 amount) public {
amount = bound(amount, 1 ether, MAX_STAKING_TOKENS);

// create provision and legacy delegation pool - this is done by the bridge when indexers move to L2
resetPrank(users.indexer);
_createProvision(subgraphDataServiceLegacyAddress, 100 ether, 0, 0);

resetPrank(users.delegator);
_delegateLegacy(users.indexer, 1 ether);

bytes memory data = abi.encode(uint8(IL2StakingTypes.L1MessageCodes.RECEIVE_DELEGATION_CODE), new bytes(0));
vm.expectRevert();
staking.onTokenTransfer(counterpartStaking, 0, data);
// send amount to staking contract - this should be done by the bridge
resetPrank(users.delegator);
token.transfer(address(staking), amount);

resetPrank(graphTokenGatewayAddress);
bytes memory data = abi.encode(
uint8(IL2StakingTypes.L1MessageCodes.RECEIVE_DELEGATION_CODE),
abi.encode(users.indexer, users.delegator)
);
_onTokenTransfer_ReceiveDelegation(counterpartStaking, amount, data);
}

function testOnTransfer_ReceiveDelegation(
uint256 amount,
uint256 delegationAmount
) public useIndexer useProvision(amount, 0, 0) useDelegation(delegationAmount) {
resetPrank(graphTokenGatewayAddress);
function testOnTransfer_ReceiveDelegation_WhenThawing(uint256 amount) public {
amount = bound(amount, 1 ether, MAX_STAKING_TOKENS);
uint256 originalDelegationAmount = 10 ether;

// create provision and legacy delegation pool - this is done by the bridge when indexers move to L2
resetPrank(users.indexer);
_createProvision(subgraphDataServiceLegacyAddress, 100 ether, 0, 1 days);

resetPrank(users.delegator);
_delegateLegacy(users.indexer, originalDelegationAmount);

// send amount to staking contract - this should be done by the bridge
resetPrank(users.delegator);
token.transfer(address(staking), amount);

// thaw some delegation before receiving new delegation from L1
resetPrank(users.delegator);
_undelegateLegacy(users.indexer, originalDelegationAmount / 10);

resetPrank(graphTokenGatewayAddress);
bytes memory data = abi.encode(
uint8(IL2StakingTypes.L1MessageCodes.RECEIVE_DELEGATION_CODE),
abi.encode(users.indexer, users.delegator)
);
_onTokenTransfer_ReceiveDelegation(counterpartStaking, amount, data);
}

/**
* HELPERS
*/

function _onTokenTransfer_ReceiveDelegation(address from, uint256 tokens, bytes memory data) internal {
(, bytes memory fnData) = abi.decode(data, (uint8, bytes));
(address serviceProvider, address delegator) = abi.decode(fnData, (address, address));
bytes32 slotPoolTokens = bytes32(uint256(keccak256(abi.encode(serviceProvider, 20))) + 2);

// before
DelegationPool memory beforePool = staking.getDelegationPool(serviceProvider, subgraphDataServiceLegacyAddress);
Delegation memory beforeDelegation = staking.getDelegation(
serviceProvider,
subgraphDataServiceLegacyAddress,
delegator
);
uint256 beforeStoragePoolTokens = uint256(vm.load(address(staking), slotPoolTokens));
uint256 beforeDelegatedTokens = staking.getDelegatedTokensAvailable(
serviceProvider,
subgraphDataServiceLegacyAddress
);
uint256 beforeDelegatorBalance = token.balanceOf(delegator);
uint256 beforeStakingBalance = token.balanceOf(address(staking));
uint256 calcShares = (beforePool.tokens == 0)
? tokens
: ((tokens * beforePool.shares) / (beforePool.tokens - beforePool.tokensThawing));

// onTokenTransfer
if (calcShares == 0 || tokens < 1 ether) {
vm.expectEmit();
emit Transfer(address(staking), delegator, tokens);
vm.expectEmit();
emit IL2StakingBase.TransferredDelegationReturnedToDelegator(serviceProvider, delegator, tokens);
} else {
vm.expectEmit();
emit IHorizonStakingExtension.StakeDelegated(serviceProvider, delegator, tokens, calcShares);
}
staking.onTokenTransfer(counterpartStaking, tokens, data);

// after
DelegationPool memory afterPool = staking.getDelegationPool(serviceProvider, subgraphDataServiceLegacyAddress);
Delegation memory afterDelegation = staking.getDelegation(
serviceProvider,
subgraphDataServiceLegacyAddress,
delegator
);
uint256 afterStoragePoolTokens = uint256(vm.load(address(staking), slotPoolTokens));
uint256 afterDelegatedTokens = staking.getDelegatedTokensAvailable(
serviceProvider,
subgraphDataServiceLegacyAddress
);
uint256 afterDelegatorBalance = token.balanceOf(delegator);
uint256 afterStakingBalance = token.balanceOf(address(staking));

uint256 deltaShares = afterDelegation.shares - beforeDelegation.shares;

bytes memory data = abi.encode(uint8(IL2StakingTypes.L1MessageCodes.RECEIVE_DELEGATION_CODE), abi.encode(users.indexer, users.delegator));
staking.onTokenTransfer(counterpartStaking, 0, data);
// assertions
if (calcShares == 0 || tokens < 1 ether) {
assertEq(beforePool.tokens, afterPool.tokens);
assertEq(beforePool.shares, afterPool.shares);
assertEq(beforePool.tokensThawing, afterPool.tokensThawing);
assertEq(beforePool.sharesThawing, afterPool.sharesThawing);
assertEq(0, deltaShares);
assertEq(beforeDelegatedTokens, afterDelegatedTokens);
assertEq(beforeStoragePoolTokens, afterStoragePoolTokens);
assertEq(beforeDelegatorBalance + tokens, afterDelegatorBalance);
assertEq(beforeStakingBalance - tokens, afterStakingBalance);
} else {
assertEq(beforePool.tokens + tokens, afterPool.tokens);
assertEq(beforePool.shares + calcShares, afterPool.shares);
assertEq(beforePool.tokensThawing, afterPool.tokensThawing);
assertEq(beforePool.sharesThawing, afterPool.sharesThawing);
assertEq(calcShares, deltaShares);
assertEq(beforeDelegatedTokens + tokens, afterDelegatedTokens);
// Ensure correct slot is being updated, pools are stored in different storage locations for legacy subgraph data service
assertEq(beforeStoragePoolTokens + tokens, afterStoragePoolTokens);
assertEq(beforeDelegatorBalance, afterDelegatorBalance);
assertEq(beforeStakingBalance, afterStakingBalance);
}
}
}

0 comments on commit afe8bbc

Please sign in to comment.