Skip to content

Commit 53a8fec

Browse files
committed
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.
1 parent 96f1c05 commit 53a8fec

4 files changed

Lines changed: 106 additions & 60 deletions

File tree

README.md

Lines changed: 14 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,46 +1,27 @@
11
# Smart Contracts
22

3-
A collection of smart contracts for Gem Wallet.
3+
Gem Wallet deployment helpers and read lenses.
44

5-
- [src/hub_reader](src/hub_reader): A contract that simplify interacting with BSC Staking Hub
6-
- [src/stargate](src/stargate): A contract that allow to do onchain calls on destination chain after Stargate Bridge
5+
- `src/hub_reader`: BSC staking hub reader.
6+
- `src/stargate`: post-bridge call handler for Stargate V2.
7+
- `src/monad`: staking lens for Monad (precompile reader).
78

89
## Development
910

10-
1. Install [Foundry](https://book.getfoundry.sh/) and you're good to go.
11-
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.
11+
1) Install [Foundry](https://book.getfoundry.sh/).
12+
2) Copy `.env.example` to `.env` and fill RPCs (including `MONAD_RPC_URL`), scan keys, and `PRIVATE_KEY` for deploys.
1213

13-
## Usage
14+
## Common Tasks
1415

15-
### Build
16+
- Build: `forge build`
17+
- Lint/format: `forge lint && forge fmt`
18+
- Test: `forge test` (HubReader tests expect a live BSC RPC; the Monad lens tests are mocked)
1619

17-
```shell
18-
forge build
19-
```
20-
21-
### Test
22-
23-
```shell
24-
forge test --rpc-url <your_rpc_url>
25-
```
26-
27-
### Deploy
28-
29-
```shell
30-
# deploy hub_reader
31-
just deploy-hub-reader
32-
```
33-
34-
```shell
35-
# deploy stargate to all supported chains
36-
just deploy-stargate
37-
```
38-
39-
```shell
40-
# deploy stargate to specific chain
41-
just deploy-stargate optimism
42-
```
20+
## Deploy
4321

22+
- Hub Reader (BSC): `just deploy-hub-reader`
23+
- Stargate fee receiver: `just deploy-stargate optimism` (or another supported chain)
24+
- Monad staking lens: `just deploy-monad-staking`
4425

4526

4627

justfile

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@ build:
99
test:
1010
forge test
1111

12+
test-monad:
13+
forge test --match-path test/monad/*
14+
1215
deploy-stargate CHAIN_NAME:
1316
bash ./deploy/deploy-stargate.sh {{CHAIN_NAME}}
1417

src/monad/StakingLens.sol

Lines changed: 8 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ contract StakingLens {
1919
uint256 public constant MONAD_BLOCKS_PER_YEAR = 78_840_000;
2020
uint64 public constant APY_BPS_PRECISION = 10_000;
2121
uint64 public constant MONAD_BOUNDARY_BLOCK_PERIOD = 50_000;
22-
uint64 public constant MONAD_EPOCH_SECONDS = MONAD_BOUNDARY_BLOCK_PERIOD * 2 / 5; // 0.4s block time
22+
uint64 public constant MONAD_EPOCH_SECONDS = MONAD_BOUNDARY_BLOCK_PERIOD * 2 / 5; // 0.4s blocks
2323

2424
enum DelegationState {
2525
Active,
@@ -33,8 +33,8 @@ contract StakingLens {
3333
uint8 withdrawId;
3434
DelegationState state;
3535
uint256 amount;
36-
uint256 rewards; // unclaimed rewards only for Active entries
37-
uint64 withdrawEpoch; // populated for withdrawals
36+
uint256 rewards;
37+
uint64 withdrawEpoch;
3838
uint64 completionTimestamp;
3939
}
4040

@@ -46,7 +46,6 @@ contract StakingLens {
4646

4747
struct ValidatorInfo {
4848
uint64 validatorId;
49-
string name;
5049
uint256 stake;
5150
uint256 commission;
5251
uint64 apyBps;
@@ -60,10 +59,6 @@ contract StakingLens {
6059
uint256 commission;
6160
}
6261

63-
/**
64-
* @notice Sum staked, pending, and reward balances for a delegator.
65-
* @dev Same data the Rust provider assembles for staking_monad::get_monad_staking_balance.
66-
*/
6762
function getBalance(address delegator) external returns (uint256 staked, uint256 pending, uint256 rewards) {
6863
bool isDone;
6964
uint64 nextValId;
@@ -91,12 +86,6 @@ contract StakingLens {
9186
}
9287
}
9388

