Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
57ec7f1
feat: implement P3 permanent staking redesign
rplusq Sep 10, 2025
a69ede1
docs: streamline P3 audit documentation to essential high-signal content
rplusq Sep 24, 2025
0508fcd
feat: migrate StakingRewardDistributor to AccessControl and make Lock…
rplusq Sep 25, 2025
56ec06b
fix: resolve stack too deep error in fork tests
rplusq Sep 26, 2025
3c18344
refactor: remove deprecated feed() function and fix security issues
rplusq Sep 26, 2025
299e7ba
ci: fix build job to exclude external contracts and apply formatting
rplusq Sep 26, 2025
1d15cdf
fix(CANTINA-15): Externally created locks can lead to DOS while claim…
rplusq Oct 7, 2025
b591e4f
fix(CANTINA-9): Vesting revocation is not relayed to StakeWeight
rplusq Oct 7, 2025
555566e
fix(CANTINA-8): Reward Injection Allowed After kill() and While Paused#8
rplusq Oct 7, 2025
3349955
test(CANTINA-12): Document duplicate allocation claim blocking with a…
rplusq Oct 8, 2025
1575914
fix(CANTINA-10): Add _disableInitializers to StakeWeight constructor
rplusq Oct 8, 2025
497a1ea
fix(CANTINA-6): Use SafeCast for int256 to uint256 conversions in Sta…
rplusq Oct 8, 2025
8663497
docs(CANTINA-7): Document token injection requirement in StakingRewar…
rplusq Oct 8, 2025
1681da6
fix(CANTINA-3): Remove unreachable branch in StakingRewardDistributor…
rplusq Oct 8, 2025
21e3ec6
gas(CANTINA-5): Remove redundant isPermanent check in withdrawAllFor
rplusq Oct 8, 2025
26c0bbf
fix(build): Restore optimizer_runs to 10_000 to preserve storage layout
rplusq Oct 9, 2025
6ac72b9
test(CANTINA-13): Add storage baseline regression testing to upgrade …
rplusq Oct 9, 2025
7516559
refactor(CANTINA-7): Missing Reusable Modifier Across Contracts
rplusq Oct 10, 2025
1752fc0
docs(timelock): Add executeBatch calldata generation for P3 upgrade
rplusq Oct 11, 2025
9b36e79
docs: Remove P3 audit artifacts and clean up developer guide
rplusq Oct 14, 2025
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
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ jobs:
run: "pnpm install"

- name: "Build the contracts and print their size"
run: "cd evm && forge build --sizes"
run: "cd evm && forge build --sizes src/**/*.sol --skip src/utils/**/*.sol"

- name: "Add build summary"
run: |
Expand Down
26 changes: 26 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
## Testing Patterns

- **Test Philosophy**: Concrete tests validate specific paths, fuzz tests provide confidence across ranges
- **Foundry Gotchas**:
- `vm.prank` and `vm.expectRevert` apply to the NEXT external call only
- Use `vm.startPrank/stopPrank` for multi-call setups to avoid losing the prank context
- **BTT Structure**: Tests use modifiers to match Branch-Based Testing tree structure for clarity
- **Time Advancement**: Use `_mineBlocks()` not `vm.warp()` (updates both timestamp and block number)
- **Invariant Cache**: The `cache/invariant` directory stores failed sequences for regression testing - only clear after
fixing bugs

## Fork Testing Setup

```bash
source .common.env && source .optimism.env
export OPTIMISM_RPC_URL=https://optimism-rpc.publicnode.com
forge test --force # --force required for OpenZeppelin plugin
```

## Critical System Constraints

### Token Supply Limits

- **WCT Max Supply**: 1 billion tokens (no holder > 15%)
- **int128 Safety**: Max 100M tokens (1e26) for test amounts to prevent overflows
- **Test Amount Guidelines**: whale (10M), large (1M), medium (100k), small (10k)
74 changes: 73 additions & 1 deletion evm/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ VERBOSITY = -vvvv
ETHEREUM_DEPLOY = script/deploy/EthereumDeploy.s.sol:EthereumDeploy
OPTIMISM_DEPLOY = script/deploy/OptimismDeploy.s.sol:OptimismDeploy
ANVIL_DEPLOY = script/deploy/AnvilDeploy.s.sol:AnvilDeploy
P3_UPGRADE = script/deploy/P3Upgrade.s.sol:P3Upgrade

