Description
Remove an undocumented ordering constraint that can deadlock a pending upgrade.
Problem Statement
approve_upgrade rejects any approval whose address does not sort strictly after the last recorded one (src/contracts/upgrade.rs:236-240, and identically at 153-157):
if let Some(previous) = approvals.last() {
if previous >= signer {
return Err(ContractError::InvalidUpgradeConfig);
}
}
Duplicate approvals are already rejected two lines above by approvals.contains(signer.clone()), so this adds nothing except a hidden constraint on the order signers happen to transact in.
Verified against current main: with signers [A, B, C] sorted ascending and threshold 2, if B proposes first, A's subsequent approve_upgrade returns Err(InvalidUpgradeConfig) — and there is no way to withdraw B's approval, so the proposal can only ever be completed by C. With threshold equal to the signer count, a single out-of-order proposal deadlocks the upgrade until an admin calls cancel_upgrade.
It's also undocumented: the # Errors list at src/contracts/upgrade.rs:203-206 names only NotUpgradeSigner, NoPendingUpgrade, and AlreadyApproved. Every test in tests/upgrade.rs approves in ascending order, so the case is untested.
Proposed Changes
Technical Implementation Scaffolding
- Target Repository: vero-core-contracts
- Target Path: src/contracts/upgrade.rs
- Branch Naming: fix/issue--upgrade-approval-ordering
- Authority Context: Security-sensitive — contract upgrade governance
Acceptance Criteria
Definition of Done
Description
Remove an undocumented ordering constraint that can deadlock a pending upgrade.
Problem Statement
approve_upgraderejects any approval whose address does not sort strictly after the last recorded one (src/contracts/upgrade.rs:236-240, and identically at153-157):Duplicate approvals are already rejected two lines above by
approvals.contains(signer.clone()), so this adds nothing except a hidden constraint on the order signers happen to transact in.Verified against current
main: with signers[A, B, C]sorted ascending and threshold 2, ifBproposes first,A's subsequentapprove_upgradereturnsErr(InvalidUpgradeConfig)— and there is no way to withdrawB's approval, so the proposal can only ever be completed byC. With threshold equal to the signer count, a single out-of-order proposal deadlocks the upgrade until an admin callscancel_upgrade.It's also undocumented: the
# Errorslist atsrc/contracts/upgrade.rs:203-206names onlyNotUpgradeSigner,NoPendingUpgrade, andAlreadyApproved. Every test intests/upgrade.rsapproves in ascending order, so the case is untested.Proposed Changes
previous >= signercheck from bothapprove_upgradeandpropose_upgrade; rely onapprovals.contains(...)for dedupis_strictly_sorted_addresseson theset_upgrade_signersconfiguration list (src/contracts/upgrade.rs:63) — canonical ordering is meaningful thereTechnical Implementation Scaffolding
Acceptance Criteria
[A, B, C]and threshold 2,propose_upgrade(B)followed byapprove_upgrade(A)succeedsAlreadyApprovedexecute_upgradesucceeds in eachDefinition of Done