Poel Review

Issue Unused Constant Definitions Creates Dead Code
Location Location: sources/poel/iAsset.move, lines 53, 59, 65, 74, 77, 85, 106
Severity Informational
Description The following constants are defined in iAsset.move but are never referenced anywhere in the codebase:

const EWRONG_CWV_LENGTH: u64 = 8;                    const EWRONG_DESIRABILITY_SCORE_LEN: u64 = 10;      const EWRONG_ASSET_COUNT: u64 = 12;                  const ENOT_ENOUGH_TIME_PASSED: u64 = 16;          const ENOT_ENOUGH_WITHDRAWABLE_REWARDS: u64 = 17;   const INVALID_TS: u64 = 20;                          const MAX_U64: u64 = 18446744073709551615; | | Recommendations | Remove all unused constant definitions from iAsset.move: • EWRONG_CWV_LENGTH • EWRONG_DESIRABILITY_SCORE_LEN • EWRONG_ASSET_COUNT • ENOT_ENOUGH_TIME_PASSED • ENOT_ENOUGH_WITHDRAWABLE_REWARDS • INVALID_TS • MAX_U64 | | Comments / Resolution | Acknowledged |

Issue Error Code Numbering Inconsistencies
Location Location: sources/poel/iAsset.move, lines 53, 59, 65, 74, 77, 85, 106
Severity Informational
Description There are inconsistencies in the definition and assignment of error code numbers across the Move codebase. These include duplicate error codes within the same module, gaps in error code sequences, and potential cross-module confusion due to reused error code numbers.
  1. Duplicate Error Codes (Within a Single Module) File: sources/config/config.move • const EINVALID_MIN_COLL_RATE: u64 = 1; (Line 12) • const EACCESS_DENIED: u64 = 1; (Line 30) • Impact: This creates ambiguity when error code 1 is encountered, making debugging and error handling unreliable.
  2. Gaps in Error Code Sequences File: sources/poel/iAsset.move • Missing error code 1 (starts at 2) • Missing error code 14 (jumps from 13 to 15) File: sources/config/asset_config.move • Missing error code 7 (jumps from 6 to 8) File: sources/poel/poel.move • Missing error codes 1, 5, 7–10, 14–15 File: sources/asset_pool/asset_router.move • Missing error codes 4–19 (jumps from 3 to 20) • Impact: Gaps may indicate missing or deprecated errors, and can make maintenance and auditing more difficult.
  3. Cross-Module Error Code Reuse • Error codes such as 20 (ECANT_COVER_FEES) are used in multiple modules (iAsset.move, poel.move). • Impact: While not strictly incorrect, this can cause confusion if error codes are expected to be unique across the system. | | Recommendations | • Within each module: Ensure all error codes are unique and sequential, with no duplicates or unexplained gaps. • Across modules: Consider adopting a naming or numbering convention to avoid confusion (e.g., module-specific prefixes or ranges). • Immediate fix: Resolve the duplicate error code 1 in config.move. | | Comments / Resolution | Acknowledged |
