Skip to content

Commit 92bd99d

Browse files
authored
Merge branch 'main' into ma/b08-reward-claims-interface-doc
2 parents 001b5d2 + 6db00f0 commit 92bd99d

File tree

9 files changed

+3673
-74
lines changed

9 files changed

+3673
-74
lines changed

contracts/rust/adapter/src/bindings/access_control_upgradeable.rs

Lines changed: 3192 additions & 0 deletions
Large diffs are not rendered by default.

contracts/rust/adapter/src/bindings/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
//! This is autogenerated code.
44
//! Do not manually edit these files.
55
//! These files may be overwritten by the codegen system at any time.
6+
pub mod r#access_control_upgradeable;
67
pub mod r#erc1967_proxy;
78
pub mod r#esp_token;
89
pub mod r#esp_token_v2;

contracts/rust/adapter/src/sol_types.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ use alloy::sol;
2323
/// - structs should be exported and renamed with `xxSol` suffix to avoid confusion with other rust types
2424
/// - see module doc for more explanation on types duplication issue in alloy
2525
pub use crate::bindings::{
26+
access_control_upgradeable::AccessControlUpgradeable,
2627
erc1967_proxy::ERC1967Proxy,
2728
esp_token::EspToken,
2829
esp_token_v2::EspTokenV2,

contracts/rust/deployer/README.md

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
- [Safe Multisig Proposals](#safe-multisig-proposals)
1111
- [Troubleshooting](#troubleshooting)
1212
- [POS Deployment](#pos-deployment)
13+
- [Automated Deployment Verification](#automated-deployment-verification)
1314

1415
## Prerequisites
1516

@@ -1286,6 +1287,32 @@ After completing all steps, verify:
12861287
4. **StakeTable**: Upgraded to V2, owned by OpsTimelock, EspressoSys multisig has pauser role
12871288
5. **All proxies**: Point to correct implementation addresses
12881289

1290+
### Automated Deployment Verification
1291+
1292+
The `scripts/verify-pos-deployment.sh` script automates the verification of your deployment configuration. It checks
1293+
timelock delays, roles, contract ownership, versions, and cross-contract references.
1294+
1295+
#### Prerequisites
1296+
1297+
- Foundry installed (for `cast` command)
1298+
- Environment variables set (see below)
1299+
- Access to the RPC endpoint where contracts are deployed
1300+
1301+
#### Usage
1302+
1303+
##### Source environment variables from your deployment output file
1304+
1305+
source .env.eth.mainnet.staketable # or your deployment output file
1306+
1307+
##### Run verification
1308+
1309+
./scripts/verify-pos-deployment.sh --rpc-url $RPC_URL#### Environment Variables
1310+
1311+
For a complete list of required and optional environment variables, run:
1312+
1313+
./scripts/verify-pos-deployment.sh --helpThe script will skip checks for any unset variables and show warnings. At
1314+
minimum, you should set the contract proxy addresses you want to verify.
1315+
12891316
## Arbitrum Mainnet
12901317

12911318
### Step 1: Deploy the `OpsTimelock`

contracts/rust/deployer/src/builder.rs

Lines changed: 22 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -770,9 +770,10 @@ impl<P: Provider + WalletProvider> DeployerArgs<P> {
770770
"feecontract" | "feecontractproxy" => Contract::FeeContractProxy,
771771
"esptoken" | "esptokenproxy" => Contract::EspTokenProxy,
772772
"staketable" | "staketableproxy" => Contract::StakeTableProxy,
773+
"rewardclaim" | "rewardclaimproxy" => Contract::RewardClaimProxy,
773774
_ => anyhow::bail!(
774775
"Unknown contract type: {}. Supported types: lightclient, feecontract, esptoken, \
775-
staketable",
776+
staketable, rewardclaim",
776777
target_contract
777778
),
778779
};
@@ -785,19 +786,29 @@ impl<P: Provider + WalletProvider> DeployerArgs<P> {
785786
)
786787
})?;
787788

788-
tracing::info!(
789-
"Transferring ownership of {} from EOA to {}",
790-
target_contract,
791-
new_owner
792-
);
793-
794-
// Use the existing transfer_ownership function from lib.rs
795-
let receipt =
789+
// RewardClaim uses AccessControl instead of Ownable, so we need to grant the admin role
790+
// instead of transferring ownership
791+
let receipt = if contract_type == Contract::RewardClaimProxy {
792+
tracing::info!(
793+
"Granting DEFAULT_ADMIN_ROLE for {} to {} (RewardClaim uses AccessControl, not \
794+
Ownable)",
795+
target_contract,
796+
new_owner
797+
);
798+
crate::grant_admin_role(&self.deployer, contract_type, contract_address, new_owner)
799+
.await?
800+
} else {
801+
tracing::info!(
802+
"Transferring ownership of {} from EOA to {}",
803+
target_contract,
804+
new_owner
805+
);
796806
crate::transfer_ownership(&self.deployer, contract_type, contract_address, new_owner)
797-
.await?;
807+
.await?
808+
};
798809

799810
tracing::info!(
800-
"Successfully transferred ownership of {} to {}. Transaction: {}",
811+
"Successfully transferred admin control of {} to {}. Transaction: {}",
801812
target_contract,
802813
new_owner,
803814
receipt.transaction_hash

contracts/rust/deployer/src/lib.rs

Lines changed: 174 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -1208,63 +1208,74 @@ pub async fn transfer_ownership(
12081208
target_address: Address,
12091209
new_owner: Address,
12101210
) -> Result<TransactionReceipt> {
1211-
let receipt = match target_contract {
1212-
Contract::LightClient | Contract::LightClientProxy => {
1213-
tracing::info!(%target_address, %new_owner, "Transfer LightClient ownership");
1214-
let lc = LightClient::new(target_address, &provider);
1215-
lc.transferOwnership(new_owner)
1216-
.send()
1217-
.await?
1218-
.get_receipt()
1219-
.await?
1220-
},
1221-
Contract::FeeContract | Contract::FeeContractProxy => {
1222-
tracing::info!(%target_address, %new_owner, "Transfer FeeContract ownership");
1223-
let fee = FeeContract::new(target_address, &provider);
1224-
fee.transferOwnership(new_owner)
1225-
.send()
1226-
.await?
1227-
.get_receipt()
1228-
.await?
1229-
},
1230-
Contract::EspToken | Contract::EspTokenProxy => {
1231-
tracing::info!(%target_address, %new_owner, "Transfer EspToken ownership");
1232-
let token = EspToken::new(target_address, &provider);
1233-
token
1234-
.transferOwnership(new_owner)
1235-
.send()
1236-
.await?
1237-
.get_receipt()
1238-
.await?
1239-
},
1240-
Contract::StakeTable | Contract::StakeTableProxy | Contract::StakeTableV2 => {
1241-
tracing::info!(%target_address, %new_owner, "Transfer StakeTable ownership");
1242-
let stake_table = StakeTable::new(target_address, &provider);
1243-
stake_table
1244-
.transferOwnership(new_owner)
1245-
.send()
1246-
.await?
1247-
.get_receipt()
1248-
.await?
1249-
},
1250-
Contract::RewardClaim | Contract::RewardClaimProxy => {
1251-
tracing::info!(%target_address, %new_owner, "Grant RewardClaim DEFAULT_ADMIN_ROLE");
1252-
let reward_claim = RewardClaim::new(target_address, &provider);
1253-
let admin_role = reward_claim.DEFAULT_ADMIN_ROLE().call().await?;
1254-
reward_claim
1255-
.grantRole(admin_role, new_owner)
1256-
.send()
1257-
.await?
1258-
.get_receipt()
1259-
.await?
1260-
},
1261-
_ => return Err(anyhow!("Not Ownable, can't transfer ownership!")),
1262-
};
1211+
// Use OwnableUpgradeable interface for all Ownable contracts
1212+
// This is more generic and maintainable than matching on each contract type
1213+
let ownable = OwnableUpgradeable::new(target_address, &provider);
1214+
1215+
// Verify the contract is actually Ownable by checking if we can read the owner
1216+
let current_owner = ownable.owner().call().await.context(format!(
1217+
"Contract at {target_address:#x} does not implement Ownable interface"
1218+
))?;
1219+
1220+
tracing::info!(%target_contract, %target_address, current_owner = %current_owner, new_owner = %new_owner, "Transferring ownership of {target_contract}");
1221+
1222+
let receipt = ownable
1223+
.transferOwnership(new_owner)
1224+
.send()
1225+
.await?
1226+
.get_receipt()
1227+
.await?;
1228+
12631229
let tx_hash = receipt.transaction_hash;
12641230
tracing::info!(%receipt.gas_used, %tx_hash, "ownership transferred");
12651231
Ok(receipt)
12661232
}
12671233

1234+
/// Grant DEFAULT_ADMIN_ROLE to a new admin for AccessControl-based contracts
1235+
/// This handles contracts like RewardClaim that use AccessControl instead of Ownable
1236+
/// TODO: create a function for pauser roles
1237+
pub async fn grant_admin_role(
1238+
provider: impl Provider,
1239+
target_contract: Contract,
1240+
target_address: Address,
1241+
new_admin: Address,
1242+
) -> Result<TransactionReceipt> {
1243+
// Use AccessControlUpgradeable interface
1244+
let access_control = AccessControlUpgradeable::new(target_address, &provider);
1245+
1246+
// Verify the contract is actually AccessControl by checking if we can read roles
1247+
let admin_role = access_control
1248+
.DEFAULT_ADMIN_ROLE()
1249+
.call()
1250+
.await
1251+
.context(format!(
1252+
"Contract at {target_address:#x} does not implement AccessControl interface"
1253+
))?;
1254+
1255+
// Check if new_admin already has the role (for logging purposes)
1256+
let already_has_role = access_control.hasRole(admin_role, new_admin).call().await?;
1257+
1258+
tracing::info!(
1259+
%target_contract,
1260+
%target_address,
1261+
new_admin = %new_admin,
1262+
already_has_role = %already_has_role,
1263+
"Granting DEFAULT_ADMIN_ROLE for {target_contract}"
1264+
);
1265+
1266+
// For RewardClaim, grantRole handles the revoke of the previous admin internally
1267+
let receipt = access_control
1268+
.grantRole(admin_role, new_admin)
1269+
.send()
1270+
.await?
1271+
.get_receipt()
1272+
.await?;
1273+
1274+
let tx_hash = receipt.transaction_hash;
1275+
tracing::info!(%receipt.gas_used, %tx_hash, "admin role granted");
1276+
Ok(receipt)
1277+
}
1278+
12681279
/// helper function to decide if the contract at given address `addr` is a proxy contract
12691280
pub async fn is_proxy_contract(provider: impl Provider, addr: Address) -> Result<bool> {
12701281
// when the implementation address is not equal to zero, it's a proxy
@@ -3468,4 +3479,115 @@ mod tests {
34683479

34693480
Ok(())
34703481
}
3482+
3483+
#[test_log::test(tokio::test)]
3484+
async fn test_grant_admin_role_reward_claim() -> Result<()> {
3485+
let (_anvil, provider, l1_client) =
3486+
ProviderBuilder::new().connect_anvil_with_l1_client()?;
3487+
3488+
let mut contracts = Contracts::new();
3489+
let deployer = l1_client.get_accounts().await?[0];
3490+
let new_admin = Address::random();
3491+
3492+
// Deploy RewardClaim
3493+
let esp_token_addr = deploy_token_proxy(
3494+
&provider,
3495+
&mut contracts,
3496+
deployer,
3497+
deployer,
3498+
U256::from(10_000_000u64),
3499+
"Test Token",
3500+
"TEST",
3501+
)
3502+
.await?;
3503+
let lc_addr = deploy_light_client_contract(&provider, &mut contracts, false).await?;
3504+
let reward_claim_addr = deploy_reward_claim_proxy(
3505+
&provider,
3506+
&mut contracts,
3507+
esp_token_addr,
3508+
lc_addr,
3509+
deployer,
3510+
deployer, // pauser
3511+
)
3512+
.await?;
3513+
3514+
// Verify initial admin
3515+
let reward_claim = RewardClaim::new(reward_claim_addr, &provider);
3516+
let admin_role = reward_claim.DEFAULT_ADMIN_ROLE().call().await?;
3517+
assert!(reward_claim.hasRole(admin_role, deployer).call().await?);
3518+
assert!(!reward_claim.hasRole(admin_role, new_admin).call().await?);
3519+
3520+
// Grant admin role
3521+
let receipt = grant_admin_role(
3522+
&provider,
3523+
Contract::RewardClaimProxy,
3524+
reward_claim_addr,
3525+
new_admin,
3526+
)
3527+
.await?;
3528+
3529+
assert!(receipt.inner.is_success());
3530+
3531+
// Verify new admin has role and old admin doesn't
3532+
assert!(reward_claim.hasRole(admin_role, new_admin).call().await?);
3533+
assert!(!reward_claim.hasRole(admin_role, deployer).call().await?);
3534+
3535+
Ok(())
3536+
}
3537+
3538+
#[test_log::test(tokio::test)]
3539+
async fn test_transfer_ownership_from_eoa_reward_claim_routes_to_grant_role() -> Result<()> {
3540+
let (anvil, provider, l1_client) = ProviderBuilder::new().connect_anvil_with_l1_client()?;
3541+
3542+
let mut contracts = Contracts::new();
3543+
let deployer = l1_client.get_accounts().await?[0];
3544+
let new_admin = Address::random();
3545+
3546+
// Deploy RewardClaim
3547+
let esp_token_addr = deploy_token_proxy(
3548+
&provider,
3549+
&mut contracts,
3550+
deployer,
3551+
deployer,
3552+
U256::from(10_000_000u64),
3553+
"Test Token",
3554+
"TEST",
3555+
)
3556+
.await?;
3557+
let lc_addr = deploy_light_client_contract(&provider, &mut contracts, false).await?;
3558+
let reward_claim_addr = deploy_reward_claim_proxy(
3559+
&provider,
3560+
&mut contracts,
3561+
esp_token_addr,
3562+
lc_addr,
3563+
deployer,
3564+
deployer, // pauser
3565+
)
3566+
.await?;
3567+
3568+
// Verify initial admin
3569+
let reward_claim = RewardClaim::new(reward_claim_addr, &provider);
3570+
let admin_role = reward_claim.DEFAULT_ADMIN_ROLE().call().await?;
3571+
assert!(reward_claim.hasRole(admin_role, deployer).call().await?);
3572+
assert!(!reward_claim.hasRole(admin_role, new_admin).call().await?);
3573+
3574+
use builder::DeployerArgsBuilder;
3575+
3576+
let mut args_builder = DeployerArgsBuilder::default();
3577+
args_builder
3578+
.deployer(provider.clone())
3579+
.rpc_url(anvil.endpoint_url())
3580+
.transfer_ownership_from_eoa(true)
3581+
.target_contract("rewardclaim".to_string())
3582+
.transfer_ownership_new_owner(new_admin);
3583+
let args = args_builder.build()?;
3584+
3585+
args.transfer_ownership_from_eoa(&mut contracts).await?;
3586+
3587+
// Verify new admin has role and old admin doesn't
3588+
assert!(reward_claim.hasRole(admin_role, new_admin).call().await?);
3589+
assert!(!reward_claim.hasRole(admin_role, deployer).call().await?);
3590+
3591+
Ok(())
3592+
}
34713593
}

justfile

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -224,7 +224,7 @@ build-docker-images:
224224

225225
# generate rust bindings for contracts
226226
VERSIONED := "LightClient(Arbitrum)?(V\\d+)?(Mock)?|PlonkVerifier(V\\d+)?|StakeTable(V\\d+)?|EspToken(V\\d+)?|RewardClaim(V\\d+)?"
227-
EXACT := "FeeContract|ERC1967Proxy|OpsTimelock|SafeExitTimelock|OwnableUpgradeable|IRewardClaim|IPlonkVerifier"
227+
EXACT := "FeeContract|ERC1967Proxy|OpsTimelock|SafeExitTimelock|OwnableUpgradeable|AccessControlUpgradeable|IRewardClaim|IPlonkVerifier"
228228
REGEXP := "^(" + VERSIONED + "|" + EXACT + ")$"
229229
gen-bindings:
230230
# Update the git submodules
@@ -248,7 +248,7 @@ gen-bindings:
248248
export-contract-abis:
249249
rm -rv contracts/artifacts/abi
250250
mkdir -p contracts/artifacts/abi
251-
for contract in LightClient{,Mock,V2{,Mock}} StakeTable{,V2} EspToken IRewardClaim; do \
251+
for contract in LightClient{,Mock,V2{,Mock}} StakeTable{,V2} EspToken{,V2} IRewardClaim; do \
252252
cat "contracts/out/${contract}.sol/${contract}.json" | jq .abi > "contracts/artifacts/abi/${contract}.json"; \
253253
done
254254

0 commit comments

Comments
 (0)