Skip to content
Merged
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
1 change: 1 addition & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -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=
Expand Down
5 changes: 5 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,11 @@ jobs:
with:
version: nightly

- name: Forge lint
run: |
forge lint
id: lint

- name: Forge build
run: |
forge --version
Expand Down
34 changes: 34 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -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.
47 changes: 14 additions & 33 deletions README.md
Original file line number Diff line number Diff line change
@@ -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 <your_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`



Expand Down
2 changes: 2 additions & 0 deletions foundry.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
src = "src"
out = "out"
libs = ["lib"]
via_ir = true

[rpc_endpoints]
ethereum = "${ETHEREUM_RPC_URL}"
Expand All @@ -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}" }
Expand Down
8 changes: 8 additions & 0 deletions justfile
Original file line number Diff line number Diff line change
@@ -1,14 +1,22 @@
set dotenv-load := true

list:
just --list

build:
forge build

test:
forge test

test-monad:
forge test --match-path test/monad/*

deploy-stargate CHAIN_NAME:
bash ./deploy/deploy-stargate.sh {{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
2 changes: 1 addition & 1 deletion script/hub_reader/HubReader.s.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
15 changes: 15 additions & 0 deletions script/monad/StakingLens.s.sol
Original file line number Diff line number Diff line change
@@ -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();
}
}
4 changes: 2 additions & 2 deletions script/stargate/GemStargateDeployer.s.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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 {
Expand Down
97 changes: 31 additions & 66 deletions src/hub_reader/HubReader.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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);
Expand All @@ -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++;
}
Expand All @@ -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],
Expand All @@ -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;
Expand Down
14 changes: 4 additions & 10 deletions src/hub_reader/interface/IStakeCredit.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Loading