Skip to content

Commit 6ea283c

Browse files
authored
Merge pull request #1 from lemonjetx/UUPS
Universal Upgradeable Proxy Standard (UUPS)
2 parents 3c29b57 + af95eb5 commit 6ea283c

17 files changed

+480
-128
lines changed

AGENTS.md

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
# Repository Guidelines
2+
3+
## Project Structure & Module Organization
4+
- `src/` Solidity contracts (e.g., `LemonJetUpgradeable.sol`, `VaultUpgradeable.sol`); interfaces live in `src/interfaces/`.
5+
- `test/` Foundry tests (`*.t.sol`) and `test/mocks/` for mock contracts.
6+
- `script/` Foundry scripts (`*.s.sol`) with helpers in `script/utils/`.
7+
- `broadcast/` deployment artifacts per script/chain, `out/` build outputs, `cache/` local build cache.
8+
- `lib/` and `dependencies/` vendor libraries (forge-std, OpenZeppelin, Chainlink), configured via `foundry.toml` and `remappings.txt`.
9+
10+
## Build, Test, and Development Commands
11+
- `forge build` (CI uses `forge build --sizes`) to compile contracts and report size.
12+
- `forge test` (CI uses `forge test -vvv`) to run the suite.
13+
- `forge fmt` or `forge fmt --check` to format or verify formatting.
14+
- `forge snapshot` to capture gas usage snapshots.
15+
- `anvil` to run a local EVM node for scripts and manual testing.
16+
- Example deploy script: `forge script script/LemonJet.s.sol:LemonJetScript --rpc-url $SEPOLIA_RPC_URL --private-key $PRIVATE_KEY` (add `--broadcast` for live deploys).
17+
18+
## Coding Style & Naming Conventions
19+
- Solidity version is 0.8.28 (`foundry.toml`); keep `pragma solidity ^0.8.28`.
20+
- Use 4-space indentation and let `forge fmt` normalize spacing/imports.
21+
- Contracts and files use PascalCase (`LemonJetUpgradeable.sol`), functions use camelCase.
22+
- Tests are `*.t.sol` with `*Test` contracts; revert cases often use `test_RevertWhen_...`.
23+
24+
## Testing Guidelines
25+
- Tests use `forge-std/Test.sol` and a `setUp()` for fixtures.
26+
- Prefer targeted runs while iterating: `forge test --match-contract LemonJetTest` or `--match-test testPlayLjt`.
27+
- No explicit coverage threshold is defined; add regression tests alongside new behavior.
28+
29+
## Commit & Pull Request Guidelines
30+
- Commit messages are short and imperative; some use conventional prefixes like `chore: fmt`. Stay consistent and keep the subject concise.
31+
- PRs should include a summary, motivation, tests run (or `not run` with reason), and any contract address changes or upgrade notes.
32+
33+
## Security & Configuration Tips
34+
- Copy `.env.example` to `.env` and set `SEPOLIA_RPC_URL`, `PRIVATE_KEY`, `OWNER_ADDRESS`, `ETHERSCAN_API_KEY`.
35+
- Never commit private keys or `.env` contents.
36+
- FFI is enabled in `foundry.toml`; review scripts carefully before running untrusted code.

foundry.toml

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,11 @@ libs = ["lib"]
55
optimizer = true
66
optimizer_runs = 20_000
77
solc_version = "0.8.28"
8-
gas_reports = ["LemonJet"]
8+
gas_reports = ["LemonJetUpgradeable"]
9+
ffi = true
10+
ast = true
11+
build_info = true
12+
extra_output = ["storageLayout"]
913

1014
[rpc_endpoints]
1115
sepolia = "${SEPOLIA_RPC_URL}"

