PerpetualManager - Hedging Agents Positions
Angle's Hedging Agents contracts

1. Introduction

PerpetualManager is the contract handling all the Hedging Agents perpetuals and positions. There is one PerpetualManager contract per collateral/stablecoin pair, meaning that every time a new collateral is deployed for a given stablecoin, a new PerpetualManager contract should be deployed.
Hedging Agents can open or close their perpetuals directly from this contract. Angle also allows HAs to modify their perpetuals by adding/removing collateral from margin without having to close and open a new perpetual.
Keepers can interact with this contract to liquidate unhealthy perpetuals (below the maintenance margin) or to cash out some when there are too many HAs in the protocol.
Besides, PerpetualManager has all the fees logic for Hedging Agents' perpetual futures: fees are computed when Hedging Agents go in the protocol and when they exit the protocol. It is important to note that there is no funding rate for Hedging Agents at Angle.
Perpetuals are NFTs on the ERC721Metadata standard. They are non fungible, but they can be transferred from one address to another. It is possible to interact directly with the PerpetualManager contract for the part that concerns this NFT logic: HAs can transfer their perpetuals from one address to another, approve other addresses to use their perpetuals or simply check the number of perpetuals they own.
When a HA opens a perpetual, if governance token distribution is activated, then HAs can automatically stake and receive governance tokens from the position they have. The PerpetualManager contract also works like a staking contract, that allows HAs to receive rewards depending on the amount of collateral cover.

2. Contract Details


Implements ReentrancyGuardUpgradeable, IPerpetualManagerFront, IPerpetualManager, IStakingRewards, IAccessControl, IERC721Metadata, IERC721, IERC165, PausableUpgradeable, .
The contract is upgradeable.
This means that the reference to a PerpetualManager contract in another contract (like a PoolManager contract) will never change even though the code in the contract may change.


Besides the classical staking variables like rewardRate , rewardsDuration or rewardPerTokenStored , the contract involves many other variables. For contract size reasons, some variables had to be stored as internal variables.

Perpetual Data

In each perpetual, we store 4 different variables
  • entryRate: Oracle value at the moment of perpetual creation
  • entryTimestamp: Timestamp at which the perpetual was created
  • margin: Amount initially brought in the perpetual (net from fees) + amount added - amount removed from it. This is the only element that can be modified in the perpetual after its creation
  • committedAmount: Amount hedged by the perpetual. This cannot be modified once the perpetual is created

Perpetual Variables

  • totalHedgeAmount: Total amount of stablecoins that are insured (i.e. that could be redeemed against collateral thanks to HAs). When a HA opens a perpetual, it covers a fixed amount of stablecoins for the protocol, equal to the committed amount times the entry rate. totalHedgeAmount is the sum of all these hedged amounts
  • _perpetualIDcount: Counter to generate a unique perpetual ID for each perpetual

Immutable References to Other Contracts

  • _stableMaster: Address of the StableMasterreference
  • _token: Interface for the underlying token accepted by this contract
  • poolManager : PoolManager handling the collateral
  • rewardToken: Interface for the token distributed as a staking reward

Mutable References

The following references can be modified by the StableMaster contract:
  • _oracle: Oracle to give the rate feed that is the price of the collateral with respect to the price of the stablecoin
  • _feeManager:Keeper address allowed to update the fees for this contract

