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 = 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;
}
}
107 changes: 104 additions & 3 deletions contracts/StakingManager.sol
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ 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 +52,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 +257,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-1000, where 1000 = 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);
}
21 changes: 20 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -101,5 +101,24 @@
"prettier --write --plugin=prettier-plugin-solidity"
]
},
"packageManager": "[email protected]"
"resolutions": {
"@ethersproject/abstract-provider": "~5.5.1",
"@ethersproject/abstract-signer": "~5.5.0",
"@ethersproject/address": "~5.5.0",
"@ethersproject/bignumber": "~5.5.0",
"@ethersproject/bytes": "~5.5.0",
"@ethersproject/constants": "~5.5.0",
"@ethersproject/contracts": "~5.5.0",
"@ethersproject/hash": "~5.5.0",
"@ethersproject/keccak256": "~5.5.0",
"@ethersproject/logger": "~5.5.0",
"@ethersproject/networks": "~5.5.2",
"@ethersproject/properties": "~5.5.0",
"@ethersproject/providers": "~5.5.3",
"@ethersproject/signing-key": "~5.5.0",
"@ethersproject/strings": "~5.5.0",
"@ethersproject/transactions": "~5.5.0",
"@ethersproject/wallet": "~5.5.0"
},
"packageManager": "[email protected]"
}
Loading