Operators and Controllers
ERC-7540 introduces two concepts that do not exist in vanilla ERC-4626: operators and controllers. This page explains how Olla implements them and how to use them in an integration.
Vocabulary
- Owner. The address that holds the stAztec being redeemed. Shares are burned from the owner's balance.
- Controller. The address against which a withdrawal request is tracked. The controller is the only address (aside from its approved operators) that can claim the finalized request.
- Operator. An address that a user has authorized to act on their behalf. An operator can call
requestRedeemwith the user asownerand can claim finalized requests whose controller is the user.
In most integrations, owner, controller, and the caller are the same address. Operators and split owner/controller roles are needed when one contract manages redemptions on behalf of another.
Approving an operator
function setOperator(address operator, bool approved) external returns (bool);
Called by the user who wants to grant or revoke operator rights. Emits OperatorSet(controller, operator, approved).
A user cannot set themselves as their own operator; the call reverts with OllaVault__InvalidParameter.
Check an existing approval:
function isOperator(address controller, address operator) external view returns (bool);
What operators can do
Given an approval by controller, an operator can:
- Call
requestRedeem(shares, controller, owner)where eithermsg.sender == owneror the operator is approved byowner. Shares are burned fromowner's balance. - Call
claimRequestById(requestId)for requests whose controller is the approving address. - Call
redeem(shares, receiver, controller)(the ERC-4626 claim alias) for the same set of requests.
Operators cannot change setOperator approvals on behalf of another user, cannot deposit on behalf of another user, and cannot call instantRedeem on behalf of another user.
Typical flows
Self-service exit (no operator)
// owner == controller == msg.sender
uint256 requestId = vault.requestRedeem(shares, msg.sender, msg.sender);
// ... wait for finalization
uint256 assets = vault.claimRequestById(requestId);
This is the common case. You do not need setOperator for this flow.
Keeper-managed exit
A user authorizes a keeper contract to enqueue and claim on their behalf:
// called once by the user
vault.setOperator(address(keeper), true);
// called by the keeper, on behalf of the user
uint256 requestId = vault.requestRedeem(shares, user, user);
// ... later, also by the keeper
uint256 assets = vault.claimRequestById(requestId);
Because controller == user, the keeper can claim the request once it is finalized. The claimed assets go to the user by default, because claimRequestById sends to the request's original recipient.
Strategy contract holding user shares
When the shares sit on a contract (a yield strategy, for example) and the end user is a different address, the strategy is both owner and controller:
// strategy holds the shares
uint256 requestId = vault.requestRedeem(shares, address(this), address(this));
// strategy claims, then forwards to the user internally
uint256 assets = vault.claimRequestById(requestId);
The strategy does not need to grant operator rights because msg.sender == owner == controller.
The requestRedeemWithPermit asymmetry
requestRedeemWithPermit looks like the permit version of requestRedeem, but its arguments are different:
function requestRedeemWithPermit(
uint256 shares,
address controller,
uint256 deadline,
uint8 v,
bytes32 r,
bytes32 s
) external returns (uint256 requestId);
There is no owner parameter. The function always treats msg.sender as the owner and pulls shares into the vault via safeTransferFrom using the permit-set allowance. This means:
- The permit variant cannot be used by an operator to redeem on behalf of another user. For operator flows, use the non-permit
requestRedeem(shares, controller, owner). - The
controllercan still differ frommsg.sender, so you can queue a request where a different address is responsible for claiming.
If your integration assumed symmetry with requestRedeem(shares, controller, owner), be explicit about the difference in your client code. The permit variant is a convenience for the common case of "user pays their own gas and exits their own shares". It is not a general replacement.
Permit frontrun protection
Every WithPermit function in the vault wraps the permit call in a try/catch. If the permit reverts (for example because another transaction already consumed the signature), the vault checks whether the allowance is already sufficient for the operation and proceeds if it is. This applies to:
depositWithPermitinstantRedeemWithPermitrequestRedeemWithPermit
Only when the permit fails and there is no prior allowance does the call revert with OllaVault__PermitFailed. You generally do not need to handle permit frontrunning in client code.
Related reading
- Redemptions
- OllaVault
- ERC-7540 specification: https://eips.ethereum.org/EIPS/eip-7540