94-
/**
95-
* @notice Get all delegation positions for a delegator, including withdrawals.
96-
* @dev Emits one entry per state (active, activating, withdrawal requests).
97-
* Uses withdrawId as the tie-breaker so clients can build delegation_ids
98-
* compatible with the Rust client (address:withdrawId).
99-
*/
10089
function getDelegations(address delegator) external returns (DelegationPosition[] memory positions) {
10190
positions = new DelegationPosition[](MAX_POSITIONS);
10291
uint256 positionCount = 0;
@@ -233,10 +222,9 @@ contract StakingLens {
233222

234223
/**
235224
* @notice Return validator stats plus APY for a set of validator ids.
236-
* @param validatorIds If empty, uses the curated Monad validator list.
237-
* @param fallbackApyBps Optional fallback APY (basis points) if the network or validator math returns zero.
225+
* @param validatorIds If empty, uses the full Monad validator set.
238226
*/
239-
function getValidators(uint64[] calldata validatorIds, uint64 fallbackApyBps)
227+
function getValidators(uint64[] calldata validatorIds)
240228
external
241229
returns (ValidatorInfo[] memory validators, uint64 networkApyBps)
242230
{
@@ -254,13 +242,9 @@ contract StakingLens {
254242
}
255243

256244
uint64 validatorApy = _validatorApyBps(snapshot.stake, totalStake, snapshot.commission, networkApyBps);
257-
if (validatorApy == 0) {
258-
validatorApy = fallbackApyBps;
259-
}
260245

261246
validators[i] = ValidatorInfo({
262247
validatorId: snapshot.validatorId,
263-
name: uint256(snapshot.validatorId).toString(),
264248
stake: snapshot.stake,
265249
commission: snapshot.commission,
266250
apyBps: validatorApy,
@@ -270,12 +254,10 @@ contract StakingLens {
270254
}
271255

272256
/**
273-
* @notice Return APYs for a set of validator ids, mirroring HubReader.getAPYs style.
274-
* @param validatorIds If empty, uses the curated Monad validator list.
275-
* @param fallbackApyBps Optional fallback APY (basis points) if the network or validator math returns zero.
257+
* @notice Return APYs for a set of validator ids. Defaults to the full validator set when empty.
276258
*/
277259
// forge-lint: disable-next-line(mixed-case-function)
278-
function getAPYs(uint64[] calldata validatorIds, uint64 fallbackApyBps) external returns (uint64[] memory apysBps) {
260+
function getAPYs(uint64[] calldata validatorIds) external returns (uint64[] memory apysBps) {
279261
uint64[] memory allValidatorIds = _allValidatorIds();
280262
uint64[] memory targetIds = validatorIds.length == 0 ? allValidatorIds : validatorIds;
281263

@@ -286,12 +268,11 @@ contract StakingLens {
286268
for (uint256 i = 0; i < targetIds.length; ++i) {
287269
(ValidatorData memory snapshot, bool found) = _findValidator(data, targetIds[i]);
288270
if (!found) {
289-
apysBps[i] = fallbackApyBps;
290271
continue;
291272
}
292273

293274
uint64 validatorApy = _validatorApyBps(snapshot.stake, totalStake, snapshot.commission, networkApyBps);
294-
apysBps[i] = validatorApy > 0 ? validatorApy : fallbackApyBps;
275+
apysBps[i] = validatorApy;
295276
}
296277
}
297278

test/monad/StakingLens.t.sol

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
// SPDX-License-Identifier: MIT
2+
pragma solidity ^0.8.15;
3+
4+
import {Test} from "forge-std/Test.sol";
5+
import {StakingLens} from "../../src/monad/StakingLens.sol";
6+
import {IStaking} from "../../src/monad/IStaking.sol";
7+
8+
contract StakingLensTest is Test {
9+
StakingLens private lens;
10+
address private constant STAKING_PRECOMPILE = address(0x0000000000000000000000000000000000001000);
11+
12+
uint64[] private validatorIds;
13+
uint256 private constant TOTAL_STAKE = 1e30;
14+
uint256 private constant VALIDATOR_STAKE = TOTAL_STAKE / 2;
15+
16+
function setUp() public {
17+
lens = new StakingLens();
18+
19+
validatorIds = new uint64[](2);
20+
validatorIds[0] = 1;
21+
validatorIds[1] = 2;
22+
23+
_mockConsensusSet();
24+
_mockValidator(validatorIds[0], VALIDATOR_STAKE, 0);
25+
_mockValidator(validatorIds[1], VALIDATOR_STAKE, 0);
26+
}
27+
28+
function test_getAPYsUsesAllValidatorsWhenEmpty() public {
29+
uint64[] memory apys = lens.getAPYs(new uint64[](0));
30+
31+
uint64 expected = _expectedNetworkApy();
32+
assertEq(apys.length, validatorIds.length);
33+
assertEq(apys[0], expected);
34+
assertEq(apys[1], expected);
35+
}
36+
37+
function test_getValidatorsReturnsNetworkApy() public {
38+
(StakingLens.ValidatorInfo[] memory validators, uint64 networkApy) = lens.getValidators(new uint64[](0));
39+
40+
uint64 expected = _expectedNetworkApy();
41+
assertEq(networkApy, expected);
42+
assertEq(validators.length, validatorIds.length);
43+
assertEq(validators[0].validatorId, validatorIds[0]);
44+
assertEq(validators[0].apyBps, expected);
45+
assertEq(validators[1].validatorId, validatorIds[1]);
46+
assertEq(validators[1].apyBps, expected);
47+
}
48+
49+
function _mockConsensusSet() internal {
50+
bytes memory data = abi.encodeCall(IStaking.getConsensusValidatorSet, (0));
51+
bytes memory result = abi.encode(true, uint32(0), validatorIds);
52+
vm.mockCall(STAKING_PRECOMPILE, data, result);
53+
}
54+
55+
function _mockValidator(uint64 validatorId, uint256 stake, uint256 commission) internal {
56+
bytes memory data = abi.encodeCall(IStaking.getValidator, (validatorId));
57+
bytes memory result = abi.encode(
58+
address(0),
59+
uint64(0),
60+
stake,
61+
uint256(0),
62+
commission,
63+
uint256(0),
64+
uint256(0),
65+
uint256(0),
66+
uint256(0),
67+
uint256(0),
68+
bytes(""),
69+
bytes("")
70+
);
71+
vm.mockCall(STAKING_PRECOMPILE, data, result);
72+
}
73+
74+
function _expectedNetworkApy() internal view returns (uint64) {
75+
uint256 annualRewards = lens.MONAD_BLOCK_REWARD() * lens.MONAD_BLOCKS_PER_YEAR();
76+
uint256 apy = (annualRewards * lens.APY_BPS_PRECISION()) / TOTAL_STAKE;
77+
// casting to uint64 is safe because APY basis points are intentionally capped within uint64 range
78+
// forge-lint: disable-next-line(unsafe-typecast)
79+
return uint64(apy);
80+
}
81+
}

0 commit comments

Comments
 (0)