# Default RPC URL for Anvil
DEFAULT_ANVIL_RPC = http://localhost:8545
Expand All @@ -20,7 +21,7 @@ else
endif

# Network-specific targets
.PHONY: deploy-mainnet deploy-sepolia deploy-optimism deploy-optimism-sepolia deploy-anvil log-mainnet log-sepolia log-optimism log-optimism-sepolia log-anvil fund-deployer json-mainnet json-sepolia json-optimism json-optimism-sepolia json-anvil
.PHONY: deploy-mainnet deploy-sepolia deploy-optimism deploy-optimism-sepolia deploy-anvil log-mainnet log-sepolia log-optimism log-optimism-sepolia log-anvil fund-deployer json-mainnet json-sepolia json-optimism json-optimism-sepolia json-anvil p3-upgrade-optimism p3-calldata-optimism

deploy-mainnet:
$(broadcast_info)
Expand All @@ -47,6 +48,16 @@ deploy-anvil:
@echo "Deploying to Anvil local network"
@$(MAKE) _deploy ENV_FILE=.anvil.env SCRIPT=$(ANVIL_DEPLOY) IS_ANVIL=true

# P3 Upgrade targets
p3-upgrade-optimism:
$(broadcast_info)
@echo "Deploying P3 Upgrade to Optimism Mainnet"
@$(MAKE) _p3_deploy ENV_FILE=.optimism.env SCRIPT=$(P3_UPGRADE) LEDGER=true

p3-calldata-optimism:
@echo "Generating P3 Upgrade timelock calldata for Optimism Mainnet"
@$(MAKE) _p3_calldata ENV_FILE=.optimism.env CHAIN_ID=10

log-mainnet:
@echo "Logging deployments for Ethereum Mainnet"
@$(MAKE) _log_deployments ENV_FILE=.mainnet.env SCRIPT=$(ETHEREUM_DEPLOY)
Expand Down Expand Up @@ -159,6 +170,62 @@ endif
--sender ${ETH_FROM}
@echo "JSON deployment file generated at deployments/$(CHAIN_ID).json"

# Internal P3 deploy function (with --force for OpenZeppelin plugin)
_p3_deploy:
$(eval include .common.env)
$(eval include $(ENV_FILE))
$(eval ETHSCAN_KEY = $(if $(findstring optimism,$(ENV_FILE)),${API_KEY_OPTIMISTIC_ETHERSCAN},${API_KEY_ETHERSCAN}))

