Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
541 changes: 0 additions & 541 deletions .yarn/plugins/@yarnpkg/plugin-interactive-tools.cjs

This file was deleted.

9 changes: 0 additions & 9 deletions .yarn/plugins/@yarnpkg/plugin-typescript.cjs

This file was deleted.

550 changes: 0 additions & 550 deletions .yarn/plugins/@yarnpkg/plugin-version.cjs

This file was deleted.

874 changes: 0 additions & 874 deletions .yarn/releases/yarn-3.6.4.cjs

This file was deleted.

942 changes: 942 additions & 0 deletions .yarn/releases/yarn-4.10.3.cjs

Large diffs are not rendered by default.

18 changes: 5 additions & 13 deletions .yarnrc.yml
Original file line number Diff line number Diff line change
@@ -1,23 +1,15 @@
changesetBaseRefs:
- main
- origin/main
- main
- origin/main

enableImmutableInstalls: false

enableProgressBars: false

nodeLinker: node-modules

npmAuthToken: '${NPM_TOKEN:-}'
npmAuthToken: "${NPM_TOKEN:-}"

npmPublishRegistry: 'https://registry.npmjs.org'
npmPublishRegistry: "https://registry.npmjs.org"

plugins:
- path: .yarn/plugins/@yarnpkg/plugin-typescript.cjs
spec: '@yarnpkg/plugin-typescript'
- path: .yarn/plugins/@yarnpkg/plugin-version.cjs
spec: '@yarnpkg/plugin-version'
- path: .yarn/plugins/@yarnpkg/plugin-interactive-tools.cjs
spec: '@yarnpkg/plugin-interactive-tools'