remappings.txt

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
11
@chainlink-contracts-1.2.0/=dependencies/@chainlink-contracts-1.2.0/
22
forge-std-1.9.3/=dependencies/forge-std-1.9.3/
3-
@pythnetwork/entropy-sdk-solidity/=node_modules/@pythnetwork/entropy-sdk-solidity
4-
@layerzerolabs/oft-evm/=lib/devtools/packages/oft-evm/
5-
@layerzerolabs/oapp-evm/=lib/devtools/packages/oapp-evm/
6-
@layerzerolabs/lz-evm-protocol-v2/=lib/layerzero-v2/packages/layerzero-v2/evm/protocol/
73
@openzeppelin/contracts/=lib/openzeppelin-contracts-upgradeable/lib/openzeppelin-contracts/contracts/
84
@openzeppelin/contracts-upgradeable/=lib/openzeppelin-contracts-upgradeable/contracts/
5+
openzeppelin-contracts/=lib/openzeppelin-contracts-upgradeable/lib/openzeppelin-contracts/
6+
openzeppelin-contracts-upgradeable/=lib/openzeppelin-contracts-upgradeable/
7+
openzeppelin-foundry-upgrades/=lib/openzeppelin-foundry-upgrades/src/

script/LJTFaucet.s.sol

Lines changed: 0 additions & 27 deletions
This file was deleted.

script/LemonJet.s.sol

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,21 +3,30 @@ pragma solidity ^0.8.13;
33

44
import {Script} from "forge-std/Script.sol";
55
import {console2} from "forge-std/console2.sol";
6-
import {ERC20Mock} from "openzeppelin-contracts/contracts/mocks/token/ERC20Mock.sol";
7-
import {LemonJet} from "../src/LemonJet.sol";
6+
import {Upgrades} from "openzeppelin-foundry-upgrades/Upgrades.sol";
7+
import {LemonJetUpgradeable} from "../src/LemonJetUpgradeable.sol";
8+
import {IERC20} from "openzeppelin-contracts/contracts/token/ERC20/IERC20.sol";
89

910
contract LemonJetDeployScript is Script {
1011
function run() external {
1112
uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY");
1213
address reserveFund = vm.envAddress("RESERVE_FUND_ADDRESS");
1314
address vrfWrapper = vm.envAddress("VRF_WRAPPER_ADDRESS");
1415
address vaultToken = vm.envAddress("VAULT_TOKEN_ADDRESS");
16+
1517
vm.startBroadcast(deployerPrivateKey);
1618

17-
ERC20Mock asset = new ERC20Mock();
19+
// Deploy a UUPS proxy with the LemonJetUpgradeable implementation
20+
address proxy = Upgrades.deployUUPSProxy(
21+
"LemonJetUpgradeable.sol:LemonJetUpgradeable",
22+
abi.encodeCall(
23+
LemonJetUpgradeable.initialize,
24+
(vrfWrapper, reserveFund, IERC20(vaultToken), "LemonJet Vault", "LJUSDC")
25+
)
26+
);
27+
28+
console2.log("LemonJetUpgradeable UUPS proxy deployed at:", proxy);
1829

19-
LemonJet lemonJet = new LemonJet(vrfWrapper, reserveFund, address(asset), "LemonJet Vault", "LJUSDC");
20-
console2.log(address(lemonJet));
2130
vm.stopBroadcast();
2231
}
2332
}

script/Play.s.sol

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

44
import {Script} from "forge-std/Script.sol";
55
import {console2} from "forge-std/console2.sol";
6-
import {LemonJet} from "../src/LemonJet.sol";
6+
import {LemonJetUpgradeable} from "../src/LemonJetUpgradeable.sol";
77
import {LemonJetToken} from "../test/mocks/LemonJetToken.sol";
88