Fees and Other Parameters

  • targetHAHedge: Target proportion of stablecoins issued using this collateral to insure with HAs. Above this proportion of coverage (or hedge), HAs cannot open new perpetuals. When keepers are forcing the cash out of some perpetuals, they are incentivized to bringing the hedge ratio to this proportion
  • limitHAHedge: Limit proportion of stablecoins issued using this collateral that HAs can insure. Above this ratio forceClosePerpetuals is activated and anyone can see its perpetual cashed out
  • maxLeverage: Maximum leverage multiplier authorized for HAs
  • maintenanceMargin: Maintenance Margin (in BASE_PARAMS) for each perpetual. If the marginRatio of a perpetual is below maintenanceMargin: then the perpetual can be liquidated
  • lockTime: Amount of time before a HA is allowed to withdraw funds from its perpetual (through cash out or remove)
  • xHAFeesDeposit: Thresholds for the ratio between to amount hedged and the amount to hedge. The bigger the ratio the bigger the fees will be because this means that the max amount to insure is soon to be reached
  • yHAFeesDeposit: Deposit fees at threshold values
  • xHAFeesWithdraw: Thresholds for the hedge ratio
  • yHAFeesDeposit: Withdraw fees at threshold values
  • haBonusMalusDeposit: Extra parameter from the FeeManager contract that is multiplied to the fees from above and that can be used to change deposit fees. It works as a bonus - malus fee, if haBonusMalusDeposit > BASE_PARAMS, then the fee will be larger than haFeesDeposit, if haBonusMalusDeposit < BASE_PARAMS, fees will be smaller. This parameter, updated by keepers in the FeeManager contract, could most likely depend on the collateral ratio
  • haBonusMalusWithdraw: Extra parameter from the keeper that is multiplied to the fees from above and that can be used to change withdraw fees

Keeper Fees

Keepers interacting with the protocol to liquidate unhealthy perpetuals or cash out some positions when there are too many HAs have their own set of fees and incentives, encoded by the following parameters:
  • keeperFeesLiquidationRatio: Portion of the fees that go to keepers liquidating HA perpetuals
  • keeperFeesLiquidationCap: Cap on the fees that go to keepers liquidating a perpetual
  • keeperFeesClosingCap: Cap on the fees that go to keepers cashing out perpetuals when too much collateral is hedged by HAs (hedge ratio above limitHAHedge)
  • xKeeperFeesClosing: Thresholds on the values of the rate between the current hedged amount (totalHedgeAmount) and the target hedged amount by HAs (targetCoveredAmount) divided by 2. A value of 0.5 corresponds to a hedge ratio of 1. Doing this allows to maintain an array with values of x inferior to BASE_PARAMS.
  • yKeeperFeesClosing: Values at thresholds of the proportions of the fees that should go to keepers cashing out perpetuals because there were too many HAs in the protocol


  • perpetualData: Mapping from a perpetual ID to the struct with the data of the perpetual (detailed above)
  • _owners: Mapping from perpetual ID to owner address
  • _balances: Mapping from owner address to perpetual owned count
There are then other mappings associated to the ERC721 structure of perpetuals like approval mappings: _operatorApprovals, _perpetualApprovals.

Access Control

  • POOLMANAGER_ROLE: this role is admin of all roles, meaning changes in other roles will take place from the PoolManager. This allows the manager contract corresponding to the contract to for instance revoke the GUARDIAN_ROLE without having to call a specific function in this contract
  • GUARDIAN_ROLE : there is no GOVERNOR_ROLE in this contract, the governor addresses are all guardian
The following roles are not formally defined but still used in modifiers of the contract:
  • FEEMANAGER_ROLE: some parameters need to be updated by the FeeManager contract.
  • STABLEMASTER_ROLE: only the StableMaster is allowed to change the reference to the Oracle contract

3. Key Mechanisms & Concepts

EIP721 Methods

All standard EIP721 methods are implemented, such as balanceOf(), ownerOf(), approve(), transferFrom, etc.
The implementation of these methods is derived from OpenZeppelin's library.

Perpetual Interaction Methods

The following functions are the ones that allow HAs to directly to create, modify or cash out perpetuals. For an existing perpetual, it is possible to have an address interacting with the perpetual that is not the owner of the perpetual: this address needs to have been approved for the concerned perpetual. For these cases: msg.sender should be the owner of the concerned perpetual or be approved for this perpetual.