yarnPath: .yarn/releases/yarn-3.6.4.cjs
yarnPath: .yarn/releases/yarn-4.10.3.cjs
82 changes: 78 additions & 4 deletions contracts/Staking.sol
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,17 @@ contract Staking is IStaking, Initializable, OwnableUpgradeable, SQParameter {
// Staking runner lengths
mapping(address => uint256) public stakingIndexerLengths;

// Instant delegation quota per era (per wallet)
uint256 public instantDelegationQuota;

// Era window percentage for instant delegation (in perMill, e.g., 700,000 = 70%)
uint256 public instantEraWindowPercent;

// Instant quota usage tracking

// Track instant quota used per delegator: delegator => QuotaUsage
mapping(address => InstantQuotaUsage) public instantQuotaUsed;

// -- Events --

/**
Expand Down Expand Up @@ -350,18 +361,20 @@ contract Staking is IStaking, Initializable, OwnableUpgradeable, SQParameter {
emit DelegationAdded2(_source, _runner, _amount, instant);
}

function delegateToIndexer(
/**
* @dev Transfer tokens from source to this contract (for instant delegation)
* @param _source The source address
* @param _amount The amount to transfer
*/
function transferDelegationTokens(
address _source,
address _runner,
uint256 _amount
) external onlyStakingManager {
IERC20(settings.getContractAddress(SQContracts.SQToken)).safeTransferFrom(
_source,
address(this),
_amount
);

this.addDelegation(_source, _runner, _amount, false);
}

function removeDelegation(address _source, address _runner, uint256 _amount) external {
Expand Down Expand Up @@ -492,11 +505,72 @@ contract Staking is IStaking, Initializable, OwnableUpgradeable, SQParameter {
this.startUnbond(_runner, _runner, _amount, UnbondType.Commission);
}

/**
* @notice Update instant quota used for a delegator in a specific era
* @param delegator The delegator address
* @param era The era number
* @param amount The amount to add to used quota
*/
function updateInstantQuotaUsed(
address delegator,
uint256 era,
uint256 amount
) external onlyStakingManager {
InstantQuotaUsage storage usage = instantQuotaUsed[delegator];

// If era changed, reset the quota usage
if (usage.era != era) {
usage.era = era;
usage.amount = amount;
} else {
// Same era, accumulate
usage.amount += amount;
}
}

/**
* @notice Set instant delegation parameters
* @param _perEraQuota The quota per era per wallet
* @param _windowPercent The era window percentage (in perMill)
*/
function setInstantDelegationParams(
uint256 _perEraQuota,
uint256 _windowPercent
) external onlyOwner {
require(_windowPercent <= PER_MILL, 'S015');
instantDelegationQuota = _perEraQuota;
instantEraWindowPercent = _windowPercent;

emit Parameter('instantDelegationQuota', abi.encode(_perEraQuota));
emit Parameter('instantEraWindowPercent', abi.encode(_windowPercent));
}

// -- Views --

function isEmptyDelegation(address _source, address _runner) external view returns (bool) {
return
delegation[_source][_runner].valueAt == 0 &&
delegation[_source][_runner].valueAfter == 0;
}

/**
* @notice Get remaining instant quota for a delegator in a specific era
* @param delegator The delegator address
* @param era The era number
* @return The remaining quota amount
*/
function getInstantQuotaRemaining(
address delegator,
uint256 era
) external view returns (uint256) {
InstantQuotaUsage memory usage = instantQuotaUsed[delegator];

// If different era or not yet used, full quota available
if (usage.era != era) {
return instantDelegationQuota;
}

// Same era, return remaining
return instantDelegationQuota > usage.amount ? instantDelegationQuota - usage.amount : 0;
}
}
108 changes: 105 additions & 3 deletions contracts/StakingManager.sol
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,16 @@ import './interfaces/IIndexerRegistry.sol';
import './interfaces/IStakingManager.sol';
import './utils/MathUtil.sol';
import './utils/StakingUtil.sol';
import './Constants.sol';
import '@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol';
import '@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol';

/**
* Split from Staking, to keep contract size under control
*/
contract StakingManager is IStakingManager, Initializable, OwnableUpgradeable {
using MathUtil for uint256;

ISettings public settings;

/**
Expand Down Expand Up @@ -50,18 +53,59 @@ contract StakingManager is IStakingManager, Initializable, OwnableUpgradeable {
} else {
require(msg.sender == _runner, 'G002');
}
staking.delegateToIndexer(_runner, _runner, _amount);
staking.transferDelegationTokens(_runner, _amount);
staking.addDelegation(_runner, _runner, _amount, false);
}

/**
* @dev Delegator stake to Indexer, Indexer cannot call this.
* Supports instant delegation with quota-based limits and era window restrictions.
*/
function delegate(address _runner, uint256 _amount) external {
require(msg.sender != _runner, 'G004');
Staking staking = Staking(settings.getContractAddress(SQContracts.Staking));
// delegation limit should not exceed

// Check delegation limitation
staking.checkDelegateLimitation(_runner, _amount);
staking.delegateToIndexer(msg.sender, _runner, _amount);

// Transfer tokens first
staking.transferDelegationTokens(msg.sender, _amount);

// Check era progress (70% window by default)
uint256 eraProgress = _calculateEraProgress();
uint256 windowPercent = staking.instantEraWindowPercent();
bool inInstantWindow = windowPercent > 0 && eraProgress <= windowPercent;

if (!inInstantWindow) {
// After window: all delegation is pending
staking.addDelegation(msg.sender, _runner, _amount, false);
return;
}

// Within window: check quota
uint256 remainingQuota = _getRemainingQuota(msg.sender);

if (_amount <= remainingQuota) {
// Case A: Fully instant
staking.addDelegation(msg.sender, _runner, _amount, true);
_applyInstantDelegation(msg.sender, _runner);
_consumeInstantQuota(msg.sender, _amount);
} else if (remainingQuota > 0) {
// Case B: Split - instant + pending
uint256 instantAmount = remainingQuota;
uint256 pendingAmount = _amount - remainingQuota;

// Instant portion
staking.addDelegation(msg.sender, _runner, instantAmount, true);
_applyInstantDelegation(msg.sender, _runner);
_consumeInstantQuota(msg.sender, instantAmount);

// Pending portion
staking.addDelegation(msg.sender, _runner, pendingAmount, false);
} else {
// Case C: Quota exhausted - all pending
staking.addDelegation(msg.sender, _runner, _amount, false);
}
}

/**
Expand Down Expand Up @@ -214,6 +258,64 @@ contract StakingManager is IStakingManager, Initializable, OwnableUpgradeable {
staking.slashRunner(_indexer, _amount);
}

/**
* @dev Calculate current era progress as a percentage (in perMill)
* @return Progress value (0-PER_MILL, where PER_MILL = 100%)
*/
function _calculateEraProgress() internal view returns (uint256) {
IEraManager eraManager = IEraManager(settings.getContractAddress(SQContracts.EraManager));

uint256 eraStartTime = eraManager.eraStartTime();
uint256 eraPeriod = eraManager.eraPeriod();

uint256 elapsed = block.timestamp - eraStartTime;

// Prevent overflow: if elapsed >= eraPeriod, return 100%
if (elapsed >= eraPeriod) {
return PER_MILL;
}

return MathUtil.mulDiv(elapsed, PER_MILL, eraPeriod);
}

/**
* @dev Get remaining instant delegation quota for a delegator
* @param delegator The delegator address
* @return Remaining quota amount
*/
function _getRemainingQuota(address delegator) internal view returns (uint256) {
IEraManager eraManager = IEraManager(settings.getContractAddress(SQContracts.EraManager));
Staking staking = Staking(settings.getContractAddress(SQContracts.Staking));

uint256 currentEra = eraManager.eraNumber();
return staking.getInstantQuotaRemaining(delegator, currentEra);
}

/**
* @dev Apply instant delegation to rewards system
* @param delegator The delegator address
* @param runner The runner address
*/
function _applyInstantDelegation(address delegator, address runner) internal {
IRewardsStaking rewardsStaking = IRewardsStaking(
settings.getContractAddress(SQContracts.RewardsStaking)
);
rewardsStaking.applyRedelegation(runner, delegator);
}

/**
* @dev Consume instant quota for a delegator
* @param delegator The delegator address
* @param amount The amount to consume
*/
function _consumeInstantQuota(address delegator, uint256 amount) internal {
IEraManager eraManager = IEraManager(settings.getContractAddress(SQContracts.EraManager));
Staking staking = Staking(settings.getContractAddress(SQContracts.Staking));

uint256 currentEra = eraManager.eraNumber();
staking.updateInstantQuotaUsed(delegator, currentEra, amount);
}

// -- Views --

function _getCurrentDelegationAmount(
Expand Down
31 changes: 31 additions & 0 deletions contracts/interfaces/IStaking.sol
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,39 @@ enum UnbondType {
Merge
}

/**
* @dev Instant delegation quota tracking. One per Delegator.
* Tracks quota usage within current era, auto-resets on era change.
*/
struct InstantQuotaUsage {
uint256 era; // era of quota usage
uint256 amount; // quota used in this era
}

interface IStaking {
function lockedAmount(address _delegator) external view returns (uint256);

function unbondCommission(address _runner, uint256 _amount) external;

function addDelegation(
address _source,
address _runner,
uint256 _amount,
bool instant
) external;

function transferDelegationTokens(address _source, uint256 _amount) external;

function updateInstantQuotaUsed(address delegator, uint256 era, uint256 amount) external;

function setInstantDelegationParams(uint256 _perEraQuota, uint256 _windowPercent) external;

function getInstantQuotaRemaining(
address delegator,
uint256 era
) external view returns (uint256);

function instantDelegationQuota() external view returns (uint256);

function instantEraWindowPercent() external view returns (uint256);
}
30 changes: 2 additions & 28 deletions hardhat.config.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import * as dotenv from 'dotenv';
import '@nomiclabs/hardhat-etherscan';
import '@nomicfoundation/hardhat-verify';
import '@nomiclabs/hardhat-waffle';
import '@typechain/hardhat';
import 'hardhat-contract-sizer';
Expand Down Expand Up @@ -453,33 +453,7 @@ const config: HardhatUserConfig = {
gasPrice: 21,
},
etherscan: {
apiKey: {
polygonMumbai: process.env.POLYGONSCAN_API_KEY,
goerli: process.env.ETHERSCAN_API_KEY,
sepolia: process.env.ETHERSCAN_API_KEY,
base: process.env.BASESCAN_API_KEY,
'base-sepolia': process.env.BASESCAN_API_KEY,
polygon: process.env.POLYGONSCAN_API_KEY,
mainnet: process.env.ETHERSCAN_API_KEY,
},
customChains: [
{
network: 'base-sepolia',
chainId: 84532,
urls: {
apiURL: 'https://api-sepolia.basescan.org/api',
browserURL: 'https://sepolia.basescan.org',
},
},
{
network: 'base',
chainId: 8453,
urls: {
apiURL: 'https://api.basescan.org/api',
browserURL: 'https://basescan.org',
},
},
],
apiKey: process.env.ETHERSCAN_API_KEY,
},
Comment on lines 455 to 457
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

Verify multi-network verification still works.

The etherscan configuration has been simplified from a multi-network apiKey object to a single string. According to the AI summary, customChains configuration for base-sepolia and base networks was also removed.

The scripts at lines 26-27 reference scan-verify tasks for testnet (base-sepolia) and mainnet (base) networks. Ensure these verification tasks still function correctly without the custom chain definitions.


🌐 Web query:

Does @nomicfoundation/hardhat-verify version 2.1.3 require customChains configuration for Base and Base Sepolia networks?

💡 Result:

Short answer: No — not always. The plugin doesn't always force a customChains entry, but Base Sepolia frequently needs one unless your installed hardhat-verify version already includes Basescan as a built-in explorer. Check what networks your installed plugin knows with:

npx hardhat verify --list-networks

If "base" / "base-sepolia" (or the exact network key you use) is not listed, add a customChains entry for Base / Base Sepolia in your etherscan config (examples and Base docs show adding customChains for base-sepolia). [1][2][3]

Sources:

  • hardhat-verify docs (how to list supported networks / add custom chains). [1]
  • Base docs showing example customChains for Base Sepolia. [2]
  • Hardhat verification guide explaining customChains usage. [3]

I need to verify the repository's setup to determine if the removal of customChains is indeed problematic. Let me inspect the configuration and installed versions.


🏁 Script executed:

#!/bin/bash
# Check installed version of hardhat-verify and current hardhat.config.ts state
echo "=== Checking package.json for hardhat-verify version ===" 
jq '.devDependencies["@nomicfoundation/hardhat-verify"]' package.json 2>/dev/null || rg '@nomicfoundation/hardhat-verify' package.json -A1 -B1

echo ""
echo "=== Current etherscan config in hardhat.config.ts (lines 450-480) ==="
sed -n '450,480p' hardhat.config.ts

echo ""
echo "=== Checking for customChains in hardhat.config.ts ==="
rg -n 'customChains' hardhat.config.ts -C3 || echo "No customChains found"

echo ""
echo "=== Lines 20-40 to see scan-verify task references ==="
sed -n '20,40p' hardhat.config.ts

Length of output: 1773


I need to locate the scan-verify tasks referenced and search for more specific information about Base network support in v2.1.3.


🏁 Script executed:

#!/bin/bash
# Find scan-verify or verify task definitions
echo "=== Searching for scan-verify or verify tasks ==="
rg -n 'scan-verify|verify.*task' hardhat.config.ts -C5

echo ""
echo "=== Searching for any reference to base networks ==="
rg -n -i 'base.*sepolia|base-sepolia|base' hardhat.config.ts -C2

echo ""
echo "=== Checking git log to see what was changed ==="
git log --oneline -20 hardhat.config.ts 2>/dev/null || echo "Git history not available"

echo ""
echo "=== Checking git diff for recent changes to etherscan config ==="
git diff HEAD~5..HEAD hardhat.config.ts 2>/dev/null | grep -A10 -B10 'etherscan\|customChains' || echo "Git diff not available"

Length of output: 2751


I need to search for specific information about Base and Base Sepolia support in @nomicfoundation/hardhat-verify v2.1.3, and whether the single apiKey string is compatible with multi-network verification.


🌐 Web query:

@nomicfoundation/hardhat-verify 2.1.3 Base Sepolia built-in support

💡 Result:

Short answer: No — @nomicfoundation/hardhat-verify v2.1.3 does not include built‑in support for Base Sepolia (chainId 84532). Use a customChains entry (or map the explorer to a supported provider) to verify on Base Sepolia. [1][2]

Sources:

  • Hardhat verify docs (plugin uses built‑in Etherscan/Blockscout support; customChains available). [1]
  • Community report showing IGN1002 error for "Base Sepolia" (chainId 84532) and instruction to add a custom chain. [2]

References:
[1] https://hardhat.org/docs/plugins/hardhat-verify
[2] https://forum.openzeppelin.com/t/error-in-plugin-hardhat-ignition-need-some-help/42142


I need to verify if Base mainnet has built-in support in v2.1.3, and clarify the apiKey configuration approach.


🌐 Web query:

@nomicfoundation/hardhat-verify Base mainnet chainId 8453 built-in explorer

💡 Result:

Yes — Base mainnet uses chainId 8453, and @nomicfoundation/hardhat-verify supports Blockscout-compatible explorers (so you can verify on Base’s Blockscout/Etherscan-style explorers). Sources: Hardhat Verify docs and Base docs. [1][2][3]

Minimal example (hardhat.config.js / ts) — verify using Blockscout for Base mainnet (chainId 8453):

  • set your network entry with chainId: 8453
  • run verify with provider "blockscout" (or use the verify task)

Example (JS):
{
networks: {
base: {
url: "https://mainnet.base.org",
chainId: 8453,
accounts: [ "0x..." ]
}
},
verify: {
// API key not required for some Blockscout instances; when needed put it here
blockscout: "<API_KEY_IF_REQUIRED>"
}
}

Verify command examples:

  • npx hardhat verify --network base <DEPLOYED_ADDRESS> "constructorArg1"
  • programmatic: verifyContract({ address: "...", constructorArgs: [...], provider: "blockscout" }, hre).

References:

  1. @nomicfoundation/hardhat-verify docs — usage and provider "blockscout". [1]
  2. Base docs — Base Mainnet chainId = 8453 and Blockscout explorer (base.blockscout.com). [2]
  3. Blockscout announcement / REST API for Base explorer. [3]

Would you like a ready-to-use hardhat.config.js snippet with an example verify command including an actual Blockscout API URL?


Multiple critical issues introduced by this configuration change.

The removal of customChains combined with the simplified apiKey configuration breaks multi-network verification:

  1. Base Sepolia verification will fail — @nomicfoundation/hardhat-verify v2.1.3 does not have built-in Base Sepolia support; it requires a customChains entry for chainId 84532, which was removed.

  2. Multi-network API key structure is broken — The old configuration used an apiKey object with network-specific keys (separate BASESCAN_API_KEY and ETHERSCAN_API_KEY). The new configuration uses a single ETHERSCAN_API_KEY string for all networks, which is incorrect. Basescan requires its own API key, and other networks (Polygon, etc.) require their respective keys.

  3. Base mainnet may work via Blockscout — Base mainnet (8453) appears to have Blockscout support, but only if explicitly configured; the current simplified config does not guarantee this.

Fix required: Either restore the customChains definitions for Base and Base Sepolia and the multi-network apiKey object structure, or properly configure the verify provider and API keys for each network according to v2.1.3 plugin requirements.

🤖 Prompt for AI Agents
hardhat.config.ts around lines 455-457: the etherscan config was simplified to a
single apiKey string and customChains removed, which breaks multi-network
verification (Base Sepolia needs a customChains entry for chainId 84532 and
different networks require distinct API keys). Restore the apiKey object with
per-network keys (e.g., ETHERSCAN_API_KEY, BASESCAN_API_KEY,
POLYGONSCAN_API_KEY, etc.) and re-add customChains entries for Base (chainId
8453) and Base Sepolia (chainId 84532) per @nomicfoundation/hardhat-verify
v2.1.3 requirements; alternatively, configure the verify provider explicitly for
Base and Base Sepolia with the correct explorer URLs and API key mappings so
each network uses its proper provider and key.

typechain: {
outDir: 'src/typechain',
Expand Down
Loading