ifeq ($(IS_ANVIL),)
$(eval RPC_URL = https://${CHAIN_NAME}.infura.io/v3/${API_KEY_INFURA})
$(eval BROADCAST_FLAGS += --verify --etherscan-api-key ${ETHSCAN_KEY})
else
$(eval RPC_URL = $(DEFAULT_ANVIL_RPC))
endif

@if [ "$(LEDGER)" = "true" ]; then \
$(FORGE_CMD) $(SCRIPT) \
$(VERBOSITY) \
--rpc-url $(RPC_URL) \
--sender ${ETH_FROM} \
--mnemonic-indexes ${MNEMONIC_INDEX} \
--ledger \
--force \
$(BROADCAST_FLAGS); \
else \
$(FORGE_CMD) $(SCRIPT) \
$(VERBOSITY) \
--rpc-url $(RPC_URL) \
--sender ${ETH_FROM} \
--account ${KEYSTORE_ACCOUNT} \
--force \
$(BROADCAST_FLAGS); \
fi

# Generate P3 upgrade timelock calldata
_p3_calldata:
$(eval include .common.env)
$(eval include $(ENV_FILE))
$(eval RPC_URL = https://${CHAIN_NAME}.infura.io/v3/${API_KEY_INFURA})
$(eval DEPLOYMENT_JSON = deployments/$(CHAIN_ID)-p3-upgrade.json)
@if [ ! -f "$(DEPLOYMENT_JSON)" ]; then \
echo "Error: $(DEPLOYMENT_JSON) not found. Run p3-upgrade-optimism* first."; \
exit 1; \
fi
$(eval PAUSER_IMPL = $(shell cat $(DEPLOYMENT_JSON) | jq -r '.NewImplementations.Pauser'))
$(eval STAKEWEIGHT_IMPL = $(shell cat $(DEPLOYMENT_JSON) | jq -r '.NewImplementations.StakeWeight'))
$(eval SRD_IMPL = $(shell cat $(DEPLOYMENT_JSON) | jq -r '.NewImplementations.StakingRewardDistributor'))
@echo "Loading implementation addresses from $(DEPLOYMENT_JSON):"
@echo " Pauser: $(PAUSER_IMPL)"
@echo " StakeWeight: $(STAKEWEIGHT_IMPL)"
@echo " StakingRewardDistributor: $(SRD_IMPL)"
@echo ""
$(FORGE_CMD) $(P3_UPGRADE) \
$(VERBOSITY) \
-s "logTimelockCalldata(address,address,address)" \
$(PAUSER_IMPL) $(STAKEWEIGHT_IMPL) $(SRD_IMPL) \
--rpc-url $(RPC_URL)

# Help target
.PHONY: help
help:
Expand All @@ -179,6 +246,11 @@ help:
@echo " json-optimism-sepolia - Generate JSON deployments for Optimism Sepolia testnet"
@echo " json-anvil - Generate JSON deployments for Anvil local network"
@echo " fund-anvil - Fund admin/treasury account on Anvil"
@echo ""
@echo "P3 Upgrade targets:"
@echo " p3-upgrade-optimism - Deploy P3 upgrade to Optimism Mainnet"
@echo " p3-calldata-optimism - Generate timelock calldata for Optimism Mainnet"
@echo ""
@echo " help - Show this help message"
@echo ""
@echo "Flags:"
Expand Down
23 changes: 23 additions & 0 deletions evm/deployments/10-p3-upgrade.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
{
"LockedTokenStakerBackersP3": {
"admin": "0x8B79481a2d8EEbe0F4d6Fd1705b0598540875407",
"implementation": "0xB52b4633eA577A143fAa811127CeE7163b023bFb",
"proxy": "0x37BAdde9E3c9E108065Db36dCcce91E00556e334"
},
"LockedTokenStakerReownP3": {
"admin": "0x54594f1aF2A6bd876bF0f6d45FB7c3cb54eFB5d5",
"implementation": "0xABB9F4CCb5025c446c7FF2e000c705332d21f760",
"proxy": "0xCcCf4C051C6F5a82D6bD76a1F66fc3cCdE8b3D6b"
},
"LockedTokenStakerWalletConnectP3": {
"admin": "0xBa900e340f781A18C34Ff9D73CDb9756329a71d9",
"implementation": "0xEc32c9280163795921caF79205C6850C67023F04",
"proxy": "0xb6034870fdd84f09cB63433297C667A1A7771824"
},
"NewImplementations": {
"Pauser": "0xf69771dAc1c783F6Ae6ddbC3BF6B4869E7c43a70",
"StakeWeight": "0x3ad012e14f6c27dd4a990d09126fcc255b024564",
"StakingRewardDistributor": "0x98c27ae4C11F67CDE0E3aFDf90fBba01Bf826238"
},
"chainId": 10
}
44 changes: 25 additions & 19 deletions evm/foundry.toml
Original file line number Diff line number Diff line change
@@ -1,30 +1,36 @@
# Full reference https://github.com/foundry-rs/foundry/tree/master/crates/config

[profile.default]
auto_detect_solc = false
block_timestamp = 1_680_220_800 # March 31, 2023 at 00:00 GMT
bytecode_hash = "none"
evm_version = "paris" # See https://www.evmdiff.com/features?name=PUSH0&kind=opcode
fuzz = { runs = 1_000 }
gas_reports = ["*"]
optimizer = true
optimizer_runs = 10_000
out = "out"
script = "script"
solc = "0.8.25"
src = "src"
test = "test"
fs_permissions = [{ access = "read-write", path = "./deployments"}, { access = "read", path = "./"}]
ffi = true
ast = true
build_info = true
extra_output = ["storageLayout"]
libs = ["lib", "../node_modules"]
src = "src"
out = "out"
libs = ["lib", "../node_modules"]
solc = "0.8.25" # Pin specific version for reproducibility
optimizer = true
optimizer_runs = 10_000 # Match deployed contracts (DO NOT CHANGE - affects storage layout)
fuzz = { runs = 256 } # Reduced for faster normal runs
invariant = { runs = 10, depth = 15, fail_on_revert = false } # Keep false for now to match current behavior
gas_reports = ["*"] # Generate gas reports for all contracts
script = "script"
test = "test"
auto_detect_solc = false
block_timestamp = 1_680_220_800 # March 31, 2023 at 00:00 GMT
bytecode_hash = "none"
evm_version = "cancun" # Needed for MerkleVester interactions
fs_permissions = [{ access = "read-write", path = "./deployments"}, { access = "read", path = "./"}]
ffi = true
ast = true
build_info = true
extra_output = ["storageLayout"]

[profile.ci]
fuzz = { runs = 10_000 }
verbosity = 4

[profile.thorough]
# Deep testing profile for comprehensive coverage
fuzz = { runs = 1000 }
invariant = { runs = 100, depth = 50, fail_on_revert = false }
verbosity = 2

[fmt]
bracket_spacing = true
Expand Down
10 changes: 5 additions & 5 deletions evm/script/Base.s.sol
Original file line number Diff line number Diff line change
Expand Up @@ -114,12 +114,12 @@ abstract contract BaseScript is Script, StdCheats {
});
}
// Length per address is 32 bytes => 64 characters
// 10 addresses are needed for the Optimism deployments
// If the length is not 0 nor 64 * 14, we assume the deployments are missing contracts and we append as much
// as needed to make it 64 * 14 bytes
if (data.length != 64 * 14) {
// 15 addresses are needed for the Optimism deployments
// If the length is not 0 nor 64 * 15, we assume the deployments are missing contracts and we append as much
// as needed to make it 64 * 15 bytes
if (data.length != 64 * 15) {
console2.log("Appending zeroes to deployments");
while (data.length < 64 * 14) {
while (data.length < 64 * 15) {
data = bytes.concat(data, abi.encode(bytes32(0)));
}
}
Expand Down
41 changes: 30 additions & 11 deletions evm/script/deploy/OptimismDeploy.s.sol
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@ import {
newWalletConnectConfig,
newPauser,
newStakeWeight,
newStakingRewardDistributor
newStakingRewardDistributor,
newLockedTokenStaker
} from "script/helpers/Proxy.sol";

struct OptimismDeploymentParams {
Expand Down Expand Up @@ -233,19 +234,36 @@ contract OptimismDeploy is BaseScript {
}

if (address(deps.lockedTokenStakerReown) == address(0)) {
deps.lockedTokenStakerReown = new LockedTokenStaker{
salt: keccak256(abi.encodePacked("walletconnect.lockedtokenstaker"))
}(deps.merkleVesterReown, WalletConnectConfig(address(deps.config)));
deps.lockedTokenStakerReown = newLockedTokenStaker({
initialOwner: address(deps.adminTimelock),
init: LockedTokenStaker.Init({
vesterContract: address(deps.merkleVesterReown),
config: address(deps.config)
}),
identifier: "reown"
});
}

if (address(deps.lockedTokenStakerWalletConnect) == address(0)) {
deps.lockedTokenStakerWalletConnect =
new LockedTokenStaker(deps.merkleVesterWalletConnect, WalletConnectConfig(address(deps.config)));
deps.lockedTokenStakerWalletConnect = newLockedTokenStaker({
initialOwner: address(deps.adminTimelock),
init: LockedTokenStaker.Init({
vesterContract: address(deps.merkleVesterWalletConnect),
config: address(deps.config)
}),
identifier: "walletconnect"
});
}

if (address(deps.lockedTokenStakerBackers) == address(0)) {
deps.lockedTokenStakerBackers =
new LockedTokenStaker(deps.merkleVesterBackers, WalletConnectConfig(address(deps.config)));
deps.lockedTokenStakerBackers = newLockedTokenStaker({
initialOwner: address(deps.adminTimelock),
init: LockedTokenStaker.Init({
vesterContract: address(deps.merkleVesterBackers),
config: address(deps.config)
}),
identifier: "backers"
});
}

if (vm.envOr("BROADCAST", false)) {
Expand Down Expand Up @@ -356,9 +374,10 @@ contract OptimismDeploy is BaseScript {
console2.log("L2WCT default admin is Admin MultiSig");
}

// StakingRewardDistributor
if (deps.stakingRewardDistributor.owner() != address(params.treasury)) {
revert("StakingRewardDistributor owner is not Treasury");
// StakingRewardDistributor - check REWARD_MANAGER_ROLE instead of owner
bytes32 REWARD_MANAGER_ROLE = keccak256("REWARD_MANAGER_ROLE");
if (!deps.stakingRewardDistributor.hasRole(REWARD_MANAGER_ROLE, address(params.treasury))) {
revert("StakingRewardDistributor: Treasury does not have REWARD_MANAGER_ROLE");
}

// Config
Expand Down
Loading
Loading