function openPerpetual(address owner, uint256 amountBrought, uint256 amountCommitted, uint256 maxOracleRate, uint256 minNetMargin) external returns(uint256 perpetualID);
Lets a HA join the protocol. It is with this function that HAs can create their vaults and specify the leverage they want.
This function checks if a perpetual can be created under the conditions chosen by the HA. For instance, it checks if the ratio between the committed amount and the brought amount is not bigger than the maxLeverage parameter.
This function records all the details of the perpetual created by the HA, for instance the block.timestamp of perpetual creation as well as the oracle value at this time are also stored. It also computes the fees induced for creating the perpetual.
It is possible to create a perpetual on behalf of someone else using this function, yet, the future owner of the perpetual cannot be the zero address.
  • owner: Address of the future owner of the perpetual
  • amountBrought: Amount of collateral brought by the HA
  • amountCommitted: Amount of collateral hedged by the HA
  • maxOracleRate: Maximum oracle value that the HA wants to see stored in the perpetual. This parameter serves as a protection against oracle manipulations for HAs opening perpetuals
  • minNetMargin: Minimum net margin that the HA is willing to see stored in the perpetual
Return Value:
  • perpetualID The ID of the perpetual opened by this HA


function closePerpetual(uint256 perpetualID, address to, uint256 minCashOutAmount) external;
Lets a HA cash out a perpetual owned or controlled for the stablecoin/collateral pair associated to this PerpetualManager contract. The HA gets the current amount of her position depending on the entry oracle value and current oracle value. It is at the level of this function that HAs can exit the protocol.
If the PoolManager does not have enough collateral, the perpetual owner will be converted to a SLP.
  • perpetualID: ID of the perpetual to cash out
  • to: Address which will receive the proceeds from this perpetual
  • minCashOutAmount: Minimum net cash out amount that the HA is willing to get for cashing out the perpetual. It serves as a protection for HAs cashing out their perpetuals: it protects them both from fees that would have become too high and from a too big decrease in oracle value.


function addToPerpetual(uint256 perpetualID, uint256 amount) external;
Lets a HA increase the margin in a perpetual she controls for this stablecoin/collateral pair. Calling this function has the effect of decreasing the leverage multiple of this perpetual. If this perpetual is to be liquidated, the HA is not going to be able to add liquidity to it.
  • perpetualID: ID of the perpetual to which amount should be added to cashOutAmount
  • amount: Amount to add to the perpetual's cashOutAmount


function removeFromPerpetual(uint256 perpetualID, uint256 amount, address to) external;
Lets a HA decrease the margin in a perpetual she controls for this stablecoin/collateral pair. This increases the leverage multiple of this perpetual.
  • perpetualID: ID of the perpetual from which collateral should be removed
  • amount: Amount to remove from the perpetual's cashOutAmount
  • to: Address which will receive the collateral removed from this perpetual

Keeper Functions


function liquidatePerpetuals(uint256[] memory perpetualIDs) external;
Allows an outside caller to liquidate perpetuals if their margin ratio is under the maintenance margin. The outside caller (namely a keeper) gets a portion of the leftover cash out amount of the perpetual
As keepers may directly profit from this function, there may be front-running problems with miners bots, we may have to put an access control logic for this function to only allow white-listed addresses to act as keepers for the protocol.
  • perpetualIDs: ID of the targeted perpetuals


function forceClosePerpetuals(uint256[] memory perpetualIDs) external;
Allows an outside caller to cash out perpetuals if too much of the collateral from users is hedged by HAs. This function allows to make sure that the protocol will not have too much HAs for a long period of time.
A HA that owns a targeted perpetual will get the current value of her perpetual
In this case too, as keepers may directly profit from this function, there may be front-running problems with miners bots. We may have to put an access control logic for this function to only allow white-listed addresses to act as keepers for the protocol.
  • perpetualIDs: ID of the targeted perpetuals

Staking Methods


function getReward(uint256 perpetualID) public;
Allows a perpetual owner to withdraw rewards. Only an approved caller can claim the rewards for the perpetual with perpetualID.
When a HA cashes out a perpetual or gets liquidated, this function is automatically called, meaning rewards are automatically accumulated and given to HAs.
  • perpetualID: ID of the perpetual which accumulated tokens

External View Functions


function getCashOutAmount(uint256 perpetualID, uint256 rate) external returns(uint256, uint256);
Returns the cashOutAmount of the perpetual owned by someone at a given oracle value. This function is used by the Collateral Settlement contract.
  • perpetualID: ID of the perpetual
  • rate: Oracle value
Return Values:
  • The cashOutAmount of the perpetual
  • Whether the position of the perpetual is now too small compared with its initial position


