Currently, mandatory upgrades  are implemented by updated clients changing semantics on a hard coded block number.
This has the disadvantage that non-updated clients aren't aware of the upgrade taking place, rather they eventually see all delegates signing off on a malformed chain when the new semantics eventually allow a previously invalid transaction.
I propose allowing delegates to publish the following transaction types:
wallet_delegate_propose_upgrade upgrade_name upgrade_id upgrade_quorum proposal_expiration_block upgrade_effective_block
upgrade_id is the hash of a JSON object hard-coded in updated clients, specifying the upgrade's parameters.
The upgrade process then looks like this:
- Developers create code to implement new semantics
- Developers create upgrade_id
- upgrade_quorum is the number of delegates which need to sign the upgrade (usually 51)
- After proposal_expiration_block passes, wallet_delegate_support_upgrade operations on this upgrade_id become invalid
- upgrade_effective_block is the block when the new semantics go into effect
Currently checking whether an upgrade is enabled is done by statements like this:
if( pending_block_num == BTS_V0_4_17_FORK_BLOCK_NUM )
if( pending_block_num > BTS_V0_4_21_FORK_BLOCK_NUM )
The hardcoded constant BTS_Vx_y_z_FORK_BLOCK_NUM will simply be replaced by get_upgrade_effective_block(upgrade_id).
The get_upgrade_effective_block() function will be the upgrade_effective_block if at least upgrade_quorum delegates issued wallet_delegate_support_upgrade transactions before the proposal_expiration_block.
 "Mandatory upgrade" is preferred to "hardfork" for marketing reasons.