99
contract PlayLemonJetScript is Script {
@@ -14,7 +14,7 @@ contract PlayLemonJetScript is Script {
1414
//
1515
// (bool success,) = ljtGame.call{value: 1000000000000000, gas: 500000}(
1616
// abi.encodeWithSelector(
17-
// LemonJet.play.selector, 111111, 200, address(0xBa0d95449B5E901CFb938fa6b6601281cEf679a4)
17+
// LemonJetUpgradeable.play.selector, 111111, 200, address(0xBa0d95449B5E901CFb938fa6b6601281cEf679a4)
1818
// )
1919
// );
2020
// require(success, "Failed to send Ether");

src/ERC4626Fees.sol

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,13 @@ pragma solidity 0.8.28;
44

55
import {IERC20} from "openzeppelin-contracts/contracts/token/ERC20/IERC20.sol";
66
import {ERC4626} from "openzeppelin-contracts/contracts/token/ERC20/extensions/ERC4626.sol";
7+
import {
8+
ERC4626Upgradeable
9+
} from "openzeppelin-contracts-upgradeable/contracts/token/ERC20/extensions/ERC4626Upgradeable.sol";
710
import {SafeERC20} from "openzeppelin-contracts/contracts/token/ERC20/utils/SafeERC20.sol";
811

912
/// @dev ERC4626 vault with exit fees expressed in https://en.wikipedia.org/wiki/Basis_point[basis point (bp)].
10-
abstract contract ERC4626Fees is ERC4626 {
13+
abstract contract ERC4626FeesUpgradeable is ERC4626Upgradeable {
1114
uint256 internal constant _BASIS_POINT_SCALE = 1e4;
1215
uint256 internal constant _exitFeeBasisPoints = 60; // 0.6%
1316

src/LJTFaucet.sol

Lines changed: 0 additions & 41 deletions
This file was deleted.
Lines changed: 29 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,22 @@ pragma solidity 0.8.28;
33

44
import {SafeERC20} from "openzeppelin-contracts/contracts/token/ERC20/utils/SafeERC20.sol";
55
import {IERC20} from "openzeppelin-contracts/contracts/token/ERC20/IERC20.sol";
6-
import {
7-
VRFV2PlusWrapperConsumerBase
8-
} from "@chainlink-contracts-1.2.0/src/v0.8/vrf/dev/VRFV2PlusWrapperConsumerBase.sol";
9-
6+
import {VRFV2PlusWrapperConsumerBaseUpgradeable} from "./VRFV2PlusWrapperConsumerBase.sol";
107
import {Math} from "openzeppelin-contracts/contracts/utils/math/Math.sol";
118
import {ILemonJet} from "./interfaces/ILemonJet.sol";
12-
import {Vault} from "./Vault.sol";
9+
import {VaultUpgradeable} from "./VaultUpgradeable.sol";
1310
import {Referral} from "./Referral.sol";
14-
15-
contract LemonJet is ILemonJet, Referral, Vault, VRFV2PlusWrapperConsumerBase {
11+
import {OwnableUpgradeable} from "openzeppelin-contracts-upgradeable/contracts/access/OwnableUpgradeable.sol";
12+
import {UUPSUpgradeable} from "openzeppelin-contracts-upgradeable/contracts/proxy/utils/UUPSUpgradeable.sol";
13+
14+
contract LemonJetUpgradeable is
15+
ILemonJet,
16+
Referral,
17+
VaultUpgradeable,
18+
VRFV2PlusWrapperConsumerBaseUpgradeable,
19+
OwnableUpgradeable,
20+
UUPSUpgradeable
21+
{
1622
using SafeERC20 for IERC20;
1723

1824
uint8 private constant STARTED = 1;
@@ -30,18 +36,27 @@ contract LemonJet is ILemonJet, Referral, Vault, VRFV2PlusWrapperConsumerBase {
3036
uint8 status; // 0, 1, 2
3137
}
3238

33-
constructor(
39+
/// @custom:oz-upgrades-unsafe-allow constructor
40+
constructor() {
41+
_disableInitializers();
42+
}
43+
44+
function initialize(
3445
// VRF Wrapper 2.5 for Direct Funding
3546
address wrapperAddress,
3647
// EOA which receives a fee
3748
address _reserveFund,
3849
// ERC20 token for ERC4626 Vault
39-
address _asset,
50+
IERC20 _asset,
4051
// Vault shares token name
4152
string memory _name,
4253
// Vault shares token symbol
4354
string memory _symbol
44-
) VRFV2PlusWrapperConsumerBase(wrapperAddress) Vault(_asset, _reserveFund, _name, _symbol) {}
55+
) public initializer {
56+
VRFV2PlusWrapperConsumerBaseUpgradeable.initialize(wrapperAddress);
57+
VaultUpgradeable.initialize(_asset, _reserveFund, _name, _symbol);
58+
__Ownable_init(msg.sender);
59+
}
4560

4661
function play(uint256 bet, uint32 coef, address referrer) external payable {
4762
referrer = _setReferrerIfNotExists(referrer);
@@ -112,7 +127,7 @@ contract LemonJet is ILemonJet, Referral, Vault, VRFV2PlusWrapperConsumerBase {
112127
// check if a player has won
113128
if (randomNumber <= gameThreshold) {
114129
/**
115-
* @dev See {Vault-_payoutWin}.
130+
* @dev See {VaultUpgradeable-_payoutWin}.
116131
*/
117132
_payoutWin(player, payout);
118133
} else {
@@ -153,4 +168,7 @@ contract LemonJet is ILemonJet, Referral, Vault, VRFV2PlusWrapperConsumerBase {
153168
function claimNativeBalance() external {
154169
payable(reserveFund).transfer(address(this).balance);
155170
}
171+
172+
/// @dev Required by UUPSUpgradeable - only owner can upgrade
173+
function _authorizeUpgrade(address newImplementation) internal virtual override onlyOwner {}
156174
}
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
// SPDX-License-Identifier: GPL-3.0
2+
pragma solidity 0.8.28;
3+
4+
import {LinkTokenInterface} from "@chainlink-contracts-1.2.0/src/v0.8/shared/interfaces/LinkTokenInterface.sol";
5+
import {IVRFV2PlusWrapper} from "@chainlink-contracts-1.2.0/src/v0.8/vrf/dev/interfaces/IVRFV2PlusWrapper.sol";
6+
import {Initializable} from "openzeppelin-contracts-upgradeable/contracts/proxy/utils/Initializable.sol";
7+
8+
abstract contract VRFV2PlusWrapperConsumerBaseUpgradeable is Initializable {
9+
error OnlyVRFWrapperCanFulfill(address have, address want);
10+
11+
IVRFV2PlusWrapper public i_vrfV2PlusWrapper;
12+
13+
/**
14+
* @param _vrfV2PlusWrapper is the address of the VRFV2Wrapper contract
15+
*/
16+
function initialize(address _vrfV2PlusWrapper) internal onlyInitializing {
17+
i_vrfV2PlusWrapper = IVRFV2PlusWrapper(_vrfV2PlusWrapper);
18+
}
19+
20+
// solhint-disable-next-line chainlink-solidity/prefix-internal-functions-with-underscore
21+
function requestRandomnessPayInNative(
22+
uint32 _callbackGasLimit,
23+
uint16 _requestConfirmations,
24+
uint32 _numWords,
25+
bytes memory extraArgs
26+
) internal returns (uint256 requestId, uint256 requestPrice) {
27+
requestPrice = i_vrfV2PlusWrapper.calculateRequestPriceNative(_callbackGasLimit, _numWords);
28+
return (
29+
i_vrfV2PlusWrapper.requestRandomWordsInNative{value: requestPrice}(
30+
_callbackGasLimit, _requestConfirmations, _numWords, extraArgs
31+
),
32+
requestPrice
33+
);
34+
}
35+
36+
/**
37+
* @notice fulfillRandomWords handles the VRF V2 wrapper response. The consuming contract must
38+
* @notice implement it.
39+
*
40+
* @param _requestId is the VRF V2 request ID.
41+
* @param _randomWords is the randomness result.
42+
*/
43+
// solhint-disable-next-line chainlink-solidity/prefix-internal-functions-with-underscore
44+
function fulfillRandomWords(uint256 _requestId, uint256[] memory _randomWords) internal virtual;
45+
46+
function rawFulfillRandomWords(uint256 _requestId, uint256[] memory _randomWords) external {
47+
address vrfWrapperAddr = address(i_vrfV2PlusWrapper);
48+
if (msg.sender != vrfWrapperAddr) {
49+
revert OnlyVRFWrapperCanFulfill(msg.sender, vrfWrapperAddr);
50+
}
51+
fulfillRandomWords(_requestId, _randomWords);
52+
}
53+
54+
/// @notice getBalance returns the native balance of the consumer contract
55+
function getBalance() public view returns (uint256) {
56+
return address(this).balance;
57+
}
58+
}

0 commit comments

Comments
 (0)