Issue Redundant Immutable Borrow Prevents Mutable Borrow of RewardBudget in distribute_rewards
Location Location: sources/poel/poel.move, function: distribute_rewards, line 831
Severity Informational
Description An unused immutable borrow of the RewardBudget global resource is performed on line 831:
let reward_ref = borrow_global<RewardBudget>(get_storage_address());
Later in the same function (line 839), a mutable borrow of the same RewardBudget resource is performed:
let reward_budget_ref = borrow_global_mut<RewardBudget>(obj_address);
Both borrows reference the same address (get_storage_address()), either directly or via the obj_address variable. The Move borrow checker does not permit both an immutable and a mutable borrow of the same resource at the same time, even if the immutable borrow is unused. This results in a compilation error.
Recommendations Remove the unused immutable borrow on line 831.
Comments / Resolution Acknowledged
Issue Unused Functions
Location Location:
• sources/asset_pool/asset_util.move: get_address_key, get_supra_coin_key
• sources/poel/iAsset.move: deconstruct_asset_entry (lines 879–881)
Severity Informational
Description The functions get_address_key and get_supra_coin_key in asset_util.move, and deconstruct_asset_entry in iAsset.move, are defined but not used anywhere in the codebase. Their presence may indicate planned future use, a leftover from refactoring, or an intended external API that is not yet integrated.
Recommendations • If the function is intended for future use or as a public API, document its purpose clearly in the code comments.
• If it is not needed, consider removing the function to keep the codebase clean and maintainable.
• If it is meant for external module or testing use, ensure it is properly tested and referenced in documentation.
Comments / Resolution Acknowledged
Issue Frozen Accounts Can Still Receive Minted iAssets
Location Location: sources/poel/iAsset.move, function: mint_iasset(), lines 920–955 (esp. line 950)
Severity Medium
Description The mint_iasset() function does not check if the target account is frozen before minting iAssets. This allows frozen accounts to receive new tokens, even though they cannot transfer existing balances, leading to inconsistent security controls.
Recommendations Add an account freeze check before minting. Define an appropriate error constant (e.g., EACCOUNT_FROZEN).
Comments / Resolution
Issue Division by Zero in APY Calculation Due to Unvalidated Epoch Length
Location Location
• Primary: sources/poel/iAsset.move:1805 in calculate_asset_apy() function
• Root Cause: sources/config/config.move:313 (missing validation in parameter setting)
Severity Low
Description The calculate_asset_apy function contains a division by zero vulnerability that occurs when config::get_length_of_epoch() returns 0. At line 1805, the function calculates let number_of_epoch_year = ONE_YEAR_SEC / config::get_length_of_epoch() without validating that the epoch length is non-zero. The MutableParameters.length_of_epoch field is initialized to 0 during contract deployment and can only be updated through the set_parameters function, which lacks validation to prevent administrators from setting invalid values.
This vulnerability manifests in two scenarios: initially when the protocol deploys with length_of_epoch = 0 and any APY calculation fails until properly configured, or when an administrator accidentally sets length_of_epoch = 0 during parameter updates. When triggered, this causes all reward allocation operations and APY calculations to fail with runtime aborts, effectively creating a denial of service for core protocol functionality. While no funds are at risk and the issue can be immediately resolved by calling set_parameters with a valid epoch length, the dependency on administrator action and the disruption to essential protocol operations justifies medium severity classification.
Recommendations Add validation in the set_parameters function:
assert!(length_of_epoch > 0, error::invalid_argument(EINVALID_EPOCH_LENGTH));
Additional Recommendations:
Defensive Programming: Add a check in calculate_asset_apy before division:
let epoch_length = config::get_length_of_epoch();
assert!(epoch_length > 0, error::invalid_state(EINVALID_EPOCH_CONFIG));
let number_of_epoch_year = ONE_YEAR_SEC / epoch_length;
Comments / Resolution
Issue Inconsistent Struct Reference Management in allocate_rewards
Location • File: sources/poel/poel.move
• Function: allocate_rewards (lines 739-820)
• Lines: 757, 778, 779, 791
Severity Low
Description The allocate_rewards function exhibits inconsistent and confusing patterns in managing struct references, leading to code that is difficult to maintain and prone to developer errors:
Variable Shadowing with Different Borrow Types:
// Line 757: Immutable borrow
let delegated_amount_ref = borrow_global<DelegatedAmount>(obj_address);

// Line 778: Mutable borrow shadows the immutable reference    let delegated_amount_ref = borrow_global_mut<DelegatedAmount>(obj_address); // Line 779: First mutable borrow    let reward_budget_ref = borrow_global_mut<RewardBudget>(obj_address);        // Line 791: Immediately re-borrow the same resource    let reward_budget_ref = borrow_global_mut<RewardBudget>(obj_address); The function switches between immutable and mutable references to the same resources without clear separation, making it unclear which reference capabilities are needed at each point. | | Recommendations | Use Distinct Variable Names: // Immutable references for calculations let delegated_amount_read = borrow_global<DelegatedAmount>(obj_address); let reward_budget_read = borrow_global<RewardBudget>(obj_address); // Mutable references for updates let delegated_amount_mut = borrow_global_mut<DelegatedAmount>(obj_address); let reward_budget_mut = borrow_global_mut<RewardBudget>(obj_address); | | Comments / Resolution | |

Issue Division by Zero Risk in Collateralization Rate Calculation
Location Location: sources/poel/iAsset.move, lines 53, 59, 65, 74, 77, 85, 106
Severity Low
Description The calculate_collateralization_rate function in the iAsset.move module performs division operations using the desired_weight variable as a denominator. If desired_weight is set to 0 (the default for new assets) or to MAX_WEIGHT (100,000), this will result in a division by zero, causing the transaction to abort unexpectedly. This can occur immediately after asset creation (since the default is zero) or if an administrator sets an invalid weight via the batch update function. This makes the system fragile and prone to runtime errors if not carefully managed.
Recommendations • Initialization: Set a safe, non-zero default value for desired_weight when creating new assets (e.g., desired_weight: 1).
• Validation: Add assertions in the batch_update_desired_weight function to ensure that desired_weight is always greater than 0 and less than MAX_WEIGHT.
Comments / Resolution
Issue Admin Accounting Updates Lack Validation, Risk of Accidental Protocol Insolvency
Location File: sources/poel/poel.move
Function: update_accounting_states (lines 389-403)
Line: 399 - delegated_amount_ref.total_delegated_amount = total_delegated_amount;
Severity Low
Description The update_accounting_states function allows administrators to manually update total_delegated_amount without any validation against actual on-chain state. This creates significant risk for accidental protocol damage through human error.
Since reward calculations depend on this value:
Apply to poel.move
total_delegated_amount
Accidental scenarios that could break the protocol:
Typo/Input Error:
Actual staked: 1,000,000 SUPRA
Admin meant:   1,000,000 SUPRA
Admin typed:     100,000 SUPRA (missing zero)
Result: 900,000 "fake rewards" → Protocol insolvency
Wrong Units:
Admin thinks in millions: enters 1 (meaning 1M)
System interprets as: 1 SUPRA
Result: Nearly all staked funds treated as "rewards"
Recommendations If this function is to be kept, then perhaps add validation such as:

