Skip to content

Commit

Permalink
Merge pull request #71 from Tenderize/nv/mainnet-fork-tests
Browse files Browse the repository at this point in the history
Feat: mainnet fork tests
  • Loading branch information
kyriediculous committed Dec 15, 2023
2 parents 78b890b + e0b49a6 commit 2e2571f
Show file tree
Hide file tree
Showing 23 changed files with 1,215 additions and 190 deletions.
7 changes: 3 additions & 4 deletions .gitmodules
Original file line number Diff line number Diff line change
@@ -1,7 +1,3 @@
[submodule "lib/forge-std"]
branch = "master"
path = lib/forge-std
url = https://github.com/foundry-rs/forge-std
[submodule "lib/prb-test"]
branch = "0.3.1"
path = "lib/prb-test"
Expand All @@ -26,3 +22,6 @@
[submodule "lib/uniswap-v3-core"]
path = lib/uniswap-v3-core
url = https://github.com/tenderize/uniswap-v3-core
[submodule "lib/forge-std"]
path = lib/forge-std
url = https://github.com/foundry-rs/forge-std
2 changes: 2 additions & 0 deletions foundry.toml
Original file line number Diff line number Diff line change
Expand Up @@ -32,3 +32,5 @@ depth = 100
[rpc_endpoints]
# Uncomment to enable the RPC server
arbitrum_goerli = "${ARBITRUM_GOERLI_RPC}"
arbitrum = "${ARBITRUM_RPC}"
mainnet = "${MAINNET_RPC}"
4 changes: 2 additions & 2 deletions src/adapters/Adapter.sol
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import { IERC165 } from "core/interfaces/IERC165.sol";
pragma solidity >=0.8.19;

