Introduction
On July 22, 2024, Demox Labs tasked zkSecurity with auditing its Aleo Standard Programs. The specific code to review was shared via GitHub as a public repository (https://github.com/demox-labs/aleo-standard-programs at commit f86d12b45ee2529512d44afe79bfe6045933eaab
). The audit lasted 8 workdays with 1 consultant.
A number of issues were found, which are detailed in the following sections. One consultant performed a three-day review following the audit, in order to ensure that the issues were fixed correctly and without introducing new bugs. The result of the review is included in this report as well (under each finding).
Scope
The scope of the audit includes two components:
- Multi Token Support Program: The token registry that provides token management functionality on Aleo.
- Pondo Protocol: The liquid staking protocol on Aleo.
Summary and recommendations
The codebase was found to be well-organized with sufficient inline code comments. The protocol is well split into isolated components. The state and state transitions of the components are clear and elegantly designed.
In addition to the findings discussed later in the report, we have the following strategic recommendations:
Add tools for monitoring and auto-triggering. The Pondo protocol consists of several components, each with its own states. It is possible that some components get stuck and temporarily block the whole protocol. It is recommended to add tools to monitor the state of each component and then automatically trigger the corresponding function to resolve the issue.
Fix high-severity issues. Ensure that the findings with high severity are properly fixed, as these findings can lead to critical issues.
Add more tests. Add more tests for the calculations, especially regarding token swaps, yield calculations, and validator rankings.
Overview of Multi Token Support Program
The Multi Token Support Program (MTSP) is a token registry that provides token management functionality in Aleo. Like ERC20 in Ethereum, tokens registered in MTSP share the same functions for minting, burning, transferring, and allowance. The difference is that in ERC20 each token is an independent contract, whereas MTSP manages all tokens in one contract. Additionally, MTSP provides private token transfer functionality built on top of the Aleo record model.
Role Management
MTSP supports three kinds of roles to manage the tokens:
Admin: The admin
role has the permission to manage all roles. It also has permission to mint and burn tokens directly. The admin
role can be updated in the update_token_management
function by admin
.
Supply Management: There are three roles for supply management: MINTER_ROLE
, BURNER_ROLE
, and SUPPLY_MANAGER_ROLE
. The MINTER_ROLE
can mint new tokens by executing the mint_public
and mint_private
functions. The BURNER_ROLE
can burn existing tokens by executing the burn_public
and burn_private
functions. The SUPPLY_MANAGER_ROLE
can perform both minting and burning. These roles can be updated using the set_role
and remove_role
functions by the admin
role.
Transfer Authorization: The external_authorization_party
role has the permission to authorize users’ transfers. This role can be updated in the update_token_management
function by admin
.
Token Authorization
MTSP supports two kinds of tokens marked by external_authorization_required
.
Restricted Token: If external_authorization_required
is true, it requires authorization from the external_authorization_party
role before every transfer. The authorized_until
field in the Token
record and Balance
struct marks the expiration block number of the authorization.
There are two maps to store the public balance of the token. The balances[]
map holds the locked balance that cannot be transferred directly. The authorized_balances[]
map holds the authorized balance that can be transferred if it is not expired (i.e., block.height <= authorized_until
). Whenever new public funds are received (e.g., mint_public
, transfer_public
, transfer_public_as_signer
, and transfer_private_to_public
), they will be stored in the balances[]
map. This way, the receiver cannot directly transfer the new incoming funds. The external_authorization_party
role can execute the prehook_public
function to move the public funds from the balances[]
map to the authorized_balances[]
map to authorize the next transfer.
For private funds, whenever a new Token
record is received (e.g., transfer_public_to_private
and transfer_private
but excluding mint_private
), the authorized_until
in the new record will be set to 0
. This way, the receiver cannot directly transfer the newly received private record. The only way to unlock the record is via the prehook_private
function by the external_authorization_party
.
Unrestricted Token: If external_authorization_required
is false, users can transfer their tokens freely without restriction. All public funds are stored in the authorized_balances[]
map. The authorized_until
field in the Token
record and Balance
struct don’t have any actual effect.
Overview of Pondo Protocol
Pondo is a liquid staking protocol on Aleo. It allows users to stake arbitrary amount of Aleo credits and get the liquidity token pALEO, while Aleo natively only allows staking at least 10k credits.
Pondo consists of four parts: delegator, oracle, Pondo core protocol, and Pondo token. When a user deposits Aleo credits, the protocol will mint pALEO token for the users according to the share of new staked Aleo credits to the whole pool. The user then can withdraw the stake with the pALEO token either instantly (with a 0.25% fee) or after several days. Pondo will charge 10% of the staking rewards as a commission fee.
User’s stake is first sent to the core protocol program and then distributed to the delegators. The pondo core protocol manages 5 delegators that perform the delegation. The delegators then delegate to top yield validators, which are tracked by the Pondo oracle. The allocation ratio of each delegator is also specified by pondo oracle and is managed manually by the admin.
The protocol has an epoch of about 7 days that is specified by block number. At each epoch, pondo core protocol will retrieve and redistribute these credits (possibly to new delegators with new allocation). Aleo does not have constant block interval, so the actual time may vary. For simplicity, we still use time instead of block number in this report.
Pondo Delegator
Pondo delegator (pondo_delegator.aleo
) performs the actual delegation to validator. It receives credits from pondo core protocol, bonds credits to validator, unbonds and sends credits back to the pondo core protocol. It is a state machine with 5 states: TERMINAL
, BOND_ALLOWED
, UNBOND_NOT_ALLOWED
, UNBOND_ALLOWED
, and UNBONDING
.
Pondo delegator starts with TERMINAL
state. In this state, the delegator does not have any bonding or unbonding credits to validators, i.e, the credits only exist in the credits.aleo/account
map. Under normal circumstances, pondo delegator just cycles through the 5 states in every epoch:
TERMINAL
–>BOND_ALLOWED
: The pondo core protocol specifies a validator for delegator viaset_validator
function.BOND_ALLOWED
–>UNBOND_NOT_ALLOWED
: The delegator performs the first bond.UNBOND_NOT_ALLOWED
is the ‘normal’ state that earns staking rewards and usually occupies most of the epoch. At this state, delegator can always bond any existing credits to validators but is not allowed to perform unbond.UNBOND_NOT_ALLOWED
–>UNBOND_ALLOWED
: The pondo core protocol allows the delegator to unbond.UNBOND_ALLOWED
–>UNBONDING
: The delegator unbonds all of the stake. InUNBONDING
state, it has to wait 360 blocks to withdraw the stake.UNBONDING
–>TERMINAL
: Once all the stake has been fully withdrawn, anyone can callterminal_state
function to turn delegator back toTERMINAL
state.
It important that the the delegator shouldn’t be stuck in some states forever under any circumstances. Pondo delegator has some permissionless functions that can reset itself to TERMINAL
state:
- If the validator is not open or is unbonding and the delegator is in
BOND_ALLOWED
state, the delegator won’t be able to bond.bond_failed
function should be called. - If the delegator has insufficient balance to bond (less than 10k credits),
insufficient_balance
function should be called. - If the delegator is in
UNBOND_NOT_ALLOWED
orUNBOND_ALLOWED
state but is already unbonded by the validator,unbond
function will fail.terminal_state
function should be called.
Pondo Oracle
The Pondo oracle (pondo_oracle.aleo
) maintains the candidate validators ordered by performance. The performance of a validator is tracked with a reference delegator. It also maintains the allocation ratio of each position. The allocation can be updated manually by the admin.
On the last day of the epoch, the oracle can get the bond amount of the delegator and then subtract the bond amount from the last epoch to get the staking reward. In order to avoid the impact of arbitrary bonding, the reference delegator is restricted to be a program that can only perform bond transactions to the validator at the beginning.
The Pondo oracle also supports boost: the validator can send credits to the core protocol to improve the record of performance. In this way, the validator may rank higher in the top validators list and get more credits allocation (thus may earn more commission fees).
Pondo Core Protocol
The Pondo core protocol (pondo_core_protocol.aleo
) drives the whole protocol. Users directly interact with Pondo core protocol to stake and withdraw credits.
State Transition. The Pondo core protocol is a state machine with 3 states: REBALANCING_STATE
, NORMAL_STATE
, and PREP_REBALANCE_STATE
.
The core protocol starts with REBALANCING_STATE
. Under normal circumstances, it just cycles through the 3 states in every epoch. All of the transition functions are permissionless.
REBALANCING_STATE
–>NORMAL_STATE
: Executerebalance_redistribute
function. It transfers the staked credits to Pondo delegators and set the validators for delegators. This requires all the delegators inTERMINAL
state and then turns them toBOND_ALLOWED
state. InNORMAL_STATE
the protocol earns staking rewards and usually occupies most of the epoch.NORMAL_STATE
–>PREP_REBALANCE_STATE
: Executeprep_rebalance
function. It allows all the delegators to unbond and withdraw the stake. This transition is restricted to the first day of the epoch.PREP_REBALANCE_STATE
–>REBALANCING_STATE
: Executerebalance_retrieve_credits
function. This requires all the delegators to be in theTERMINAL
state and then retrieves all the Aleo credits back to Pondo protocol.
The state transition ensures that the cycle of Pondo core protocol and Pondo delegator is synchronized: when the core protocol is in REBALANCING_STATE
, all the delegators must be in TERMINAL
state. The only chance that the delegators move from TERMINAL
state to next state is when the core protocol moves from REBALANCING_STATE
to NORMAL_STATE
. Therefore, when the core protocol goes through a cycle, the delegator must also go through a cycle, and vice versa.
Commission. Pondo protocol is entitled to 10% of the staking reward as commission. Every time users deposit or withdraw, the protocol calculates the commission according to the staking reward. The staking reward is tracked as current_total_stake - balances[DELEGATED_BALANCE]
, where the balances[DELEGATED_BALANCE]
stores the last tracked total stake. The commission is charged as pALEO token and saved into owed_commission
for future minting.
Credits Reservation. The core protocol reserves 2.5% of total Aleo credits (250k capped) for the instant withdrawal. During NORMAL_STATE
, the Aleo credits held by the protocol consist of two parts:
- Credits on Pondo core protocol =
balances[CLAIMABLE_WITHDRAWALS]
+ instant withdrawal reservation + recent deposit + directly sent credits - Credits on Pondo delegator =
balances[DELEGATED_BALANCE]
+balances[BONDED_WITHDRAWALS]
+ new stake earnings + directly sent credits
Pondo Token
Pondo introduces two tokens: pALEO and PONDO token. pALEO is the staked Aleo credits and represents the share of the staked Aleo credits pool. PONDO token represents the share of the commission fee pool. In the Pondo core protocol, all the commission fees will be transferred to pondo_token.aleo
. PONDO token owners can burn the token to get the corresponding share of pALEO tokens owned by the pondo_token.aleo
program.