Skip to content

Commit 625dc52

Browse files
authored
Merge pull request #2 from gemwalletcom/monad-lens
Add monad staking lens contract
2 parents 800ba44 + ac708e7 commit 625dc52

17 files changed

Lines changed: 644 additions & 162 deletions

File tree

.env.example

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ BSC_RPC_URL=
1515
AVALANCHE_RPC_URL=
1616
POLYGON_RPC_URL=
1717
ARBITRUM_RPC_URL=
18+
MONAD_RPC_URL=
1819

1920
# Etherscan API Keys
2021
ETHEREUM_SCAN_API_KEY=

.github/workflows/ci.yml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,11 @@ jobs:
2323
with:
2424
version: nightly
2525

26+
- name: Forge lint
27+
run: |
28+
forge lint
29+
id: lint
30+
2631
- name: Forge build
2732
run: |
2833
forge --version

AGENTS.md

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
# Repository Guidelines
2+
3+
## Project Structure & Module Organization
4+
- Core Solidity modules live in `src/`, split into `src/hub_reader` and `src/stargate` for BSC staking and cross-chain calls.
5+
- Automation scripts reside in `script/` (Foundry) and `deploy/` (bash helpers like `deploy-stargate.sh`); compiled artifacts land in `out/`.
6+
- Tests live in `test/` using `.t.sol` suffixes; dependencies stay in `lib/`; align configuration via root-level `foundry.toml` and `.env.example`.
7+
8+
## Build, Test, and Development Commands
9+
- `forge build` or `just build` compiles the workspace with default remappings.
10+
- `forge test` and `forge test --rpc-url $BSC_RPC_URL` execute the suite locally or against a fork.
11+
- `forge lint` then `forge fmt` keep Solidity style consistent; run them before sharing branches.
12+
- `just deploy-hub-reader` and `just deploy-stargate optimism` broadcast deployments through the prewired RPCs.
13+
14+
## Coding Style & Naming Conventions
15+
- Use 4-space indentation, `pragma solidity ^0.8.x`, sorted imports, and SPDX identifiers.
16+
- Name contracts, libraries, and interfaces in PascalCase; state variables in camelCase; constants in ALL_CAPS.
17+
- Match test filenames to their targets (`StargateFeeReceiver.t.sol`) and prefix helper contracts with `Test`.
18+
- Validate formatting with `forge fmt` or `forge fmt --check` before review.
19+
20+
## Testing Guidelines
21+
- Keep integration scenarios in dedicated contracts and isolate unit fixtures per module.
22+
- Leverage `vm.expectRevert`, `vm.prank`, and explicit `assertEq` messages to clarify intent.
23+
- When forking, pass the RPC with `--rpc-url` and note chain assumptions in header comments.
24+
- Prioritize coverage of deposit, withdrawal, and fee flows; `forge coverage --report lcov` helps quantify readiness.
25+
26+
## Commit & Pull Request Guidelines
27+
- Follow the short imperative style seen in history (`add auto formatter`, `rename to StargateFeeReceiver`), keeping summaries under 65 characters.
28+
- Reference tickets, flag deployment or configuration impacts, and list the tests you ran.
29+
- For PRs, link on-chain transactions, attach explorer URLs or calldata, and note any environment variable or RPC updates.
30+
31+
## Security & Configuration Tips
32+
- Copy `.env.example` to `.env`, add RPC URLs and scan keys matching `foundry.toml`, and keep secrets untracked.
33+
- Limit raw private keys to deployment contexts; favor hardware signing for `forge script --broadcast`.
34+
- Before merging, confirm remappings, target chain IDs, and contract addresses to avoid cross-chain leaks.

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

foundry.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
src = "src"
33
out = "out"
44
libs = ["lib"]
5+
via_ir = true
56

67
[rpc_endpoints]
78
ethereum = "${ETHEREUM_RPC_URL}"
@@ -11,6 +12,7 @@ bsc = "${BSC_RPC_URL}"
1112
avalanche = "${AVALANCHE_RPC_URL}"
1213
polygon = "${POLYGON_RPC_URL}"
1314
arbitrum = "${ARBITRUM_RPC_URL}"
15+
monad = "${MONAD_RPC_URL}"
1416

1517
[etherscan]
1618
ethereum = { key = "${ETHEREUM_SCAN_API_KEY}" }

justfile

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,22 @@
11
set dotenv-load := true
22

3+
list:
4+
just --list
35

46
build:
57
forge build
68

79
test:
810
forge test
911