let actual_staked = get_total_staked() +            borrow_global<ReplacedDelegationPool>(get_storage_address()).delegated;     let min_allowed = actual_staked * 80 / 100;  // Allow 20% below actual     let max_allowed = actual_staked * 120 / 100; // Allow 20% above actual          assert!(total_delegated_amount >= min_allowed &&              total_delegated_amount <= max_allowed,              error::invalid_argument(EINVALID_ACCOUNTING_VALUE));          // ... rest of function | | Comments / Resolution | |

Issue Misuse of #[view] Annotation in get_fa_pool_details
Location File: sources/asset_pool/asset_pool.move
• Function: get_fa_pool_details
• Line: 138
Severity Low
Description The function get_fa_pool_details is annotated with #[view], which signals to off-chain clients and tooling (e.g., aptos move view, REST API queries, or SDKs) that the function is read-only. However, the function currently uses borrow_global_mut:
let pool = borrow_global_mut<AssetPoolVault>(get_storage_address());
While this doesn't break on-chain execution, it violates the semantics of a view function, which is expected to make no mutations or acquire mutable references.  Using borrow_global_mut in a function marked #[view] contradicts its intended semantics and may confuse developers or break future assumptions in tooling or audits.
Recommendations Change the function public fun get_fa_pool_details:
let pool = borrow_global_mut<AssetPoolVault>(get_storage_address());
To
let pool = borrow_global<AssetPoolVault>(get_storage_address());
Comments / Resolution Acknowledged
Issue Incentive Inversion in Collateralization Rate Calculation Causes Protocol Imbalance
Location File: sources/poel/iAsset.move
• Function: calculate_collateralization_rate
• Lines: 1336–1346 (underweight asset branch)
Severity High
Descript
ion The withdraw_surplus_rewards function contains a  logic error in its assertion that validates surplus reward withdrawals. The assertion incorrectly adds total_borrowed_amount (funds already lent out) back to the vault balance, creating a false impression of available funds:
assert!(balance + delegated_amount_ref.total_borrowed_amount - requested >= delegated_amount_ref.total_borrowable_amount + reward_budget_ref.allocated_reward_balance, error::invalid_state(EAMOUNT_EXCEEDED));
The total_borrowed_amount represents funds that have already been borrowed out of the vault and are no longer available. Adding these funds back to the balance calculation allows withdrawal of funds that should be reserved for future borrowing capacity and allocated rewards. This can lead to protocol undercollateralization where the vault lacks sufficient funds to honor borrowing commitments and reward distributions.
Recommendations Change the assertion:
assert!(balance + delegated_amount_ref.total_borrowed_amount - requested >= delegated_amount_ref.total_borrowable_amount + reward_budget_ref.allocated_reward_balance, error::invalid_state(EAMOUNT_EXCEEDED));
To:
assert!(balance - requested >= (delegated_amount_ref.total_borrowable_amount - delegated_amount_ref.total_borrowed_amount) + reward_budget_ref.allocated_reward_balance, error::invalid_state(EAMOUNT_EXCEEDED));
Comments / Resolution Fixed
Issue Decimal Mismatch in Redemption Leads to Incorrect FA Withdrawal Amounts
Location Location:File: sources/poel/poel.moveFunction: redeem_iassetLines: 1455–1459Related helper:File: sources/asset_pool/asset_util.moveFunction: scale_to_originLines: 44–46
Severity High
Description The redemption path computes amount in 8-decimal iAsset units and then “de-normalizes” it using origin_token_decimals before passing it to the withdrawal router. However, the router withdraws from the FA as represented on this chain, whose Metadata-defined decimals may differ from the origin chain’s decimals.
If origin_token_decimals ≠ on-chain FA decimals(asset), the scaled n_amount will not match the units expected by asset_pool::withdraw_fa.
Example: For USDC with 6 origin decimals but 8 decimals on this chain, scale_to_origin converts 8→6 (divides by 100). The router then uses this 6-decimal-sized value against an 8-decimal FA, causing a 100x under-withdrawal. The reverse configuration can cause over-withdrawal.
Recommendations Compute the final scale using the FA’s actual on-chain decimals instead of origin chain decimals.
Two safe approaches:
  1. Move scaling to the router and use FA metadata decimals: Keep poel::redeem_iasset passing the raw 8-decimal amount to the router. In redeem_router::redeem_iasset, after resolving asset = get_fa_metadata(origin_token_address), fetch fa_decimals = fungible_asset::decimals(asset) and compute: amount_scaled = asset_util::scale((amount as u128), /from=/8, /to=/fa_decimals) Use amount_scaled (u128→u64 with bounds check) for withdraw_fa.
  2. If scaling remains in poel::redeem_iasset, replace origin_token_decimals with the FA’s on-chain decimals: Resolve the FA object (or expose a helper) and use fungible_asset::decimals to scale 8→FA-decimals. Add an assertion that origin_token_decimals == fa_decimals if that invariant is intended; otherwise, ignore origin decimals entirely for the final withdrawal. | | Comments / Resolution | Fixed |