MultistrategyLockedVault
Inherits: MultistrategyVault, IMultistrategyLockedVault
Author: Golem Foundation
A locked vault with custody-based rage quit mechanism and two-step cooldown period changes
*This vault implements a secure custody system that prevents rage quit cooldown bypass attacks and provides user protection through a two-step governance process for cooldown period changes.
Custody Mechanism:
- Share Locking During Rage Quit:
- Users must initiate rage quit for a specific number of shares
- Those shares are placed in custody and cannot be transferred
- Locked shares are tracked separately from the user's transferable balance
- Transfer restrictions prevent bypassing the cooldown period
- Only one active rage quit per user (no multiple concurrent rage quits)
- Custody Lifecycle:
- Initiation: User specifies exact number of shares to lock for rage quit
- Cooldown: Shares remain locked and non-transferable during cooldown period
- Unlock: After cooldown, user can withdraw/redeem up to their custodied amount
- Withdrawal: Users can make multiple withdrawals from the same custody
- Completion: Custody is cleared when all locked shares are withdrawn
- Transfer Restrictions:
- Users cannot transfer locked shares to other addresses
- Available shares = total balance - locked shares
- Prevents rage quit cooldown bypass through share transfers
- Use
getTransferableShares()to check available balance for transfers
- Withdrawal Rules:
- Users can only withdraw shares if they have active custody
- Withdrawal amount cannot exceed remaining custodied shares
- Multiple partial withdrawals are allowed from the same custody
- New rage quit required after custody is fully withdrawn
maxWithdraw()andmaxRedeem()return 0 if no custody or still in cooldown
- Utility Functions:
getTransferableShares(user): Returns shares available for transfergetRageQuitableShares(user): Returns shares available for rage quit initiationcustodyInfo(user): Returns custody details (locked shares, unlock time)
Two-Step Cooldown Period Changes:
- Grace Period Protection:
- Governance proposes cooldown period changes with 14-day delay
- Users can rage quit under current terms during grace period
- Protects users from unfavorable governance decisions
- Change Process:
- Propose: Governance proposes new period, starts grace period
- Grace Period: 14 days for users to exit under current terms
- Finalize: Anyone can finalize change after grace period
- Cancel: Governance can cancel during grace period
- User Protection:
- Users who rage quit before finalization use old cooldown period
- Users who rage quit after finalization use new cooldown period
- No retroactive application of cooldown changes
Governance:
- Regen Governance: Has control over rage quit cooldown period changes
- Direct Transfer: Governance can be transferred immediately (no 2-step process)
- Access Control: Only regen governance can propose/cancel cooldown changes
Example Scenarios:
Scenario A - Basic Custody Flow:**
- User has 1000 shares, initiates rage quit for 500 shares
- 500 shares locked in custody, 500 shares remain transferable
getTransferableShares(user)returns 500,getRageQuitableShares(user)returns 0- After cooldown, user can withdraw up to 500 shares
- User withdraws 300 shares, 200 shares remain in custody
- User can later withdraw remaining 200 shares without new rage quit Scenario B - Two-Step Cooldown Change:**
- Current cooldown: 7 days, governance proposes 14 days
- Grace period: Users have 14 days to rage quit under 7-day terms
- User A rage quits during grace period → uses 7-day cooldown
- Change finalized after grace period
- User B rage quits after finalization → uses 14-day cooldown Scenario C - Utility Function Usage:**
- User has 1000 shares, no active rage quit
getTransferableShares(user)returns 1000getRageQuitableShares(user)returns 1000- User initiates rage quit for 400 shares
getTransferableShares(user)returns 600getRageQuitableShares(user)returns 0 (already has active rage quit)*
Note: security-contact: [email protected]
State Variables
custodyInfo
Mapping of user addresses to their custody information
Tracks locked shares and unlock timestamp for each user's rage quit Only one active custody per user (no concurrent rage quits)
mapping(address => CustodyInfo) public custodyInfo;
regenGovernance
Address of regen governance controlling rage quit parameters
Can propose/cancel rage quit cooldown period changes Set during initialize() to the roleManager address
address public regenGovernance;
rageQuitCooldownPeriod
Current active cooldown period for rage quits
In seconds. Applied to new rage quits when initiateRageQuit() is called Can be changed via two-step process (propose + finalize after grace period)
uint256 public rageQuitCooldownPeriod;
pendingRageQuitCooldownPeriod
Pending new cooldown period awaiting finalization
Set to 0 when no change is pending. Non-zero indicates active proposal Requires RAGE_QUIT_COOLDOWN_CHANGE_DELAY to elapse before finalization
uint256 public pendingRageQuitCooldownPeriod;
rageQuitCooldownPeriodChangeTimestamp
Timestamp when current cooldown period change was proposed
Unix timestamp in seconds. Used to calculate grace period expiration Set to 0 when no change is pending
uint256 public rageQuitCooldownPeriodChangeTimestamp;
INITIAL_RAGE_QUIT_COOLDOWN_PERIOD
Initial rage quit cooldown period set at deployment
7 days in seconds. Applied until governance changes it
uint256 public constant INITIAL_RAGE_QUIT_COOLDOWN_PERIOD = 7 days;
RANGE_MINIMUM_RAGE_QUIT_COOLDOWN_PERIOD
Minimum allowed rage quit cooldown period
1 day in seconds. Prevents cooldown from being set too short
uint256 public constant RANGE_MINIMUM_RAGE_QUIT_COOLDOWN_PERIOD = 1 days;
RANGE_MAXIMUM_RAGE_QUIT_COOLDOWN_PERIOD
Maximum allowed rage quit cooldown period
30 days in seconds. Prevents cooldown from being set too long
uint256 public constant RANGE_MAXIMUM_RAGE_QUIT_COOLDOWN_PERIOD = 30 days;
RAGE_QUIT_COOLDOWN_CHANGE_DELAY
Grace period delay for cooldown changes
14 days in seconds. Users have this time to rage quit under old terms
uint256 public constant RAGE_QUIT_COOLDOWN_CHANGE_DELAY = 14 days;
Functions
onlyRegenGovernance
Modifier to restrict access to regen governance only
Note: modifier: Reverts with NotRegenGovernance if caller is not regen governance
modifier onlyRegenGovernance();
initialize
Initializes the locked vault with custody mechanism
*Extends MultistrategyVault.initialize() with custody features INITIALIZATION:
- Sets initial rage quit cooldown to INITIAL_RAGE_QUIT_COOLDOWN_PERIOD (7 days)
- Calls parent initialize() for standard vault setup
- Sets _roleManager as regenGovernance address DUAL ROLE:
- _roleManager becomes both roleManager AND regenGovernance
- roleManager: Controls vault roles (from parent)
- regenGovernance: Controls rage quit parameters (this contract)*
Note: security: Can only be called once per deployment
function initialize(
address _asset,
string memory _name,
string memory _symbol,
address _roleManager,
uint256 _profitMaxUnlockTime
) public override(MultistrategyVault, IMultistrategyVault);
Parameters
| Name | Type | Description |
|---|---|---|
_asset | address | Address of underlying asset token (cannot be zero) |
_name | string | Human-readable vault token name |
_symbol | string | Vault token symbol ticker |
_roleManager | address | Address for role management AND regen governance |
_profitMaxUnlockTime | uint256 | Profit unlock duration in seconds (0-31556952) |
proposeRageQuitCooldownPeriodChange
Proposes a new rage quit cooldown period (step 1 of 2)
*Initiates two-step change process with grace period for user protection VALIDATION:
- Must be within valid range (1-30 days)
- Must differ from current cooldown period PROCESS:
- Validates new period is within allowed range
- Sets pendingRageQuitCooldownPeriod
- Records proposal timestamp
- Users have 14 days to rage quit under current terms
- After 14 days, anyone can finalize the change*
Notes:
-
security: Only callable by regenGovernance
-
security: 14-day grace period protects users from unfavorable changes
function proposeRageQuitCooldownPeriodChange(uint256 _rageQuitCooldownPeriod) external onlyRegenGovernance;
Parameters
| Name | Type | Description |
|---|---|---|
_rageQuitCooldownPeriod | uint256 | New cooldown period in seconds (86400-2592000, i.e., 1-30 days) |
finalizeRageQuitCooldownPeriodChange
Finalizes the rage quit cooldown period change (step 2 of 2)
*Permissionless - anyone can call after grace period expires REQUIREMENTS:
- Must have pending change (pendingRageQuitCooldownPeriod != 0)
- Grace period (14 days) must have elapsed EFFECTS:
- Updates rageQuitCooldownPeriod to pending value
- Clears pending state
- New rage quits use new cooldown period immediately*
function finalizeRageQuitCooldownPeriodChange() external;
cancelRageQuitCooldownPeriodChange
Cancels a pending rage quit cooldown period change
*Only callable during grace period (before finalization) REQUIREMENTS:
- Must have pending change
- Grace period must NOT have elapsed yet EFFECTS:
- Clears all pending change state
- Current cooldown period remains unchanged*
Notes:
-
security: Only callable by regenGovernance
-
security: Cannot cancel after grace period expires
function cancelRageQuitCooldownPeriodChange() external onlyRegenGovernance;
initiateRageQuit
Initiates rage quit by locking shares in custody
*Creates custody entry with current cooldown period REQUIREMENTS:
- shares > 0
- shares <= user's balance
- User must NOT have existing active custody EFFECTS:
- Locks specified shares (become non-transferable)
- Sets unlock time = current timestamp + rageQuitCooldownPeriod
- User can withdraw after unlock time IMPORTANT:
- Uses CURRENT cooldown period (not pending)
- Locked shares cannot be transferred
- Only one custody per user at a time*
Notes:
-
security: Reentrancy protected
-
security: Prevents cooldown bypass via transfers
function initiateRageQuit(uint256 shares) external nonReentrant;
Parameters
| Name | Type | Description |
|---|---|---|
shares | uint256 | Number of shares to lock for rage quit |
cancelRageQuit
Cancels rage quit and releases custodied shares
*Clears custody, making all shares transferable again REQUIREMENTS:
- Must have active custody (lockedShares > 0) EFFECTS:
- Deletes entire custody entry
- All shares become transferable
- User can initiate new rage quit if desired*
function cancelRageQuit() external;
withdraw
Override withdrawal functions to handle custodied shares
function withdraw(uint256 assets, address receiver, address owner, uint256 maxLoss, address[] calldata strategiesArray)
public
override(MultistrategyVault, IMultistrategyVault)
nonReentrant
returns (uint256);
redeem
Override redeem function to handle custodied shares
function redeem(uint256 shares, address receiver, address owner, uint256 maxLoss, address[] calldata strategiesArray)
public
override(MultistrategyVault, IMultistrategyVault)
nonReentrant
returns (uint256);
setRegenGovernance
Set the regen governance address that can manage rage quit parameters
*Regen governance has exclusive control over:
- Proposing rage quit cooldown period changes
- Cancelling pending cooldown period changes*
Note: governance: Only current regen governance can call this function
function setRegenGovernance(address _regenGovernance) external override onlyRegenGovernance;
Parameters
| Name | Type | Description |
|---|---|---|
_regenGovernance | address | New address to become regen governance |
_processCustodyWithdrawal
Process withdrawal of shares from custody during withdraw/redeem operations
*Internal function that enforces custody withdrawal rules:
- Owner must have active custody (lockedShares > 0)
- Shares must still be locked (current time < unlockTime)
- Withdrawal amount cannot exceed remaining custodied shares
- Updates custody state by reducing locked shares
- Clears custody when all locked shares are withdrawn*
Note: security: Prevents unauthorized withdrawals and custody bypass
function _processCustodyWithdrawal(address owner, uint256 shares) internal;
Parameters
| Name | Type | Description |
|---|---|---|
owner | address | Address of the share owner attempting withdrawal |
shares | uint256 | Number of shares being withdrawn/redeemed |
_transfer
Override ERC20 transfer to enforce custody transfer restrictions
*Implements custody-based transfer restrictions:
- Calculates available shares (total balance - locked shares)
- Prevents transfer if amount exceeds available shares
- Allows normal transfers for non-custodied shares
- Critical security feature preventing rage quit cooldown bypass*
Note: security: Prevents users from bypassing cooldown by transferring locked shares
function _transfer(address sender_, address receiver_, uint256 amount_) internal override;
Parameters
| Name | Type | Description |
|---|---|---|
sender_ | address | Address attempting to send shares |
receiver_ | address | Address that would receive shares |
amount_ | uint256 | Number of shares being transferred |
maxWithdraw
Get the maximum amount of assets that can be withdrawn by an owner
*This override accounts for custody constraints - returns 0 if:
- Custody is still in cooldown period
- Otherwise returns min of parent calculation and custodied shares in asset terms*
function maxWithdraw(address owner_, uint256 maxLoss_, address[] calldata strategiesArray_)
external
view
override(MultistrategyVault, IMultistrategyVault)
returns (uint256);
Parameters
| Name | Type | Description |
|---|---|---|
owner_ | address | Address owning shares to check withdrawal limits for |
maxLoss_ | uint256 | Custom max_loss if any |
strategiesArray_ | address[] | Custom strategies queue if any |
Returns
| Name | Type | Description |
|---|---|---|
<none> | uint256 | Maximum amount of assets withdrawable |
maxRedeem
Get the maximum amount of shares that can be redeemed by an owner
*This override accounts for custody constraints - returns 0 if:
- Custody is still in cooldown period
- Otherwise returns min of balance and custodied shares*
function maxRedeem(address owner_, uint256 maxLoss_, address[] calldata strategiesArray_)
external
view
override(MultistrategyVault, IMultistrategyVault)
returns (uint256);
Parameters
| Name | Type | Description |
|---|---|---|
owner_ | address | Address owning shares to check redemption limits for |
maxLoss_ | uint256 | Custom max_loss if any |
strategiesArray_ | address[] | Custom strategies queue if any |
Returns
| Name | Type | Description |
|---|---|---|
<none> | uint256 | Maximum amount of shares redeemable |
getTransferableShares
Get the amount of shares that can be transferred by a user
Returns total balance minus shares currently locked in custody
function getTransferableShares(address user) external view returns (uint256);
Parameters
| Name | Type | Description |
|---|---|---|
user | address | Address to check transferable shares for |
Returns
| Name | Type | Description |
|---|---|---|
<none> | uint256 | Amount of shares available for transfer (not locked in custody) |
getRageQuitableShares
Get the amount of shares available for rage quit initiation
Returns 0 if user already has active custody, otherwise returns full balance
function getRageQuitableShares(address user) external view returns (uint256);
Parameters
| Name | Type | Description |
|---|---|---|
user | address | Address to check rage quitable shares for |
Returns
| Name | Type | Description |
|---|---|---|
<none> | uint256 | Amount of shares available for initiating rage quit |