function earned(uint256 perpetualID) external returns (uint256);
Allows to check the amount of governance tokens earned by a perpetual

Staking Functions - Rewards Distribution Functions


function notifyRewardAmount(uint256 reward) external;
Notifies the contract that rewards (often governance tokens) are going to be shared among HAs for a length equal to rewardsDuration which is a protocol parameter. Only the reward distributor contract is allowed to call this function which starts a staking cycle.
  • reward: Amount of governance tokens to be distributed to HAs


function setNewRewardsDistribution(address _rewardsDistribution) external;
Changes the rewardsDistribution associated to this contract. This function is part of the staking rewards interface and it is used to propagate a change of rewards distributor which handles all the staking at the scale of the contract.

FeeManager Function


function setFeeKeeper(uint64 feeDeposit, uint64 feesWithdraw) external;
Updates all the fees not depending on individual HA conditions and on the hedge curve by HAs via keeper utils functions. Governance may decide to incorporate a collateral ratio dependence in the entry and exit fees for HAs, in this case it will be done through keepers and hence through this function.
  • feeDeposit: New deposit global fees
  • feeWithdraw: New withdraw global fees

Guardian Functions - Staking and Pauses

The following functions can only be called by the guardian or by a governor address


function pause() external;
Pauses the getReward method as well as the functions allowing to create, modify or cash-out perpetuals. After calling this function, it is going to be impossible for HAs to interact with their perpetuals or claim their rewards on it.


function unpause() external;
Unpauses paused HAs functions.


function setRewardDistribution(uint256 _rewardsDuration, address _rewardsDistribution) external;
Sets the conditions and specifies the duration of the reward distribution. It allows governance to directly change the rewards distribution contract and the conditions at which this distribution is done. The compatibility of the reward token is not checked here: it is checked in the rewards distribution contract when activating this as a staking contract, so if a reward distributor is set here but does not have a compatible reward token, then this reward distributor will not be able to set this contract as a staking contract.
  • _rewardsDuration: Duration for the rewards for this contract
  • _rewardsDistribution: Address which will give the reward tokens

Guardian Functions - Parameters and Setters

Governance has many functions it can call to change parameters. These include:
  • setLockTime: Sets lockTime that is the minimum amount of time HAs have to stay within the protocol. This parameter is used to prevent HAs from exiting after a certain amount of time, for instance if they have an insider information about the evolution of the price of a given collateral.
  • setBoundsPerpetual: Changes the maximum leverage authorized (commit/margin) and the maintenance margin under which perpetuals can be liquidated.
  • setHAFees: Sets xHAFees that is the thresholds of values of the hedge ratio as well as yHAFees that is the value of the deposit or withdraw fees at threshold. For deposit fees, the lower the x (that is the hedge ratio), the lower y should be. For withdraw fees, evolution should follow an opposite logic.
  • setTargetAndLimitHAHedge: Sets the target and limit proportions of collateral from users that can be insured by HAs. targetHAHedge equal to BASE_PARAMS means that all the collateral from users should be insured by HAs. targetHAHedge equal to 0 means HA should not cover anything.
  • setKeeperFeesRatio: Sets the proportion of fees going to the keepers when liquidating a HA perpetual. This proportion should be inferior to BASE
  • setKeeperFeesCap: Sets the maximum amounts going to the keepers when cashing out perpetuals because too much was hedged by HAs or when liquidating a perpetual.
  • setKeeperFeesClosing: Sets the x-array (ie thresholds) for FeeManager when cashing out perpetuals and the y-array that is the value of the proportions of the fees going to keepers cashing out perpetuals. The x thresholds correspond to values of the hedge ratio divided by two.

PoolManager Function


function setFeeManager(IFeeManager feeManager_) external;
Changes the reference to the FeeManager contract. This allows the PoolManager contract to propagate changes to the PerpetualManager.This is the only place where the _feeManager can be changed, it is as if there was a FEEMANAGER_ROLE for which PoolManager was the admin.

StableMaster Function


function setOracle(contract IOracle _oracle) external;
Changes the oracle contract used to compute collateral price with respect to the stablecoin's price. The value of the oracle can directly be set by the StableMaster.