12+
test-monad:
13+
forge test --match-path test/monad/*
14+
1015
deploy-stargate CHAIN_NAME:
1116
bash ./deploy/deploy-stargate.sh {{CHAIN_NAME}}
1217

1318
deploy-hub-reader:
1419
forge script script/hub_reader/HubReader.s.sol:HubReaderScript --rpc-url "$BSC_RPC_URL" --broadcast --verify -vvvv
20+
21+
deploy-monad-staking:
22+
forge script --force script/monad/StakingLens.s.sol:StakingLensScript --rpc-url "$MONAD_RPC_URL" --broadcast -vvvv

script/hub_reader/HubReader.s.sol

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
pragma solidity ^0.8.13;
33

44
import {Script, console} from "forge-std/Script.sol";
5-
import "../../src/hub_reader/HubReader.sol";
5+
import {HubReader} from "../../src/hub_reader/HubReader.sol";
66

77
contract HubReaderScript is Script {
88
function run() public {

script/monad/StakingLens.s.sol

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
// SPDX-License-Identifier: MIT
2+
pragma solidity ^0.8.15;
3+
4+
import {Script, console} from "forge-std/Script.sol";
5+
import {StakingLens} from "../../src/monad/StakingLens.sol";
6+
7+
contract StakingLensScript is Script {
8+
function run() public {
9+
uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY");
10+
vm.startBroadcast(deployerPrivateKey);
11+
StakingLens lens = new StakingLens();
12+
console.log("StakingLens deployed to:", address(lens));
13+
vm.stopBroadcast();
14+
}
15+
}

script/stargate/GemStargateDeployer.s.sol

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
pragma solidity ^0.8.13;
33

44
import {Script, console} from "forge-std/Script.sol";
5-
import "../../src/stargate/StargateFeeReceiver.sol";
5+
import {StargateFeeReceiver} from "../../src/stargate/StargateFeeReceiver.sol";
66

77
contract GemStargateDeployerScript is Script {
88
struct NetworkConfig {
@@ -31,7 +31,7 @@ contract GemStargateDeployerScript is Script {
3131
// Get values from environment
3232
address endpoint = vm.envAddress(endpointVar);
3333

34-
return NetworkConfig(endpoint);
34+
return NetworkConfig({endpoint: endpoint});
3535
}
3636

3737
function run() public {

src/hub_reader/HubReader.sol

Lines changed: 31 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -43,32 +43,18 @@ contract HubReader {
4343
*
4444
* @return The validators
4545
*/
46-
function getValidators(
47-
uint16 offset,
48-
uint16 limit
49-
) external view returns (Validator[] memory) {
50-
(address[] memory operatorAddrs, , uint256 totalLength) = stakeHub
51-
.getValidators(offset, limit);
46+
function getValidators(uint16 offset, uint16 limit) external view returns (Validator[] memory) {
47+
(address[] memory operatorAddrs,, uint256 totalLength) = stakeHub.getValidators(offset, limit);
5248
uint256 validatorCount = totalLength < limit ? totalLength : limit;
5349
Validator[] memory validators = new Validator[](validatorCount);
5450

5551
for (uint256 i = 0; i < validatorCount; i++) {
56-
(, bool jailed, ) = stakeHub.getValidatorBasicInfo(
57-
operatorAddrs[i]
58-
);
59-
string memory moniker = stakeHub
60-
.getValidatorDescription(operatorAddrs[i])
61-
.moniker;
62-
uint64 rate = stakeHub
63-
.getValidatorCommission(operatorAddrs[i])
64-
.rate;
52+
(, bool jailed,) = stakeHub.getValidatorBasicInfo(operatorAddrs[i]);
53+
string memory moniker = stakeHub.getValidatorDescription(operatorAddrs[i]).moniker;
54+
uint64 rate = stakeHub.getValidatorCommission(operatorAddrs[i]).rate;
6555

6656
validators[i] = Validator({
67-
operatorAddress: operatorAddrs[i],
68-
moniker: moniker,
69-
commission: rate,
70-
jailed: jailed,
71-
apy: 0
57+
operatorAddress: operatorAddrs[i], moniker: moniker, commission: rate, jailed: jailed, apy: 0
7258
});
7359
}
7460
uint64[] memory apys = this.getAPYs(operatorAddrs, block.timestamp);
@@ -86,16 +72,13 @@ contract HubReader {
8672
*
8773
* @return The delegations of the delegator
8874
*/
89-
function getDelegations(
90-
address delegator,
91-
uint16 offset,
92-
uint16 limit
93-
) external view returns (Delegation[] memory) {
94-
(
95-
address[] memory operatorAddrs,
96-
address[] memory creditAddrs,
97-
uint256 totalLength
98-
) = stakeHub.getValidators(offset, limit);
75+
function getDelegations(address delegator, uint16 offset, uint16 limit)
76+
external
77+
view
78+
returns (Delegation[] memory)
79+
{
80+
(address[] memory operatorAddrs, address[] memory creditAddrs, uint256 totalLength) =
81+
stakeHub.getValidators(offset, limit);
9982
uint256 validatorCount = totalLength < limit ? totalLength : limit;
10083
uint256 delegationCount = 0;
10184
Delegation[] memory delegations = new Delegation[](validatorCount);
@@ -107,10 +90,7 @@ contract HubReader {
10790

10891
if (amount > 0) {
10992
delegations[delegationCount] = Delegation({
110-
delegatorAddress: delegator,
111-
validatorAddress: operatorAddrs[i],
112-
shares: shares,
113-
amount: amount
93+
delegatorAddress: delegator, validatorAddress: operatorAddrs[i], shares: shares, amount: amount
11494
});
11595
delegationCount++;
11696
}
@@ -131,38 +111,29 @@ contract HubReader {
131111
*
132112
* @return The undelegations of the delegator
133113
*/
134-
function getUndelegations(
135-
address delegator,
136-
uint16 offset,
137-
uint16 limit
138-
) external view returns (Undelegation[] memory) {
139-
(
140-
address[] memory operatorAddrs,
141-
address[] memory creditAddrs,
142-
uint256 totalLength
143-
) = stakeHub.getValidators(offset, limit);
114+
function getUndelegations(address delegator, uint16 offset, uint16 limit)
115+
external
116+
view
117+
returns (Undelegation[] memory)
118+
{
119+
(address[] memory operatorAddrs, address[] memory creditAddrs, uint256 totalLength) =
120+
stakeHub.getValidators(offset, limit);
144121
uint256 validatorCount = totalLength < limit ? totalLength : limit;
145122

146123
// first loop to get the number of unbond requests
147124
uint256 undelegationCount = 0;
148125
for (uint256 i = 0; i < validatorCount; i++) {
149-
undelegationCount += IStakeCredit(creditAddrs[i])
150-
.pendingUnbondRequest(delegator);
126+
undelegationCount += IStakeCredit(creditAddrs[i]).pendingUnbondRequest(delegator);
151127
}
152128

153-
Undelegation[] memory undelegations = new Undelegation[](
154-
undelegationCount
155-
);
129+
Undelegation[] memory undelegations = new Undelegation[](undelegationCount);
156130

157131
// resuse same local variable
158132
undelegationCount = 0;
159133
for (uint256 i = 0; i < validatorCount; i++) {
160-
uint256 unbondCount = IStakeCredit(creditAddrs[i])
161-
.pendingUnbondRequest(delegator);
134+
uint256 unbondCount = IStakeCredit(creditAddrs[i]).pendingUnbondRequest(delegator);
162135
for (uint256 j = 0; j < unbondCount; j++) {
163-
IStakeCredit.UnbondRequest memory req = IStakeCredit(
164-
creditAddrs[i]
165-
).unbondRequest(delegator, j);
136+
IStakeCredit.UnbondRequest memory req = IStakeCredit(creditAddrs[i]).unbondRequest(delegator, j);
166137
undelegations[undelegationCount] = Undelegation({
167138
delegatorAddress: delegator,
168139
validatorAddress: operatorAddrs[i],
@@ -183,28 +154,22 @@ contract HubReader {
183154
*
184155
* @return The APYs of the validator in basis points, e.g. 195 is 1.95%
185156
*/
186-
function getAPYs(
187-
address[] memory operatorAddrs,
188-
uint256 timestamp
189-
) external view returns (uint64[] memory) {
157+
// forge-lint: disable-next-line(mixed-case-function)
158+
function getAPYs(address[] memory operatorAddrs, uint256 timestamp) external view returns (uint64[] memory) {
190159
uint256 dayIndex = timestamp / stakeHub.BREATHE_BLOCK_INTERVAL();
191160
uint256 length = operatorAddrs.length;
192161
uint64[] memory apys = new uint64[](length);
193162
for (uint256 i = 0; i < length; i++) {
194-
uint256 total = stakeHub.getValidatorTotalPooledBNBRecord(
195-
operatorAddrs[i],
196-
dayIndex
197-
);
163+
uint256 total = stakeHub.getValidatorTotalPooledBNBRecord(operatorAddrs[i], dayIndex);
198164
if (total == 0) {
199165
continue;
200166
}
201-
uint256 reward = stakeHub.getValidatorRewardRecord(
202-
operatorAddrs[i],
203-
dayIndex
204-
);
167+
uint256 reward = stakeHub.getValidatorRewardRecord(operatorAddrs[i], dayIndex);
205168
if (reward == 0) {
206169
continue;
207170
}
171+
// casting to uint64 is safe because APY basis points from hub totals fit in 64 bits
172+
// forge-lint: disable-next-line(unsafe-typecast)
208173
apys[i] = uint64((reward * 365 * 10000) / total);
209174
}
210175
return apys;

0 commit comments

Comments
 (0)