Skip to content

Commit 0f59bd1

Browse files
committed
Include positions without active delegations
StakingLens: When a delegator has no explicit delegations and the result set is not full, iterate all validators (_allValidatorIds) and process them so positions that only have withdrawals or rewards are included. Adjust snapshot checks to treat snapshots with rewards as non-empty and include reward-only delegations when building positions. Tests: Add test_getDelegationsIncludesPositionsWithoutActiveDelegations and supporting mock helpers (_mockConsensusSet overload, _mockEpoch, _mockDelegations, _mockDelegator, _mockWithdrawalRequest) to verify positions are returned for validators with only withdrawals or rewards.
1 parent 49e2c15 commit 0f59bd1

3 files changed

Lines changed: 198 additions & 12 deletions

File tree

justfile

Lines changed: 6 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -28,26 +28,22 @@ read-staking-lens-address BROADCAST_FILE="broadcast/StakingLens.s.sol/143/run-la
2828
#!/usr/bin/env bash
2929
jq -r '.receipts[]?.contractAddress // empty' "{{BROADCAST_FILE}}"
3030

31-
verify-monad-staking ADDRESS="": build-monad
32-
ADDRESS_TO_VERIFY=${STAKING_LENS_ADDRESS:-${ADDRESS-}}
33-
[ -n "${ADDRESS_TO_VERIFY-}" ] || { echo "Set STAKING_LENS_ADDRESS (hint: STAKING_LENS_ADDRESS=$(just read-staking-lens-address))" >&2; exit 1; }
31+
verify-monad-staking ADDRESS: build-monad
3432
forge verify-contract \
3533
--rpc-url https://rpc.monad.xyz \
3634
--verifier sourcify \
3735
--verifier-url 'https://sourcify-api-monad.blockvision.org/' \
3836
--chain-id 143 \
39-
"$ADDRESS_TO_VERIFY" \
37+
"{{ADDRESS}}" \
4038
src/monad/StakingLens.sol:StakingLens
4139

42-
verify-monad-staking-etherscan ADDRESS="": build-monad
43-
ADDRESS_TO_VERIFY=${STAKING_LENS_ADDRESS:-${ADDRESS-}}
44-
[ -n "${ADDRESS_TO_VERIFY-}" ] || { echo "Set STAKING_LENS_ADDRESS (hint: STAKING_LENS_ADDRESS=$(just read-staking-lens-address))" >&2; exit 1; }
40+
verify-monad-staking-etherscan ADDRESS: build-monad
4541
[ -n "${ETHERSCAN_API_KEY-}" ] || { echo "ETHERSCAN_API_KEY is required" >&2; exit 1; }
4642
forge verify-contract \
47-
--verifier etherscan \
43+
--verifier custom \
4844
--verifier-url 'https://api.etherscan.io/v2/api?chainid=143' \
45+
--verifier-api-key "$ETHERSCAN_API_KEY" \
4946
--chain 143 \
5047
--rpc-url https://rpc.monad.xyz \
51-
--etherscan-api-key "$ETHERSCAN_API_KEY" \
52-
"$ADDRESS_TO_VERIFY" \
48+
"{{ADDRESS}}" \
5349
src/monad/StakingLens.sol:StakingLens