interface Adapter is IERC165 {
function previewDeposit(uint256 assets) external view returns (uint256);
function previewDeposit(address validator, uint256 assets) external view returns (uint256);

function previewWithdraw(uint256 unlockID) external view returns (uint256);

Expand All @@ -24,7 +24,7 @@ interface Adapter is IERC165 {

function currentTime() external view returns (uint256);

function stake(address validator, uint256 amount) external;
function stake(address validator, uint256 amount) external returns (uint256 staked);

function unstake(address validator, uint256 amount) external returns (uint256 unlockID);

Expand Down
88 changes: 39 additions & 49 deletions src/adapters/GraphAdapter.sol
Original file line number Diff line number Diff line change
Expand Up @@ -14,18 +14,18 @@ pragma solidity >=0.8.19;
import { ERC20 } from "solmate/tokens/ERC20.sol";
import { SafeTransferLib } from "solmate/utils/SafeTransferLib.sol";
import { Adapter } from "core/adapters/Adapter.sol";
import { IGraphStaking, IEpochManager } from "core/adapters/interfaces/IGraph.sol";
import { IGraphStaking, IGraphEpochManager } from "core/adapters/interfaces/IGraph.sol";
import { IERC165 } from "core/interfaces/IERC165.sol";

IGraphEpochManager constant GRAPH_EPOCHS = IGraphEpochManager(0x5A843145c43d328B9bB7a4401d94918f131bB281);
IGraphStaking constant GRAPH_STAKING = IGraphStaking(0x00669A4CF01450B64E8A2A20E9b1FCB71E61eF03);
ERC20 constant GRT = ERC20(0x9623063377AD1B27544C965cCd7342f7EA7e88C7);
uint256 constant MAX_PPM = 1e6;

contract GraphAdapter is Adapter {
using SafeTransferLib for ERC20;

IGraphStaking private constant GRAPH = IGraphStaking(0xF55041E37E12cD407ad00CE2910B8269B01263b9);
IEpochManager private constant GRAPH_EPOCHS = IEpochManager(0x03541c5cd35953CD447261122F93A5E7b812D697);
ERC20 private constant GRT = ERC20(0xc944E90C64B2c07662A292be6244BDf05Cda44a7);
uint256 private constant MAX_PPM = 1e6;

uint256 private constant STORAGE = uint256(keccak256("xyz.tenderize.graph.withdrawals.storage.location")) - 1;
uint256 private constant STORAGE = uint256(keccak256("xyz.tenderize.graph.adapter.storage.location")) - 1;

error WithdrawPending();

Expand All @@ -45,7 +45,6 @@ contract GraphAdapter is Adapter {
uint256 lastEpochUnlockedAt;
mapping(uint256 => Epoch) epochs;
mapping(uint256 => Unlock) unlocks;
uint256 tokensPerShare;
}

function _loadStorage() internal pure returns (Storage storage $) {
Expand All @@ -61,8 +60,12 @@ contract GraphAdapter is Adapter {
return interfaceId == type(Adapter).interfaceId || interfaceId == type(IERC165).interfaceId;
}

function previewDeposit(uint256 assets) external view override returns (uint256) {
return assets - assets * GRAPH.delegationTaxPercentage() / MAX_PPM;
function previewDeposit(address validator, uint256 assets) external view override returns (uint256) {
assets -= assets * GRAPH_STAKING.delegationTaxPercentage() / MAX_PPM;
IGraphStaking.DelegationPool memory delPool = GRAPH_STAKING.delegationPools(validator);

uint256 shares = delPool.tokens != 0 ? assets * delPool.shares / delPool.tokens : assets;
return shares * (delPool.tokens + assets) / (delPool.shares + shares);
}

function previewWithdraw(uint256 unlockID) external view override returns (uint256) {
Expand All @@ -75,7 +78,7 @@ contract GraphAdapter is Adapter {
function unlockMaturity(uint256 unlockID) external view override returns (uint256) {
Storage storage $ = _loadStorage();
Unlock memory unlock = $.unlocks[unlockID];
uint256 THAWING_PERIOD = GRAPH.thawingPeriod();
uint256 THAWING_PERIOD = GRAPH_STAKING.thawingPeriod();
// if userEpoch == currentEpoch, it is yet to unlock
// => unlockBlock + thawingPeriod
// if userEpoch == currentEpoch - 1, it is processing
Expand All @@ -93,16 +96,22 @@ contract GraphAdapter is Adapter {
}

function unlockTime() external view override returns (uint256) {
return GRAPH.thawingPeriod();
return GRAPH_STAKING.thawingPeriod();
}

function currentTime() external view override returns (uint256) {
return block.number;
}

function stake(address validator, uint256 amount) external override {
GRT.safeApprove(address(GRAPH), amount);
GRAPH.delegate(validator, amount);
function isValidator(address validator) public view override returns (bool) {
return GRAPH_STAKING.hasStake(validator);
}

function stake(address validator, uint256 amount) external override returns (uint256) {
GRT.safeApprove(address(GRAPH_STAKING), amount);
uint256 delShares = GRAPH_STAKING.delegate(validator, amount);
IGraphStaking.DelegationPool memory delPool = GRAPH_STAKING.delegationPools(validator);
return delShares * delPool.tokens / delPool.shares;
}

function unstake(address validator, uint256 amount) external override returns (uint256 unlockID) {
Expand Down Expand Up @@ -144,62 +153,43 @@ contract GraphAdapter is Adapter {

function rebase(address validator, uint256 currentStake) external override returns (uint256 newStake) {
Storage storage $ = _loadStorage();
Epoch memory currentEpoch = $.epochs[$.currentEpoch];
IGraphStaking.DelegationPool memory delPool = GRAPH.delegationPools(validator);
uint256 currentEpochNum = $.currentEpoch;
Epoch memory currentEpoch = $.epochs[currentEpochNum];
IGraphStaking.DelegationPool memory delPool = GRAPH_STAKING.delegationPools(validator);

uint256 _tokensPerShare = delPool.shares != 0 ? delPool.tokens * 1 ether / delPool.shares : 1 ether;
newStake = currentStake;

// Account for rounding error of -1 or +1
// This occurs due to a slight change in ratio because of new delegations or withdrawals,
// rather than an effective reward or loss
if (
(_tokensPerShare >= $.tokensPerShare && _tokensPerShare - $.tokensPerShare <= 1)
|| (_tokensPerShare < $.tokensPerShare && $.tokensPerShare - _tokensPerShare <= 1)
) {
return newStake;
}

IGraphStaking.Delegation memory delegation = GRAPH.getDelegation(validator, address(this));
uint256 staked = delegation.shares * _tokensPerShare / 1 ether;
IGraphStaking.Delegation memory delegation = GRAPH_STAKING.getDelegation(validator, address(this));
uint256 staked = delegation.shares * delPool.tokens / delPool.shares;

// account for stake still to unstake
uint256 oldStake = currentStake + currentEpoch.amount;

// Last epoch amount should be synced with Delegation.tokensLocked
if ($.currentEpoch > 0) $.epochs[$.currentEpoch - 1].amount = delegation.tokensLocked;
if (currentEpochNum > 0) $.epochs[currentEpochNum - 1].amount = delegation.tokensLocked;

if (staked > oldStake) {
// handle rewards
// To reduce long waiting periods we want to still reward users
// for which their stake is still to be unlocked
// because technically it is not unlocked from the Graph either
// We do this by adding the rewards to the current epoch
uint256 currentEpochAmount = (staked - oldStake) * currentEpoch.amount / oldStake;
currentEpoch.amount += currentEpochAmount;
} else {
return newStake;
currentEpoch.amount += (staked - oldStake) * currentEpoch.amount / oldStake;
$.epochs[currentEpochNum].amount = currentEpoch.amount;
}

$.epochs[$.currentEpoch] = currentEpoch;
$.tokensPerShare = _tokensPerShare;

// slash/rewards is already accounted for in $.epochs[$.currentEpoch].amount
// rewards is already accounted for in $.epochs[$.currentEpoch].amount
newStake = staked - currentEpoch.amount;
}

function isValidator(address validator) public view override returns (bool) {
return GRAPH.hasStake(validator);
}

function _processWithdrawals(address validator) internal {
// process possible withdrawals before unstakes
_processWithdraw(validator);
_processUnstake(validator);
}

function _processUnstake(address validator) internal {
IGraphStaking.Delegation memory del = GRAPH.getDelegation(validator, address(this));
IGraphStaking.Delegation memory del = GRAPH_STAKING.getDelegation(validator, address(this));
// undelegation already ungoing: no-op
if (del.tokensLockedUntil != 0) return;

Expand All @@ -219,13 +209,13 @@ contract GraphAdapter is Adapter {
$.lastEpochUnlockedAt = block.number;

// calculate shares to undelegate from The Graph
uint256 undelegationShares = currentEpochAmount * 1 ether / $.tokensPerShare;

IGraphStaking.DelegationPool memory delPool = GRAPH_STAKING.delegationPools(validator);
uint256 undelegationShares = currentEpochAmount * delPool.shares / delPool.tokens;
// account for possible rounding error
undelegationShares = del.shares < undelegationShares ? del.shares : undelegationShares;

// undelegate
GRAPH.undelegate(validator, undelegationShares);
GRAPH_STAKING.undelegate(validator, undelegationShares);
} else if ($.epochs[$.currentEpoch - 1].amount != 0) {
++$.currentEpoch;
$.lastEpochUnlockedAt = block.number;
Expand All @@ -234,7 +224,7 @@ contract GraphAdapter is Adapter {

function _processWithdraw(address validator) internal {
// withdrawal isn't ready: no-op
uint256 tokensLockedUntil = GRAPH.getDelegation(validator, address(this)).tokensLockedUntil;
uint256 tokensLockedUntil = GRAPH_STAKING.getDelegation(validator, address(this)).tokensLockedUntil;
if (tokensLockedUntil == 0 || tokensLockedUntil > GRAPH_EPOCHS.currentEpoch()) return;

Storage storage $ = _loadStorage();
Expand All @@ -244,7 +234,7 @@ contract GraphAdapter is Adapter {
// $.currentEpoch - 1 is safe as we only call this function after at least 1 _processUnstake
// which increments $.currentEpoch, otherwise del.tokensLockedUntil would still be 0 and we would
// not reach this branch
$.epochs[$.currentEpoch - 1].amount = GRAPH.withdrawDelegated(validator, address(0));
$.epochs[$.currentEpoch - 1].amount = GRAPH_STAKING.withdrawDelegated(validator, address(0));
}
}
}
64 changes: 34 additions & 30 deletions src/adapters/LivepeerAdapter.sol
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,16 @@ import { IWETH9 } from "core/adapters/interfaces/IWETH9.sol";
import { IERC165 } from "core/interfaces/IERC165.sol";
import { TWAP } from "core/utils/TWAP.sol";

ILivepeerBondingManager constant LIVEPEER_BONDING = ILivepeerBondingManager(0x35Bcf3c30594191d53231E4FF333E8A770453e40);
ILivepeerRoundsManager constant LIVEPEER_ROUNDS = ILivepeerRoundsManager(0xdd6f56DcC28D3F5f27084381fE8Df634985cc39f);
ERC20 constant LPT = ERC20(0x289ba1701C2F088cf0faf8B3705246331cB8A839);
IWETH9 constant WETH = IWETH9(0x82aF49447D8a07e3bd95BD0d56f35241523fBab1);
ISwapRouter constant UNISWAP_ROUTER = ISwapRouter(0xE592427A0AEce92De3Edee1F18E0157C05861564);
address constant UNI_POOL = 0x4fD47e5102DFBF95541F64ED6FE13d4eD26D2546;
uint24 constant UNISWAP_POOL_FEE = 3000;
uint256 constant ETH_THRESHOLD = 1e16; // 0.01 ETH
uint32 constant TWAP_INTERVAL = 36_000;

contract LivepeerAdapter is Adapter {
using SafeTransferLib for ERC20;

Expand All @@ -39,26 +49,16 @@ contract LivepeerAdapter is Adapter {
}
}

ILivepeerBondingManager private constant LIVEPEER = ILivepeerBondingManager(0x35Bcf3c30594191d53231E4FF333E8A770453e40);
ILivepeerRoundsManager private constant LIVEPEER_ROUNDS = ILivepeerRoundsManager(0xdd6f56DcC28D3F5f27084381fE8Df634985cc39f);
ERC20 private constant LPT = ERC20(0x289ba1701C2F088cf0faf8B3705246331cB8A839);
IWETH9 private constant WETH = IWETH9(0x82aF49447D8a07e3bd95BD0d56f35241523fBab1);
ISwapRouter private constant UNISWAP_ROUTER = ISwapRouter(0xE592427A0AEce92De3Edee1F18E0157C05861564);
address private constant UNI_POOL = 0x4fD47e5102DFBF95541F64ED6FE13d4eD26D2546;
uint24 private constant UNISWAP_POOL_FEE = 10_000;
uint256 private constant ETH_THRESHOLD = 1e16; // 0.01 ETH
uint32 private constant TWAP_INTERVAL = 36_000;

function supportsInterface(bytes4 interfaceId) external pure override returns (bool) {
return interfaceId == type(Adapter).interfaceId || interfaceId == type(IERC165).interfaceId;
}

function previewDeposit(uint256 assets) external pure returns (uint256) {
function previewDeposit(address, /*validator*/ uint256 assets) external pure returns (uint256) {
return assets;
}

function previewWithdraw(uint256 unlockID) external view returns (uint256 amount) {
(amount,) = LIVEPEER.getDelegatorUnbondingLock(address(this), unlockID);
(amount,) = LIVEPEER_BONDING.getDelegatorUnbondingLock(address(this), unlockID);
}

function unlockMaturity(uint256 unlockID) external view returns (uint256 maturity) {
Expand All @@ -67,48 +67,49 @@ contract LivepeerAdapter is Adapter {
// roundLength = n
// currentRound = r
// withdrawRound = w
// blockRemainingInCurrentRound = b = roundLength - (block.number - currentRoundStartBlock)
// blocksRemainingInCurrentRound = b = roundLength - (block.number - currentRoundStartBlock)
// maturity = n*(w - r - 1) + b
(, uint256 withdrawRound) = LIVEPEER.getDelegatorUnbondingLock(address(this), unlockID);
(, uint256 withdrawRound) = LIVEPEER_BONDING.getDelegatorUnbondingLock(address(this), unlockID);
uint256 currentRound = LIVEPEER_ROUNDS.currentRound();
uint256 roundLength = LIVEPEER_ROUNDS.roundLength();
uint256 currentRoundStartBlock = LIVEPEER_ROUNDS.currentRoundStartBlock();
uint256 blockRemainingInCurrentRound = roundLength - (block.number - currentRoundStartBlock);
uint256 blocksRemainingInCurrentRound = roundLength - (block.number - currentRoundStartBlock);
if (withdrawRound > currentRound) {
maturity = roundLength * (withdrawRound - currentRound - 1) + blockRemainingInCurrentRound;
maturity = block.number + roundLength * (withdrawRound - currentRound - 1) + blocksRemainingInCurrentRound;
}
}

function unlockTime() external view override returns (uint256) {
return LIVEPEER_ROUNDS.roundLength() * LIVEPEER.unbondingPeriod();
return LIVEPEER_ROUNDS.roundLength() * LIVEPEER_BONDING.unbondingPeriod();
}

function currentTime() external view override returns (uint256) {
return block.number;
}

function stake(address validator, uint256 amount) public {
LPT.approve(address(LIVEPEER), amount);
LIVEPEER.bond(amount, validator);
function stake(address validator, uint256 amount) public returns (uint256) {
LPT.safeApprove(address(LIVEPEER_BONDING), amount);
LIVEPEER_BONDING.bond(amount, validator);
return amount;
}

function unstake(address, /*validator*/ uint256 amount) external returns (uint256 unlockID) {
// returns the *next* Livepeer unbonding lock ID for the delegator
// this will be the `unlockID` after calling unbond
(,,,,,, unlockID) = LIVEPEER.getDelegator(address(this));
LIVEPEER.unbond(amount);
(,,,,,, unlockID) = LIVEPEER_BONDING.getDelegator(address(this));
LIVEPEER_BONDING.unbond(amount);
}

function withdraw(address, /*validator*/ uint256 unlockID) external returns (uint256 amount) {
(amount,) = LIVEPEER.getDelegatorUnbondingLock(address(this), unlockID);
LIVEPEER.withdrawStake(unlockID);
(amount,) = LIVEPEER_BONDING.getDelegatorUnbondingLock(address(this), unlockID);
LIVEPEER_BONDING.withdrawStake(unlockID);
}

function rebase(address validator, uint256 currentStake) external returns (uint256 newStake) {
uint256 currentRound = LIVEPEER_ROUNDS.currentRound();

Storage storage $ = _loadStorage();
if ($.lastRebaseRound < currentRound) {
if ($.lastRebaseRound == currentRound) {
return currentStake;
}

Expand All @@ -123,28 +124,31 @@ contract LivepeerAdapter is Adapter {
}

// Read new stake
newStake = LIVEPEER.pendingStake(address(this), 0);
newStake = LIVEPEER_BONDING.pendingStake(address(this), 0);
}

function isValidator(address validator) public view override returns (bool) {
return LIVEPEER.isRegisteredTranscoder(validator);
return LIVEPEER_BONDING.isRegisteredTranscoder(validator);
}

/// @notice function for swapping ETH fees to LPT
function _livepeerClaimFees() internal {
// get pending fees
uint256 pendingFees;
if ((pendingFees = LIVEPEER.pendingFees(address(this), 0)) < ETH_THRESHOLD) return;
if ((pendingFees = LIVEPEER_BONDING.pendingFees(address(this), 0)) < ETH_THRESHOLD) return;

if (!LIVEPEER_ROUNDS.currentRoundInitialized()) return;

// withdraw fees
LIVEPEER.withdrawFees(payable(address(this)), pendingFees);
LIVEPEER_BONDING.withdrawFees(payable(address(this)), pendingFees);
// get ETH balance
uint256 ethBalance = address(this).balance;
// convert fees to WETH
WETH.deposit{ value: ethBalance }();
ERC20(address(WETH)).safeApprove(address(UNISWAP_ROUTER), ethBalance);
// Calculate Slippage Threshold
uint256 twapPrice = TWAP.getInversePriceX96(TWAP.getPriceX96(TWAP.getSqrtTwapX96(UNI_POOL, TWAP_INTERVAL)));
uint160 sqrtPriceLimitX96 = TWAP.getSqrtTwapX96(UNI_POOL, TWAP_INTERVAL);
uint256 twapPrice = TWAP.getInversePriceX96(TWAP.getPriceX96(sqrtPriceLimitX96));
uint256 amountOut = ethBalance * twapPrice >> 96;
// Create initial params for swap
ISwapRouter.ExactInputSingleParams memory params = ISwapRouter.ExactInputSingleParams({
Expand Down
Loading

0 comments on commit 2e2571f

Please sign in to comment.