From a986841306d3b303fc5cf56c47ecfdc28d1fce8a Mon Sep 17 00:00:00 2001 From: 0xh3rman <119309671+0xh3rman@users.noreply.github.com> Date: Thu, 4 Dec 2025 16:43:19 +0900 Subject: [PATCH 1/4] add monad staking lens contract --- .env.example | 1 + .github/workflows/ci.yml | 5 + AGENTS.md | 34 ++ foundry.toml | 2 + justfile | 5 + script/hub_reader/HubReader.s.sol | 2 +- script/monad/StakingLens.s.sol | 15 + script/stargate/GemStargateDeployer.s.sol | 4 +- src/hub_reader/HubReader.sol | 97 ++---- src/hub_reader/interface/IStakeCredit.sol | 14 +- src/hub_reader/interface/IStakeHub.sol | 36 +-- src/monad/IStaking.sol | 47 +++ src/monad/StakingLens.sol | 376 ++++++++++++++++++++++ src/stargate/StargateFeeReceiver.sol | 12 +- test/hub_reader/HubReader.t.sol | 24 +- 15 files changed, 545 insertions(+), 129 deletions(-) create mode 100644 AGENTS.md create mode 100644 script/monad/StakingLens.s.sol create mode 100644 src/monad/IStaking.sol create mode 100644 src/monad/StakingLens.sol diff --git a/.env.example b/.env.example index 1cfa4b2..48b4560 100644 --- a/.env.example +++ b/.env.example @@ -15,6 +15,7 @@ BSC_RPC_URL= AVALANCHE_RPC_URL= POLYGON_RPC_URL= ARBITRUM_RPC_URL= +MONAD_RPC_URL= # Etherscan API Keys ETHEREUM_SCAN_API_KEY= diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1bfa379..cf620c4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -23,6 +23,11 @@ jobs: with: version: nightly + - name: Forge lint + run: | + forge lint + id: lint + - name: Forge build run: | forge --version diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..ed07e64 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,34 @@ +# Repository Guidelines + +## Project Structure & Module Organization +- Core Solidity modules live in `src/`, split into `src/hub_reader` and `src/stargate` for BSC staking and cross-chain calls. +- Automation scripts reside in `script/` (Foundry) and `deploy/` (bash helpers like `deploy-stargate.sh`); compiled artifacts land in `out/`. +- Tests live in `test/` using `.t.sol` suffixes; dependencies stay in `lib/`; align configuration via root-level `foundry.toml` and `.env.example`. + +## Build, Test, and Development Commands +- `forge build` or `just build` compiles the workspace with default remappings. +- `forge test` and `forge test --rpc-url $BSC_RPC_URL` execute the suite locally or against a fork. +- `forge lint` then `forge fmt` keep Solidity style consistent; run them before sharing branches. +- `just deploy-hub-reader` and `just deploy-stargate optimism` broadcast deployments through the prewired RPCs. + +## Coding Style & Naming Conventions +- Use 4-space indentation, `pragma solidity ^0.8.x`, sorted imports, and SPDX identifiers. +- Name contracts, libraries, and interfaces in PascalCase; state variables in camelCase; constants in ALL_CAPS. +- Match test filenames to their targets (`StargateFeeReceiver.t.sol`) and prefix helper contracts with `Test`. +- Validate formatting with `forge fmt` or `forge fmt --check` before review. + +## Testing Guidelines +- Keep integration scenarios in dedicated contracts and isolate unit fixtures per module. +- Leverage `vm.expectRevert`, `vm.prank`, and explicit `assertEq` messages to clarify intent. +- When forking, pass the RPC with `--rpc-url` and note chain assumptions in header comments. +- Prioritize coverage of deposit, withdrawal, and fee flows; `forge coverage --report lcov` helps quantify readiness. + +## Commit & Pull Request Guidelines +- Follow the short imperative style seen in history (`add auto formatter`, `rename to StargateFeeReceiver`), keeping summaries under 65 characters. +- Reference tickets, flag deployment or configuration impacts, and list the tests you ran. +- For PRs, link on-chain transactions, attach explorer URLs or calldata, and note any environment variable or RPC updates. + +## Security & Configuration Tips +- Copy `.env.example` to `.env`, add RPC URLs and scan keys matching `foundry.toml`, and keep secrets untracked. +- Limit raw private keys to deployment contexts; favor hardware signing for `forge script --broadcast`. +- Before merging, confirm remappings, target chain IDs, and contract addresses to avoid cross-chain leaks. diff --git a/foundry.toml b/foundry.toml index 7e3b9b4..afa4140 100644 --- a/foundry.toml +++ b/foundry.toml @@ -2,6 +2,7 @@ src = "src" out = "out" libs = ["lib"] +via_ir = true [rpc_endpoints] ethereum = "${ETHEREUM_RPC_URL}" @@ -11,6 +12,7 @@ bsc = "${BSC_RPC_URL}" avalanche = "${AVALANCHE_RPC_URL}" polygon = "${POLYGON_RPC_URL}" arbitrum = "${ARBITRUM_RPC_URL}" +monad = "${MONAD_RPC_URL}" [etherscan] ethereum = { key = "${ETHEREUM_SCAN_API_KEY}" } diff --git a/justfile b/justfile index aa400bd..7659db9 100644 --- a/justfile +++ b/justfile @@ -1,5 +1,7 @@ set dotenv-load := true +list: + just --list build: forge build @@ -12,3 +14,6 @@ deploy-stargate CHAIN_NAME: deploy-hub-reader: forge script script/hub_reader/HubReader.s.sol:HubReaderScript --rpc-url "$BSC_RPC_URL" --broadcast --verify -vvvv + +deploy-monad-staking: + forge script --force script/monad/StakingLens.s.sol:StakingLensScript --rpc-url "$MONAD_RPC_URL" --broadcast -vvvv diff --git a/script/hub_reader/HubReader.s.sol b/script/hub_reader/HubReader.s.sol index 06de822..6fb075c 100644 --- a/script/hub_reader/HubReader.s.sol +++ b/script/hub_reader/HubReader.s.sol @@ -2,7 +2,7 @@ pragma solidity ^0.8.13; import {Script, console} from "forge-std/Script.sol"; -import "../../src/hub_reader/HubReader.sol"; +import {HubReader} from "../../src/hub_reader/HubReader.sol"; contract HubReaderScript is Script { function run() public { diff --git a/script/monad/StakingLens.s.sol b/script/monad/StakingLens.s.sol new file mode 100644 index 0000000..75a68fc --- /dev/null +++ b/script/monad/StakingLens.s.sol @@ -0,0 +1,15 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.15; + +import {Script, console} from "forge-std/Script.sol"; +import {StakingLens} from "../../src/monad/StakingLens.sol"; + +contract StakingLensScript is Script { + function run() public { + uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY"); + vm.startBroadcast(deployerPrivateKey); + StakingLens lens = new StakingLens(); + console.log("StakingLens deployed to:", address(lens)); + vm.stopBroadcast(); + } +} diff --git a/script/stargate/GemStargateDeployer.s.sol b/script/stargate/GemStargateDeployer.s.sol index 2d8717b..a9430ae 100644 --- a/script/stargate/GemStargateDeployer.s.sol +++ b/script/stargate/GemStargateDeployer.s.sol @@ -2,7 +2,7 @@ pragma solidity ^0.8.13; import {Script, console} from "forge-std/Script.sol"; -import "../../src/stargate/StargateFeeReceiver.sol"; +import {StargateFeeReceiver} from "../../src/stargate/StargateFeeReceiver.sol"; contract GemStargateDeployerScript is Script { struct NetworkConfig { @@ -31,7 +31,7 @@ contract GemStargateDeployerScript is Script { // Get values from environment address endpoint = vm.envAddress(endpointVar); - return NetworkConfig(endpoint); + return NetworkConfig({endpoint: endpoint}); } function run() public { diff --git a/src/hub_reader/HubReader.sol b/src/hub_reader/HubReader.sol index a99b6ec..dc027eb 100644 --- a/src/hub_reader/HubReader.sol +++ b/src/hub_reader/HubReader.sol @@ -43,32 +43,18 @@ contract HubReader { * * @return The validators */ - function getValidators( - uint16 offset, - uint16 limit - ) external view returns (Validator[] memory) { - (address[] memory operatorAddrs, , uint256 totalLength) = stakeHub - .getValidators(offset, limit); + function getValidators(uint16 offset, uint16 limit) external view returns (Validator[] memory) { + (address[] memory operatorAddrs,, uint256 totalLength) = stakeHub.getValidators(offset, limit); uint256 validatorCount = totalLength < limit ? totalLength : limit; Validator[] memory validators = new Validator[](validatorCount); for (uint256 i = 0; i < validatorCount; i++) { - (, bool jailed, ) = stakeHub.getValidatorBasicInfo( - operatorAddrs[i] - ); - string memory moniker = stakeHub - .getValidatorDescription(operatorAddrs[i]) - .moniker; - uint64 rate = stakeHub - .getValidatorCommission(operatorAddrs[i]) - .rate; + (, bool jailed,) = stakeHub.getValidatorBasicInfo(operatorAddrs[i]); + string memory moniker = stakeHub.getValidatorDescription(operatorAddrs[i]).moniker; + uint64 rate = stakeHub.getValidatorCommission(operatorAddrs[i]).rate; validators[i] = Validator({ - operatorAddress: operatorAddrs[i], - moniker: moniker, - commission: rate, - jailed: jailed, - apy: 0 + operatorAddress: operatorAddrs[i], moniker: moniker, commission: rate, jailed: jailed, apy: 0 }); } uint64[] memory apys = this.getAPYs(operatorAddrs, block.timestamp); @@ -86,16 +72,13 @@ contract HubReader { * * @return The delegations of the delegator */ - function getDelegations( - address delegator, - uint16 offset, - uint16 limit - ) external view returns (Delegation[] memory) { - ( - address[] memory operatorAddrs, - address[] memory creditAddrs, - uint256 totalLength - ) = stakeHub.getValidators(offset, limit); + function getDelegations(address delegator, uint16 offset, uint16 limit) + external + view + returns (Delegation[] memory) + { + (address[] memory operatorAddrs, address[] memory creditAddrs, uint256 totalLength) = + stakeHub.getValidators(offset, limit); uint256 validatorCount = totalLength < limit ? totalLength : limit; uint256 delegationCount = 0; Delegation[] memory delegations = new Delegation[](validatorCount); @@ -107,10 +90,7 @@ contract HubReader { if (amount > 0) { delegations[delegationCount] = Delegation({ - delegatorAddress: delegator, - validatorAddress: operatorAddrs[i], - shares: shares, - amount: amount + delegatorAddress: delegator, validatorAddress: operatorAddrs[i], shares: shares, amount: amount }); delegationCount++; } @@ -131,38 +111,29 @@ contract HubReader { * * @return The undelegations of the delegator */ - function getUndelegations( - address delegator, - uint16 offset, - uint16 limit - ) external view returns (Undelegation[] memory) { - ( - address[] memory operatorAddrs, - address[] memory creditAddrs, - uint256 totalLength - ) = stakeHub.getValidators(offset, limit); + function getUndelegations(address delegator, uint16 offset, uint16 limit) + external + view + returns (Undelegation[] memory) + { + (address[] memory operatorAddrs, address[] memory creditAddrs, uint256 totalLength) = + stakeHub.getValidators(offset, limit); uint256 validatorCount = totalLength < limit ? totalLength : limit; // first loop to get the number of unbond requests uint256 undelegationCount = 0; for (uint256 i = 0; i < validatorCount; i++) { - undelegationCount += IStakeCredit(creditAddrs[i]) - .pendingUnbondRequest(delegator); + undelegationCount += IStakeCredit(creditAddrs[i]).pendingUnbondRequest(delegator); } - Undelegation[] memory undelegations = new Undelegation[]( - undelegationCount - ); + Undelegation[] memory undelegations = new Undelegation[](undelegationCount); // resuse same local variable undelegationCount = 0; for (uint256 i = 0; i < validatorCount; i++) { - uint256 unbondCount = IStakeCredit(creditAddrs[i]) - .pendingUnbondRequest(delegator); + uint256 unbondCount = IStakeCredit(creditAddrs[i]).pendingUnbondRequest(delegator); for (uint256 j = 0; j < unbondCount; j++) { - IStakeCredit.UnbondRequest memory req = IStakeCredit( - creditAddrs[i] - ).unbondRequest(delegator, j); + IStakeCredit.UnbondRequest memory req = IStakeCredit(creditAddrs[i]).unbondRequest(delegator, j); undelegations[undelegationCount] = Undelegation({ delegatorAddress: delegator, validatorAddress: operatorAddrs[i], @@ -183,28 +154,22 @@ contract HubReader { * * @return The APYs of the validator in basis points, e.g. 195 is 1.95% */ - function getAPYs( - address[] memory operatorAddrs, - uint256 timestamp - ) external view returns (uint64[] memory) { + // forge-lint: disable-next-line(mixed-case-function) + function getAPYs(address[] memory operatorAddrs, uint256 timestamp) external view returns (uint64[] memory) { uint256 dayIndex = timestamp / stakeHub.BREATHE_BLOCK_INTERVAL(); uint256 length = operatorAddrs.length; uint64[] memory apys = new uint64[](length); for (uint256 i = 0; i < length; i++) { - uint256 total = stakeHub.getValidatorTotalPooledBNBRecord( - operatorAddrs[i], - dayIndex - ); + uint256 total = stakeHub.getValidatorTotalPooledBNBRecord(operatorAddrs[i], dayIndex); if (total == 0) { continue; } - uint256 reward = stakeHub.getValidatorRewardRecord( - operatorAddrs[i], - dayIndex - ); + uint256 reward = stakeHub.getValidatorRewardRecord(operatorAddrs[i], dayIndex); if (reward == 0) { continue; } + // casting to uint64 is safe because APY basis points from hub totals fit in 64 bits + // forge-lint: disable-next-line(unsafe-typecast) apys[i] = uint64((reward * 365 * 10000) / total); } return apys; diff --git a/src/hub_reader/interface/IStakeCredit.sol b/src/hub_reader/interface/IStakeCredit.sol index 99bce91..9f37b36 100644 --- a/src/hub_reader/interface/IStakeCredit.sol +++ b/src/hub_reader/interface/IStakeCredit.sol @@ -10,16 +10,10 @@ interface IStakeCredit { function balanceOf(address account) external view returns (uint256); - function getPooledBNBByShares( - uint256 shares - ) external view returns (uint256); + // forge-lint: disable-next-line(mixed-case-function) + function getPooledBNBByShares(uint256 shares) external view returns (uint256); - function pendingUnbondRequest( - address delegator - ) external view returns (uint256); + function pendingUnbondRequest(address delegator) external view returns (uint256); - function unbondRequest( - address delegator, - uint256 _index - ) external view returns (UnbondRequest memory); + function unbondRequest(address delegator, uint256 _index) external view returns (UnbondRequest memory); } diff --git a/src/hub_reader/interface/IStakeHub.sol b/src/hub_reader/interface/IStakeHub.sol index 45bf90d..ef13537 100644 --- a/src/hub_reader/interface/IStakeHub.sol +++ b/src/hub_reader/interface/IStakeHub.sol @@ -17,40 +17,22 @@ struct Commission { interface IStakeHub { function BREATHE_BLOCK_INTERVAL() external view returns (uint256); - function getValidators( - uint256 offset, - uint256 limit - ) + function getValidators(uint256 offset, uint256 limit) external view - returns ( - address[] memory operatorAddrs, - address[] memory creditAddrs, - uint256 totalLength - ); - - function getValidatorBasicInfo( - address operatorAddress - ) + returns (address[] memory operatorAddrs, address[] memory creditAddrs, uint256 totalLength); + + function getValidatorBasicInfo(address operatorAddress) external view returns (uint256 createdTime, bool jailed, uint256 jailUntil); - function getValidatorDescription( - address operatorAddress - ) external view returns (Description memory); + function getValidatorDescription(address operatorAddress) external view returns (Description memory); - function getValidatorCommission( - address operatorAddress - ) external view returns (Commission memory); + function getValidatorCommission(address operatorAddress) external view returns (Commission memory); - function getValidatorRewardRecord( - address operatorAddress, - uint256 index - ) external view returns (uint256); + function getValidatorRewardRecord(address operatorAddress, uint256 index) external view returns (uint256); - function getValidatorTotalPooledBNBRecord( - address operatorAddress, - uint256 index - ) external view returns (uint256); + // forge-lint: disable-next-line(mixed-case-function) + function getValidatorTotalPooledBNBRecord(address operatorAddress, uint256 index) external view returns (uint256); } diff --git a/src/monad/IStaking.sol b/src/monad/IStaking.sol new file mode 100644 index 0000000..17efcd3 --- /dev/null +++ b/src/monad/IStaking.sol @@ -0,0 +1,47 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.15; + +interface IStaking { + function getConsensusValidatorSet(uint32 startIndex) + external + returns (bool isDone, uint32 nextIndex, uint64[] memory valIds); + + function getWithdrawalRequest(uint64 validatorId, address delegator, uint8 withdrawId) + external + returns (uint256 withdrawalAmount, uint256 accRewardPerToken, uint64 withdrawEpoch); + + function getEpoch() external returns (uint64 epoch, bool inEpochDelayPeriod); + + function getValidator(uint64 validatorId) + external + returns ( + address authAddress, + uint64 flags, + uint256 stake, + uint256 accRewardPerToken, + uint256 commission, + uint256 unclaimedRewards, + uint256 consensusStake, + uint256 consensusCommission, + uint256 snapshotStake, + uint256 snapshotCommission, + bytes memory secpPubkey, + bytes memory blsPubkey + ); + + function getDelegations(address delegator, uint64 startValId) + external + returns (bool isDone, uint64 nextValId, uint64[] memory valIds); + + function getDelegator(uint64 validatorId, address delegator) + external + returns ( + uint256 stake, + uint256 accRewardPerToken, + uint256 unclaimedRewards, + uint256 deltaStake, + uint256 nextDeltaStake, + uint64 deltaEpoch, + uint64 nextDeltaEpoch + ); +} diff --git a/src/monad/StakingLens.sol b/src/monad/StakingLens.sol new file mode 100644 index 0000000..8b23731 --- /dev/null +++ b/src/monad/StakingLens.sol @@ -0,0 +1,376 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.15; + +import {Strings} from "@openzeppelin/contracts/utils/Strings.sol"; +import {IStaking} from "./IStaking.sol"; + +contract StakingLens { + using Strings for uint256; + + IStaking public constant STAKING = IStaking(0x0000000000000000000000000000000000001000); + + uint16 public constant MAX_DELEGATIONS = 128; + uint8 public constant MAX_WITHDRAW_IDS = 8; + uint32 public constant ACTIVE_VALIDATOR_SET = 200; + uint256 public constant MAX_POSITIONS = uint256(MAX_DELEGATIONS) * (2 + MAX_WITHDRAW_IDS); + + uint256 public constant MONAD_SCALE = 1e18; + uint256 public constant MONAD_BLOCK_REWARD = 25 ether; + uint256 public constant MONAD_BLOCKS_PER_YEAR = 78_840_000; + uint64 public constant APY_BPS_PRECISION = 10_000; + + enum DelegationState { + Active, + Activating, + Deactivating, + AwaitingWithdrawal + } + + struct DelegationPosition { + uint64 validatorId; + uint8 withdrawId; + DelegationState state; + uint256 amount; + uint256 rewards; // unclaimed rewards only for Active entries + uint64 withdrawEpoch; // populated for withdrawals + } + + struct DelegatorSnapshot { + uint256 stake; + uint256 pendingStake; + uint256 rewards; + } + + struct ValidatorInfo { + uint64 validatorId; + string name; + uint256 stake; + uint256 commission; + uint64 apyBps; + bool isActive; + } + + struct ValidatorData { + uint64 validatorId; + uint64 flags; + uint256 stake; + uint256 commission; + } + + /** + * @notice Sum staked, pending, and reward balances for a delegator. + * @dev Same data the Rust provider assembles for staking_monad::get_monad_staking_balance. + */ + function getBalance(address delegator) external returns (uint256 staked, uint256 pending, uint256 rewards) { + bool isDone; + uint64 nextValId; + uint64[] memory valIds; + + (isDone, nextValId, valIds) = STAKING.getDelegations(delegator, 0); + + while (true) { + uint256 len = valIds.length; + + for (uint256 i = 0; i < len; ++i) { + (uint256 stake,, uint256 unclaimedRewards, uint256 deltaStake, uint256 nextDeltaStake,,) = + STAKING.getDelegator(valIds[i], delegator); + + staked += stake; + pending += deltaStake + nextDeltaStake; + rewards += unclaimedRewards; + } + + if (isDone) { + break; + } + + (isDone, nextValId, valIds) = STAKING.getDelegations(delegator, nextValId); + } + } + + /** + * @notice Get all delegation positions for a delegator, including withdrawals. + * @dev Emits one entry per state (active, activating, withdrawal requests). + * Uses withdrawId as the tie-breaker so clients can build delegation_ids + * compatible with the Rust client (address:withdrawId). + */ + function getDelegations(address delegator) external returns (DelegationPosition[] memory positions) { + positions = new DelegationPosition[](MAX_POSITIONS); + uint256 positionCount = 0; + uint16 validatorCount = 0; + + (uint64 currentEpoch,) = STAKING.getEpoch(); + + bool isDone; + uint64 nextValId; + uint64[] memory valIds; + + (isDone, nextValId, valIds) = STAKING.getDelegations(delegator, 0); + + while (true) { + uint256 len = valIds.length; + + for (uint256 i = 0; i < len && validatorCount < MAX_DELEGATIONS; ++i) { + uint64 validatorId = valIds[i]; + positionCount = _processValidator(delegator, validatorId, currentEpoch, positions, positionCount); + ++validatorCount; + } + + if (isDone || validatorCount == MAX_DELEGATIONS || positionCount == MAX_POSITIONS) { + break; + } + + (isDone, nextValId, valIds) = STAKING.getDelegations(delegator, nextValId); + } + + assembly { + mstore(positions, positionCount) + } + } + + function _processValidator( + address delegator, + uint64 validatorId, + uint64 currentEpoch, + DelegationPosition[] memory positions, + uint256 positionCount + ) internal returns (uint256 newPositionCount) { + DelegatorSnapshot memory snap = _readDelegator(delegator, validatorId); + uint8 lastWithdrawId; + bool hasWithdrawals; + (positionCount, lastWithdrawId, hasWithdrawals) = + _appendWithdrawals(delegator, validatorId, currentEpoch, positions, positionCount); + + if (snap.stake == 0 && snap.pendingStake == 0 && !hasWithdrawals) { + return positionCount; + } + + if (snap.stake > 0 && positionCount < MAX_POSITIONS) { + positions[positionCount] = DelegationPosition({ + validatorId: validatorId, + withdrawId: lastWithdrawId, + state: DelegationState.Active, + amount: snap.stake, + rewards: snap.rewards, + withdrawEpoch: 0 + }); + ++positionCount; + } + + if (snap.pendingStake > 0 && positionCount < MAX_POSITIONS) { + positions[positionCount] = DelegationPosition({ + validatorId: validatorId, + withdrawId: lastWithdrawId, + state: DelegationState.Activating, + amount: snap.pendingStake, + rewards: 0, + withdrawEpoch: 0 + }); + ++positionCount; + } + + return positionCount; + } + + function _readDelegator(address delegator, uint64 validatorId) internal returns (DelegatorSnapshot memory snap) { + uint256 deltaStake; + uint256 nextDeltaStake; + (snap.stake,, snap.rewards, deltaStake, nextDeltaStake,,) = STAKING.getDelegator(validatorId, delegator); + snap.pendingStake = deltaStake + nextDeltaStake; + } + + function _appendWithdrawals( + address delegator, + uint64 validatorId, + uint64 currentEpoch, + DelegationPosition[] memory positions, + uint256 positionCount + ) internal returns (uint256 newPositionCount, uint8 lastWithdrawId, bool hasWithdrawals) { + uint256 count = positionCount; + + for (uint8 withdrawId = 0; withdrawId < MAX_WITHDRAW_IDS && count < MAX_POSITIONS; ++withdrawId) { + (uint256 amount,, uint64 withdrawEpoch) = STAKING.getWithdrawalRequest(validatorId, delegator, withdrawId); + if (amount == 0) { + continue; + } + + positions[count] = DelegationPosition({ + validatorId: validatorId, + withdrawId: withdrawId, + state: withdrawEpoch < currentEpoch ? DelegationState.AwaitingWithdrawal : DelegationState.Deactivating, + amount: amount, + rewards: 0, + withdrawEpoch: withdrawEpoch + }); + + ++count; + lastWithdrawId = withdrawId; + hasWithdrawals = true; + } + + return (count, lastWithdrawId, hasWithdrawals); + } + + /** + * @notice Return validator stats plus APY for a set of validator ids. + * @param validatorIds If empty, uses the curated Monad validator list. + * @param fallbackApyBps Optional fallback APY (basis points) if the network or validator math returns zero. + */ + function getValidators(uint64[] calldata validatorIds, uint64 fallbackApyBps) + external + returns (ValidatorInfo[] memory validators, uint64 networkApyBps) + { + uint64[] memory allValidatorIds = _allValidatorIds(); + uint64[] memory targetIds = validatorIds.length == 0 ? allValidatorIds : validatorIds; + + (ValidatorData[] memory data, uint256 totalStake) = _fetchValidators(allValidatorIds); + networkApyBps = _calculateNetworkApyBps(totalStake); + + validators = new ValidatorInfo[](targetIds.length); + for (uint256 i = 0; i < targetIds.length; ++i) { + (ValidatorData memory snapshot, bool found) = _findValidator(data, targetIds[i]); + if (!found) { + snapshot.validatorId = targetIds[i]; + } + + uint64 validatorApy = _validatorApyBps(snapshot.stake, totalStake, snapshot.commission, networkApyBps); + if (validatorApy == 0) { + validatorApy = fallbackApyBps; + } + + validators[i] = ValidatorInfo({ + validatorId: snapshot.validatorId, + name: uint256(snapshot.validatorId).toString(), + stake: snapshot.stake, + commission: snapshot.commission, + apyBps: validatorApy, + isActive: found && snapshot.flags == 0 && snapshot.stake > 0 + }); + } + } + + /** + * @notice Return APYs for a set of validator ids, mirroring HubReader.getAPYs style. + * @param validatorIds If empty, uses the curated Monad validator list. + * @param fallbackApyBps Optional fallback APY (basis points) if the network or validator math returns zero. + */ + // forge-lint: disable-next-line(mixed-case-function) + function getAPYs(uint64[] calldata validatorIds, uint64 fallbackApyBps) external returns (uint64[] memory apysBps) { + uint64[] memory allValidatorIds = _allValidatorIds(); + uint64[] memory targetIds = validatorIds.length == 0 ? allValidatorIds : validatorIds; + + (ValidatorData[] memory data, uint256 totalStake) = _fetchValidators(allValidatorIds); + uint64 networkApyBps = _calculateNetworkApyBps(totalStake); + + apysBps = new uint64[](targetIds.length); + for (uint256 i = 0; i < targetIds.length; ++i) { + (ValidatorData memory snapshot, bool found) = _findValidator(data, targetIds[i]); + if (!found) { + apysBps[i] = fallbackApyBps; + continue; + } + + uint64 validatorApy = _validatorApyBps(snapshot.stake, totalStake, snapshot.commission, networkApyBps); + apysBps[i] = validatorApy > 0 ? validatorApy : fallbackApyBps; + } + } + + function _allValidatorIds() internal returns (uint64[] memory validatorIds) { + validatorIds = new uint64[](ACTIVE_VALIDATOR_SET); + + uint256 count = 0; + bool isDone; + uint32 nextIndex; + uint64[] memory page; + + (isDone, nextIndex, page) = STAKING.getConsensusValidatorSet(0); + while (true) { + uint256 len = page.length; + for (uint256 i = 0; i < len && count < ACTIVE_VALIDATOR_SET; ++i) { + validatorIds[count] = page[i]; + ++count; + } + + if (isDone || count == ACTIVE_VALIDATOR_SET) { + break; + } + + (isDone, nextIndex, page) = STAKING.getConsensusValidatorSet(nextIndex); + } + + if (count == 0) { + for (uint64 id = 1; id <= ACTIVE_VALIDATOR_SET; ++id) { + validatorIds[count] = id; + ++count; + } + } + + assembly { + mstore(validatorIds, count) + } + } + + function _fetchValidators(uint64[] memory validatorIds) + internal + returns (ValidatorData[] memory validators, uint256 totalStake) + { + uint256 len = validatorIds.length; + validators = new ValidatorData[](len); + + for (uint256 i = 0; i < len; ++i) { + (, uint64 flags, uint256 stake,, uint256 commission,,,,,,,) = STAKING.getValidator(validatorIds[i]); + + validators[i] = + ValidatorData({validatorId: validatorIds[i], flags: flags, stake: stake, commission: commission}); + + totalStake += stake; + } + } + + function _findValidator(ValidatorData[] memory validators, uint64 validatorId) + internal + pure + returns (ValidatorData memory validator, bool found) + { + uint256 len = validators.length; + for (uint256 i = 0; i < len; ++i) { + if (validators[i].validatorId == validatorId) { + return (validators[i], true); + } + } + + return (validator, false); + } + + function _calculateNetworkApyBps(uint256 totalStake) internal pure returns (uint64) { + if (totalStake == 0) { + return 0; + } + + uint256 annualRewards = MONAD_BLOCK_REWARD * MONAD_BLOCKS_PER_YEAR; + // casting to uint64 is safe because APY basis points derived from network totals fit in 64 bits + // forge-lint: disable-next-line(unsafe-typecast) + return uint64((annualRewards * APY_BPS_PRECISION) / totalStake); + } + + function _validatorApyBps(uint256 validatorStake, uint256 totalStake, uint256 commission, uint64 networkApyBps) + internal + pure + returns (uint64) + { + if (validatorStake == 0 || totalStake == 0) { + return networkApyBps; + } + + uint256 stakeWeight = (validatorStake * MONAD_SCALE) / totalStake; + uint256 expectedBlocks = (stakeWeight * MONAD_BLOCKS_PER_YEAR) / MONAD_SCALE; + uint256 grossRewards = expectedBlocks * MONAD_BLOCK_REWARD; + uint256 commissionCut = commission > MONAD_SCALE ? MONAD_SCALE : commission; + uint256 netRewards = (grossRewards * (MONAD_SCALE - commissionCut)) / MONAD_SCALE; + + uint256 apyBps = (netRewards * APY_BPS_PRECISION) / validatorStake; + // casting to uint64 is safe because APY basis points are capped by uint64 max guard above + // forge-lint: disable-next-line(unsafe-typecast) + return apyBps > type(uint64).max ? type(uint64).max : uint64(apyBps); + } +} diff --git a/src/stargate/StargateFeeReceiver.sol b/src/stargate/StargateFeeReceiver.sol index 39bf126..e921eb2 100644 --- a/src/stargate/StargateFeeReceiver.sol +++ b/src/stargate/StargateFeeReceiver.sol @@ -6,10 +6,10 @@ import {OFTComposeMsgCodec} from "@layerzerolabs/lz-evm-oapp-v2/contracts/oft/li import {MulticallHandler} from "../library/MulticallHandler.sol"; contract StargateFeeReceiver is ILayerZeroComposer, MulticallHandler { - address public immutable endpoint; + address public immutable ENDPOINT; constructor(address _endpoint) { - endpoint = _endpoint; + ENDPOINT = _endpoint; } function lzCompose( @@ -18,8 +18,12 @@ contract StargateFeeReceiver is ILayerZeroComposer, MulticallHandler { bytes calldata _message, address, // _executor bytes calldata // _extraData - ) external payable override { - require(msg.sender == endpoint, "!endpoint"); + ) + external + payable + override + { + require(msg.sender == ENDPOINT, "!endpoint"); // Decode message bytes memory composeMsg = OFTComposeMsgCodec.composeMsg(_message); diff --git a/test/hub_reader/HubReader.t.sol b/test/hub_reader/HubReader.t.sol index c97fc95..40f0d3f 100644 --- a/test/hub_reader/HubReader.t.sol +++ b/test/hub_reader/HubReader.t.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.13; -import {Test, console} from "forge-std/Test.sol"; +import {Test} from "forge-std/Test.sol"; import {HubReader, Validator, Delegation, Undelegation} from "../../src/hub_reader/HubReader.sol"; contract ValidatorsTest is Test { @@ -30,36 +30,22 @@ contract ValidatorsTest is Test { function test_getDelegations() public view { address delegator = 0xee448667ffc3D15ca023A6deEf2D0fAf084C0716; - Delegation[] memory delegations = reader.getDelegations( - delegator, - 0, - 10 - ); + Delegation[] memory delegations = reader.getDelegations(delegator, 0, 10); uint256 length = 2; assertEq(delegations.length, length); assertEq(delegations[length - 1].delegatorAddress, delegator); - assertEq( - delegations[length - 1].validatorAddress, - 0x343dA7Ff0446247ca47AA41e2A25c5Bbb230ED0A - ); + assertEq(delegations[length - 1].validatorAddress, 0x343dA7Ff0446247ca47AA41e2A25c5Bbb230ED0A); assertTrue(delegations[length - 1].amount > 0); assertTrue(delegations[length - 1].shares > 0); } function test_getUndelegations() public view { address delegator = 0xee448667ffc3D15ca023A6deEf2D0fAf084C0716; - Undelegation[] memory undelegations = reader.getUndelegations( - delegator, - 0, - 10 - ); + Undelegation[] memory undelegations = reader.getUndelegations(delegator, 0, 10); uint256 length = 1; assertEq(undelegations.length, length); assertEq(undelegations[length - 1].delegatorAddress, delegator); - assertEq( - undelegations[length - 1].validatorAddress, - 0x343dA7Ff0446247ca47AA41e2A25c5Bbb230ED0A - ); + assertEq(undelegations[length - 1].validatorAddress, 0x343dA7Ff0446247ca47AA41e2A25c5Bbb230ED0A); assertTrue(undelegations[length - 1].amount > 0); assertTrue(undelegations[length - 1].shares > 0); assertTrue(undelegations[length - 1].unlockTime > 0); From 96f1c05ea782f2e96290b72aa4c9afa5104b1bbc Mon Sep 17 00:00:00 2001 From: 0xh3rman <119309671+0xh3rman@users.noreply.github.com> Date: Thu, 4 Dec 2025 17:25:07 +0900 Subject: [PATCH 2/4] add completionTimestamp --- src/monad/StakingLens.sol | 26 +++++++++++++++++++++++--- 1 file changed, 23 insertions(+), 3 deletions(-) diff --git a/src/monad/StakingLens.sol b/src/monad/StakingLens.sol index 8b23731..a7cac64 100644 --- a/src/monad/StakingLens.sol +++ b/src/monad/StakingLens.sol @@ -18,6 +18,8 @@ contract StakingLens { uint256 public constant MONAD_BLOCK_REWARD = 25 ether; uint256 public constant MONAD_BLOCKS_PER_YEAR = 78_840_000; uint64 public constant APY_BPS_PRECISION = 10_000; + uint64 public constant MONAD_BOUNDARY_BLOCK_PERIOD = 50_000; + uint64 public constant MONAD_EPOCH_SECONDS = MONAD_BOUNDARY_BLOCK_PERIOD * 2 / 5; // 0.4s block time enum DelegationState { Active, @@ -33,6 +35,7 @@ contract StakingLens { uint256 amount; uint256 rewards; // unclaimed rewards only for Active entries uint64 withdrawEpoch; // populated for withdrawals + uint64 completionTimestamp; } struct DelegatorSnapshot { @@ -152,7 +155,8 @@ contract StakingLens { state: DelegationState.Active, amount: snap.stake, rewards: snap.rewards, - withdrawEpoch: 0 + withdrawEpoch: 0, + completionTimestamp: 0 }); ++positionCount; } @@ -164,7 +168,8 @@ contract StakingLens { state: DelegationState.Activating, amount: snap.pendingStake, rewards: 0, - withdrawEpoch: 0 + withdrawEpoch: 0, + completionTimestamp: 0 }); ++positionCount; } @@ -200,7 +205,10 @@ contract StakingLens { state: withdrawEpoch < currentEpoch ? DelegationState.AwaitingWithdrawal : DelegationState.Deactivating, amount: amount, rewards: 0, - withdrawEpoch: withdrawEpoch + withdrawEpoch: withdrawEpoch, + completionTimestamp: withdrawEpoch < currentEpoch + ? 0 + : _withdrawCompletionTimestamp(withdrawEpoch, currentEpoch) }); ++count; @@ -211,6 +219,18 @@ contract StakingLens { return (count, lastWithdrawId, hasWithdrawals); } + function _withdrawCompletionTimestamp(uint64 withdrawEpoch, uint64 currentEpoch) internal view returns (uint64) { + if (withdrawEpoch < currentEpoch) { + return 0; + } + + uint64 remainingEpochs = withdrawEpoch - currentEpoch + 1; + uint256 completion = block.timestamp + uint256(remainingEpochs) * uint256(MONAD_EPOCH_SECONDS); + // casting to uint64 is safe because completion timestamps are bounded by the type max guard above + // forge-lint: disable-next-line(unsafe-typecast) + return completion > type(uint64).max ? type(uint64).max : uint64(completion); + } + /** * @notice Return validator stats plus APY for a set of validator ids. * @param validatorIds If empty, uses the curated Monad validator list. From 53a8fecf67f42bdc8054a1b932f4ad368757219b Mon Sep 17 00:00:00 2001 From: 0xh3rman <119309671+0xh3rman@users.noreply.github.com> Date: Thu, 4 Dec 2025 18:33:59 +0900 Subject: [PATCH 3/4] Refactor Monad StakingLens and add tests Simplifies StakingLens by removing unused fields, fallback APY logic, and redundant comments. Updates validator and APY query functions to use the full validator set by default. Adds unit tests for StakingLens in test/monad/StakingLens.t.sol and updates documentation and justfile for Monad lens testing. --- README.md | 47 +++++++-------------- justfile | 3 ++ src/monad/StakingLens.sol | 35 ++++------------ test/monad/StakingLens.t.sol | 81 ++++++++++++++++++++++++++++++++++++ 4 files changed, 106 insertions(+), 60 deletions(-) create mode 100644 test/monad/StakingLens.t.sol diff --git a/README.md b/README.md index 6483d13..ccea745 100644 --- a/README.md +++ b/README.md @@ -1,46 +1,27 @@ # Smart Contracts -A collection of smart contracts for Gem Wallet. +Gem Wallet deployment helpers and read lenses. -- [src/hub_reader](src/hub_reader): A contract that simplify interacting with BSC Staking Hub -- [src/stargate](src/stargate): A contract that allow to do onchain calls on destination chain after Stargate Bridge +- `src/hub_reader`: BSC staking hub reader. +- `src/stargate`: post-bridge call handler for Stargate V2. +- `src/monad`: staking lens for Monad (precompile reader). ## Development -1. Install [Foundry](https://book.getfoundry.sh/) and you're good to go. -2. Configure `.env` using `.env.example` rpcs (if needed) and etherscan values, if you need to deploy the contract, you need to set `PRIVATE_KEY` as well. +1) Install [Foundry](https://book.getfoundry.sh/). +2) Copy `.env.example` to `.env` and fill RPCs (including `MONAD_RPC_URL`), scan keys, and `PRIVATE_KEY` for deploys. -## Usage +## Common Tasks -### Build +- Build: `forge build` +- Lint/format: `forge lint && forge fmt` +- Test: `forge test` (HubReader tests expect a live BSC RPC; the Monad lens tests are mocked) -```shell -forge build -``` - -### Test - -```shell -forge test --rpc-url -``` - -### Deploy - -```shell -# deploy hub_reader -just deploy-hub-reader -``` - -```shell -# deploy stargate to all supported chains -just deploy-stargate -``` - -```shell -# deploy stargate to specific chain -just deploy-stargate optimism -``` +## Deploy +- Hub Reader (BSC): `just deploy-hub-reader` +- Stargate fee receiver: `just deploy-stargate optimism` (or another supported chain) +- Monad staking lens: `just deploy-monad-staking` diff --git a/justfile b/justfile index 7659db9..81c0f3a 100644 --- a/justfile +++ b/justfile @@ -9,6 +9,9 @@ build: test: forge test +test-monad: + forge test --match-path test/monad/* + deploy-stargate CHAIN_NAME: bash ./deploy/deploy-stargate.sh {{CHAIN_NAME}} diff --git a/src/monad/StakingLens.sol b/src/monad/StakingLens.sol index a7cac64..c006045 100644 --- a/src/monad/StakingLens.sol +++ b/src/monad/StakingLens.sol @@ -19,7 +19,7 @@ contract StakingLens { uint256 public constant MONAD_BLOCKS_PER_YEAR = 78_840_000; uint64 public constant APY_BPS_PRECISION = 10_000; uint64 public constant MONAD_BOUNDARY_BLOCK_PERIOD = 50_000; - uint64 public constant MONAD_EPOCH_SECONDS = MONAD_BOUNDARY_BLOCK_PERIOD * 2 / 5; // 0.4s block time + uint64 public constant MONAD_EPOCH_SECONDS = MONAD_BOUNDARY_BLOCK_PERIOD * 2 / 5; // 0.4s blocks enum DelegationState { Active, @@ -33,8 +33,8 @@ contract StakingLens { uint8 withdrawId; DelegationState state; uint256 amount; - uint256 rewards; // unclaimed rewards only for Active entries - uint64 withdrawEpoch; // populated for withdrawals + uint256 rewards; + uint64 withdrawEpoch; uint64 completionTimestamp; } @@ -46,7 +46,6 @@ contract StakingLens { struct ValidatorInfo { uint64 validatorId; - string name; uint256 stake; uint256 commission; uint64 apyBps; @@ -60,10 +59,6 @@ contract StakingLens { uint256 commission; } - /** - * @notice Sum staked, pending, and reward balances for a delegator. - * @dev Same data the Rust provider assembles for staking_monad::get_monad_staking_balance. - */ function getBalance(address delegator) external returns (uint256 staked, uint256 pending, uint256 rewards) { bool isDone; uint64 nextValId; @@ -91,12 +86,6 @@ contract StakingLens { } } - /** - * @notice Get all delegation positions for a delegator, including withdrawals. - * @dev Emits one entry per state (active, activating, withdrawal requests). - * Uses withdrawId as the tie-breaker so clients can build delegation_ids - * compatible with the Rust client (address:withdrawId). - */ function getDelegations(address delegator) external returns (DelegationPosition[] memory positions) { positions = new DelegationPosition[](MAX_POSITIONS); uint256 positionCount = 0; @@ -233,10 +222,9 @@ contract StakingLens { /** * @notice Return validator stats plus APY for a set of validator ids. - * @param validatorIds If empty, uses the curated Monad validator list. - * @param fallbackApyBps Optional fallback APY (basis points) if the network or validator math returns zero. + * @param validatorIds If empty, uses the full Monad validator set. */ - function getValidators(uint64[] calldata validatorIds, uint64 fallbackApyBps) + function getValidators(uint64[] calldata validatorIds) external returns (ValidatorInfo[] memory validators, uint64 networkApyBps) { @@ -254,13 +242,9 @@ contract StakingLens { } uint64 validatorApy = _validatorApyBps(snapshot.stake, totalStake, snapshot.commission, networkApyBps); - if (validatorApy == 0) { - validatorApy = fallbackApyBps; - } validators[i] = ValidatorInfo({ validatorId: snapshot.validatorId, - name: uint256(snapshot.validatorId).toString(), stake: snapshot.stake, commission: snapshot.commission, apyBps: validatorApy, @@ -270,12 +254,10 @@ contract StakingLens { } /** - * @notice Return APYs for a set of validator ids, mirroring HubReader.getAPYs style. - * @param validatorIds If empty, uses the curated Monad validator list. - * @param fallbackApyBps Optional fallback APY (basis points) if the network or validator math returns zero. + * @notice Return APYs for a set of validator ids. Defaults to the full validator set when empty. */ // forge-lint: disable-next-line(mixed-case-function) - function getAPYs(uint64[] calldata validatorIds, uint64 fallbackApyBps) external returns (uint64[] memory apysBps) { + function getAPYs(uint64[] calldata validatorIds) external returns (uint64[] memory apysBps) { uint64[] memory allValidatorIds = _allValidatorIds(); uint64[] memory targetIds = validatorIds.length == 0 ? allValidatorIds : validatorIds; @@ -286,12 +268,11 @@ contract StakingLens { for (uint256 i = 0; i < targetIds.length; ++i) { (ValidatorData memory snapshot, bool found) = _findValidator(data, targetIds[i]); if (!found) { - apysBps[i] = fallbackApyBps; continue; } uint64 validatorApy = _validatorApyBps(snapshot.stake, totalStake, snapshot.commission, networkApyBps); - apysBps[i] = validatorApy > 0 ? validatorApy : fallbackApyBps; + apysBps[i] = validatorApy; } } diff --git a/test/monad/StakingLens.t.sol b/test/monad/StakingLens.t.sol new file mode 100644 index 0000000..89f432d --- /dev/null +++ b/test/monad/StakingLens.t.sol @@ -0,0 +1,81 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.15; + +import {Test} from "forge-std/Test.sol"; +import {StakingLens} from "../../src/monad/StakingLens.sol"; +import {IStaking} from "../../src/monad/IStaking.sol"; + +contract StakingLensTest is Test { + StakingLens private lens; + address private constant STAKING_PRECOMPILE = address(0x0000000000000000000000000000000000001000); + + uint64[] private validatorIds; + uint256 private constant TOTAL_STAKE = 1e30; + uint256 private constant VALIDATOR_STAKE = TOTAL_STAKE / 2; + + function setUp() public { + lens = new StakingLens(); + + validatorIds = new uint64[](2); + validatorIds[0] = 1; + validatorIds[1] = 2; + + _mockConsensusSet(); + _mockValidator(validatorIds[0], VALIDATOR_STAKE, 0); + _mockValidator(validatorIds[1], VALIDATOR_STAKE, 0); + } + + function test_getAPYsUsesAllValidatorsWhenEmpty() public { + uint64[] memory apys = lens.getAPYs(new uint64[](0)); + + uint64 expected = _expectedNetworkApy(); + assertEq(apys.length, validatorIds.length); + assertEq(apys[0], expected); + assertEq(apys[1], expected); + } + + function test_getValidatorsReturnsNetworkApy() public { + (StakingLens.ValidatorInfo[] memory validators, uint64 networkApy) = lens.getValidators(new uint64[](0)); + + uint64 expected = _expectedNetworkApy(); + assertEq(networkApy, expected); + assertEq(validators.length, validatorIds.length); + assertEq(validators[0].validatorId, validatorIds[0]); + assertEq(validators[0].apyBps, expected); + assertEq(validators[1].validatorId, validatorIds[1]); + assertEq(validators[1].apyBps, expected); + } + + function _mockConsensusSet() internal { + bytes memory data = abi.encodeCall(IStaking.getConsensusValidatorSet, (0)); + bytes memory result = abi.encode(true, uint32(0), validatorIds); + vm.mockCall(STAKING_PRECOMPILE, data, result); + } + + function _mockValidator(uint64 validatorId, uint256 stake, uint256 commission) internal { + bytes memory data = abi.encodeCall(IStaking.getValidator, (validatorId)); + bytes memory result = abi.encode( + address(0), + uint64(0), + stake, + uint256(0), + commission, + uint256(0), + uint256(0), + uint256(0), + uint256(0), + uint256(0), + bytes(""), + bytes("") + ); + vm.mockCall(STAKING_PRECOMPILE, data, result); + } + + function _expectedNetworkApy() internal view returns (uint64) { + uint256 annualRewards = lens.MONAD_BLOCK_REWARD() * lens.MONAD_BLOCKS_PER_YEAR(); + uint256 apy = (annualRewards * lens.APY_BPS_PRECISION()) / TOTAL_STAKE; + // casting to uint64 is safe because APY basis points are intentionally capped within uint64 range + // forge-lint: disable-next-line(unsafe-typecast) + return uint64(apy); + } +} From ac708e742d88530cb7944e36f5aa4c6fb05b093b Mon Sep 17 00:00:00 2001 From: 0xh3rman <119309671+0xh3rman@users.noreply.github.com> Date: Thu, 4 Dec 2025 18:48:33 +0900 Subject: [PATCH 4/4] rename Delegation --- src/monad/StakingLens.sol | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/monad/StakingLens.sol b/src/monad/StakingLens.sol index c006045..09ecd0e 100644 --- a/src/monad/StakingLens.sol +++ b/src/monad/StakingLens.sol @@ -28,7 +28,7 @@ contract StakingLens { AwaitingWithdrawal } - struct DelegationPosition { + struct Delegation { uint64 validatorId; uint8 withdrawId; DelegationState state; @@ -86,8 +86,8 @@ contract StakingLens { } } - function getDelegations(address delegator) external returns (DelegationPosition[] memory positions) { - positions = new DelegationPosition[](MAX_POSITIONS); + function getDelegations(address delegator) external returns (Delegation[] memory positions) { + positions = new Delegation[](MAX_POSITIONS); uint256 positionCount = 0; uint16 validatorCount = 0; @@ -124,7 +124,7 @@ contract StakingLens { address delegator, uint64 validatorId, uint64 currentEpoch, - DelegationPosition[] memory positions, + Delegation[] memory positions, uint256 positionCount ) internal returns (uint256 newPositionCount) { DelegatorSnapshot memory snap = _readDelegator(delegator, validatorId); @@ -138,7 +138,7 @@ contract StakingLens { } if (snap.stake > 0 && positionCount < MAX_POSITIONS) { - positions[positionCount] = DelegationPosition({ + positions[positionCount] = Delegation({ validatorId: validatorId, withdrawId: lastWithdrawId, state: DelegationState.Active, @@ -151,7 +151,7 @@ contract StakingLens { } if (snap.pendingStake > 0 && positionCount < MAX_POSITIONS) { - positions[positionCount] = DelegationPosition({ + positions[positionCount] = Delegation({ validatorId: validatorId, withdrawId: lastWithdrawId, state: DelegationState.Activating, @@ -177,7 +177,7 @@ contract StakingLens { address delegator, uint64 validatorId, uint64 currentEpoch, - DelegationPosition[] memory positions, + Delegation[] memory positions, uint256 positionCount ) internal returns (uint256 newPositionCount, uint8 lastWithdrawId, bool hasWithdrawals) { uint256 count = positionCount; @@ -188,7 +188,7 @@ contract StakingLens { continue; } - positions[count] = DelegationPosition({ + positions[count] = Delegation({ validatorId: validatorId, withdrawId: withdrawId, state: withdrawEpoch < currentEpoch ? DelegationState.AwaitingWithdrawal : DelegationState.Deactivating,