src/monad/StakingLens.sol

Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,8 @@ contract StakingLens {
9090
positions = new Delegation[](MAX_POSITIONS);
9191
uint256 positionCount = 0;
9292
uint16 validatorCount = 0;
93+
uint64[] memory processedValidatorIds = new uint64[](uint256(MAX_DELEGATIONS));
94+
uint256 processedValidatorCount = 0;
9395

9496
(uint64 currentEpoch,) = STAKING.getEpoch();
9597

@@ -104,7 +106,13 @@ contract StakingLens {
104106

105107
for (uint256 i = 0; i < len && validatorCount < MAX_DELEGATIONS; ++i) {
106108
uint64 validatorId = valIds[i];
109+
if (_containsValidator(processedValidatorIds, processedValidatorCount, validatorId)) {
110+
continue;
111+
}
112+
107113
positionCount = _processValidator(delegator, validatorId, currentEpoch, positions, positionCount);
114+
processedValidatorIds[processedValidatorCount] = validatorId;
115+
++processedValidatorCount;
108116
++validatorCount;
109117
}
110118

@@ -115,11 +123,41 @@ contract StakingLens {
115123
(isDone, nextValId, valIds) = STAKING.getDelegations(delegator, nextValId);
116124
}
117125

126+
if (validatorCount < MAX_DELEGATIONS && positionCount < MAX_POSITIONS) {
127+
uint64[] memory allValidatorIds = _allValidatorIds();
128+
uint256 len = allValidatorIds.length;
129+
for (uint256 i = 0; i < len && validatorCount < MAX_DELEGATIONS && positionCount < MAX_POSITIONS; ++i) {
130+
uint64 validatorId = allValidatorIds[i];
131+
if (_containsValidator(processedValidatorIds, processedValidatorCount, validatorId)) {
132+
continue;
133+
}
134+
135+
positionCount = _processValidator(delegator, validatorId, currentEpoch, positions, positionCount);
136+
processedValidatorIds[processedValidatorCount] = validatorId;
137+
++processedValidatorCount;
138+
++validatorCount;
139+
}
140+
}
141+
118142
assembly {
119143
mstore(positions, positionCount)
120144
}
121145
}
122146

147+
function _containsValidator(uint64[] memory validatorIds, uint256 count, uint64 validatorId)
148+
internal
149+
pure
150+
returns (bool)
151+
{
152+
for (uint256 i = 0; i < count; ++i) {
153+
if (validatorIds[i] == validatorId) {
154+
return true;
155+
}
156+
}
157+
158+
return false;
159+
}
160+
123161
function _processValidator(
124162
address delegator,
125163
uint64 validatorId,
@@ -133,11 +171,11 @@ contract StakingLens {
133171
(positionCount, lastWithdrawId, hasWithdrawals) =
134172
_appendWithdrawals(delegator, validatorId, currentEpoch, positions, positionCount);
135173

136-
if (snap.stake == 0 && snap.pendingStake == 0 && !hasWithdrawals) {
174+
if (snap.stake == 0 && snap.pendingStake == 0 && snap.rewards == 0 && !hasWithdrawals) {
137175
return positionCount;
138176
}
139177

140-
if (snap.stake > 0 && positionCount < MAX_POSITIONS) {
178+
if ((snap.stake > 0 || snap.rewards > 0) && positionCount < MAX_POSITIONS) {
141179
positions[positionCount] = Delegation({
142180
validatorId: validatorId,
143181
withdrawId: lastWithdrawId,

test/monad/StakingLens.t.sol

Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,12 +46,127 @@ contract StakingLensTest is Test {
4646
assertEq(validators[1].apyBps, expected);
4747
}
4848

49+
function test_getDelegationsIncludesPositionsWithoutActiveDelegations() public {
50+
address delegator = address(0xc08A759F868Ab179F1259b2A7b1B81b0B968710E);
51+
uint64 withdrawValidatorId = 7;
52+
uint64 rewardsValidatorId = 9;
53+
uint64 currentEpoch = 3;
54+
uint64 withdrawEpoch = 4;
55+
uint256 withdrawAmount = 2 ether;
56+
uint256 rewardAmount = 1 ether;
57+
58+
uint64[] memory validators = new uint64[](2);
59+
validators[0] = withdrawValidatorId;
60+
validators[1] = rewardsValidatorId;
61+
_mockConsensusSet(validators);
62+
63+
_mockEpoch(currentEpoch);
64+
_mockDelegations(delegator, new uint64[](0));
65+
_mockDelegator(delegator, withdrawValidatorId, 0, 0, 0, 0);
66+
_mockDelegator(delegator, rewardsValidatorId, 0, rewardAmount, 0, 0);
67+
68+
_mockWithdrawalRequest(withdrawValidatorId, delegator, 0, withdrawAmount, withdrawEpoch);
69+
for (uint8 withdrawId = 1; withdrawId < lens.MAX_WITHDRAW_IDS(); ++withdrawId) {
70+
_mockWithdrawalRequest(withdrawValidatorId, delegator, withdrawId, 0, 0);
71+
}
72+
for (uint8 withdrawId = 0; withdrawId < lens.MAX_WITHDRAW_IDS(); ++withdrawId) {
73+
_mockWithdrawalRequest(rewardsValidatorId, delegator, withdrawId, 0, 0);
74+
}
75+
76+
StakingLens.Delegation[] memory positions = lens.getDelegations(delegator);
77+
78+
assertEq(positions.length, 2);
79+
80+
assertEq(positions[0].validatorId, withdrawValidatorId);
81+
assertEq(positions[0].withdrawId, 0);
82+
assertEq(uint8(positions[0].state), uint8(StakingLens.DelegationState.Deactivating));
83+
assertEq(positions[0].amount, withdrawAmount);
84+
assertEq(positions[0].rewards, 0);
85+
assertEq(positions[0].withdrawEpoch, withdrawEpoch);
86+
assertGt(positions[0].completionTimestamp, 0);
87+
88+
assertEq(positions[1].validatorId, rewardsValidatorId);
89+
assertEq(uint8(positions[1].state), uint8(StakingLens.DelegationState.Active));
90+
assertEq(positions[1].amount, 0);
91+
assertEq(positions[1].rewards, rewardAmount);
92+
assertEq(positions[1].withdrawEpoch, 0);
93+
assertEq(positions[1].completionTimestamp, 0);
94+
}
95+
96+
function test_getDelegationsIncludesWithdrawalsWhenActiveDelegationsExist() public {
97+
address delegator = address(0xc08A759F868Ab179F1259b2A7b1B81b0B968710E);
98+
uint64 withdrawValidatorId = 7;
99+
uint64 activeValidatorId = 9;
100+
uint64 currentEpoch = 3;
101+
uint64 withdrawEpoch = 4;
102+
uint256 withdrawAmount = 2 ether;
103+
uint256 activeStake = 5 ether;
104+
105+
uint64[] memory validators = new uint64[](2);
106+
validators[0] = withdrawValidatorId;
107+
validators[1] = activeValidatorId;
108+
_mockConsensusSet(validators);
109+
110+
uint64[] memory activeDelegations = new uint64[](1);
111+
activeDelegations[0] = activeValidatorId;
112+
113+
_mockEpoch(currentEpoch);
114+
_mockDelegations(delegator, activeDelegations);
115+
_mockDelegator(delegator, activeValidatorId, activeStake, 0, 0, 0);
116+
_mockDelegator(delegator, withdrawValidatorId, 0, 0, 0, 0);
117+
118+
for (uint8 withdrawId = 0; withdrawId < lens.MAX_WITHDRAW_IDS(); ++withdrawId) {
119+
_mockWithdrawalRequest(activeValidatorId, delegator, withdrawId, 0, 0);
120+
}
121+
_mockWithdrawalRequest(withdrawValidatorId, delegator, 1, withdrawAmount, withdrawEpoch);
122+
for (uint8 withdrawId = 0; withdrawId < lens.MAX_WITHDRAW_IDS(); ++withdrawId) {
123+
if (withdrawId != 1) {
124+
_mockWithdrawalRequest(withdrawValidatorId, delegator, withdrawId, 0, 0);
125+
}
126+
}
127+
128+
StakingLens.Delegation[] memory positions = lens.getDelegations(delegator);
129+
130+
assertEq(positions.length, 2);
131+
132+
bool foundActive;
133+
bool foundWithdraw;
134+
for (uint256 i = 0; i < positions.length; ++i) {
135+
StakingLens.Delegation memory position = positions[i];
136+
137+
if (position.validatorId == activeValidatorId && position.state == StakingLens.DelegationState.Active) {
138+
foundActive = true;
139+
assertEq(position.amount, activeStake);
140+
assertEq(position.rewards, 0);
141+
}
142+
143+
if (
144+
position.validatorId == withdrawValidatorId
145+
&& position.state == StakingLens.DelegationState.Deactivating && position.withdrawId == 1
146+
) {
147+
foundWithdraw = true;
148+
assertEq(position.amount, withdrawAmount);
149+
assertEq(position.rewards, 0);
150+
assertEq(position.withdrawEpoch, withdrawEpoch);
151+
}
152+
}
153+
154+
assertTrue(foundActive);
155+
assertTrue(foundWithdraw);
156+
}
157+
49158
function _mockConsensusSet() internal {
50159
bytes memory data = abi.encodeCall(IStaking.getConsensusValidatorSet, (0));
51160
bytes memory result = abi.encode(true, uint32(0), validatorIds);
52161
vm.mockCall(STAKING_PRECOMPILE, data, result);
53162
}
54163

164+
function _mockConsensusSet(uint64[] memory ids) internal {
165+
bytes memory data = abi.encodeCall(IStaking.getConsensusValidatorSet, (0));
166+
bytes memory result = abi.encode(true, uint32(0), ids);
167+
vm.mockCall(STAKING_PRECOMPILE, data, result);
168+
}
169+
55170
function _mockValidator(uint64 validatorId, uint256 stake, uint256 commission) internal {
56171
bytes memory data = abi.encodeCall(IStaking.getValidator, (validatorId));
57172
bytes memory result = abi.encode(
@@ -71,6 +186,43 @@ contract StakingLensTest is Test {
71186
vm.mockCall(STAKING_PRECOMPILE, data, result);
72187
}
73188

189+
function _mockEpoch(uint64 epoch) internal {
190+
bytes memory data = abi.encodeCall(IStaking.getEpoch, ());
191+
bytes memory result = abi.encode(epoch, false);
192+
vm.mockCall(STAKING_PRECOMPILE, data, result);
193+
}
194+
195+
function _mockDelegations(address delegator, uint64[] memory valIds) internal {
196+
bytes memory data = abi.encodeCall(IStaking.getDelegations, (delegator, uint64(0)));
197+
bytes memory result = abi.encode(true, uint64(0), valIds);
198+
vm.mockCall(STAKING_PRECOMPILE, data, result);
199+
}
200+
201+
function _mockDelegator(
202+
address delegator,
203+
uint64 validatorId,
204+
uint256 stake,
205+
uint256 rewards,
206+
uint256 deltaStake,
207+
uint256 nextDeltaStake
208+
) internal {
209+
bytes memory data = abi.encodeCall(IStaking.getDelegator, (validatorId, delegator));
210+
bytes memory result = abi.encode(stake, uint256(0), rewards, deltaStake, nextDeltaStake, uint64(0), uint64(0));
211+
vm.mockCall(STAKING_PRECOMPILE, data, result);
212+
}
213+
214+
function _mockWithdrawalRequest(
215+
uint64 validatorId,
216+
address delegator,
217+
uint8 withdrawId,
218+
uint256 amount,
219+
uint64 withdrawEpoch
220+
) internal {
221+
bytes memory data = abi.encodeCall(IStaking.getWithdrawalRequest, (validatorId, delegator, withdrawId));
222+
bytes memory result = abi.encode(amount, uint256(0), withdrawEpoch);
223+
vm.mockCall(STAKING_PRECOMPILE, data, result);
224+
}
225+
74226
function _expectedNetworkApy() internal view returns (uint64) {
75227
uint256 annualRewards = lens.MONAD_BLOCK_REWARD() * lens.MONAD_BLOCKS_PER_YEAR();
76228
uint256 apy = (annualRewards * lens.APY_BPS_PRECISION()) / TOTAL_STAKE;

0 commit comments

Comments
 (0)