Skip to main content

Armor RCA

Smart Contract Security Assessment





Dedaub was commissioned to perform an audit on the RCA (reciprocally covered assets). The RCA protocol is a DeFi insurance protocol that has a novel funding model compared to previous versions of Armor. Rather than relying on insurers, or stakers to cover losses, the users of the system collectively cover other users in the event that a single protocol is hacked. Since hacks are long-tailed events it is expected that since there will be many many different pools

No Critical vulnerabilities were found, but two high vulnerabilities were found. The audit did not include OpenZeppelin library code, most notably the Merkle verifier and the Convex Shield. The audit applies to commit hash: 88bfc89d81e85a6c5d010654a4eec1120722984e.

The audit methodology involved careful review of (currently closed-source) sources, static analysis of compiled contracts and inspection of off-chain systems expected to interact with Armor RCA.


As is common in many new protocols, the maintainers of the smart contracts yield considerable power over the protocol. It is possible that some of these elements could evolve in the direction of decentralization in later versions of the protocol.

Governance (a timelocked DAO) - The power of the governance includes the ability to liquidate deposits on the protocol by setting the amounts to be liquidated for each shield. The protocol’s Governance can set many parameters that affect the user’s funds, namely the apr, discount, and withdrawal delay. The current DAO design allows for users to predict the governance actions. [Disclaimer: Dedaub has been previously commissioned to audit the Armor DAO contracts.]

Oracle (an EOA)- A centralized Oracle routinely sets the price for any asset.

Guardian (a multisig) - The “guardian” of the protocol can set the amount of funds that are paused (to be used following a hack) that directly affects withdrawals (see L3). When this happens, whoever withdraws their tokens loses a percentage (no current limits in code prevent it from reaching 100%) of the underlying tokens, but these are distributed to all other users after funds are un-paused.


The audit was conducted on an in-development version of Armor RCA. This has allowed Dedaub to make suggestions at an early stage, whilst the system is still under development. On the other hand, at this stage of development the system is not yet fully functional, lacks documentation or a complete test suite. Although a security audit may reveal a number of functional issues, it is more intended on finding security issues in an adversarial environment. In order to find functional issues, further testing of the application under many different settings needs to be conducted. There is also an additional risk that new bugs may be introduced before the final project is deployed. Further audits for the rest of the project after testing and staging, but prior to final deployment, will considerably lower these risks.

The main economic threat with the architecture of Armor RCA (H2) is the centralized Oracle that is expected to become operational whenever either a hack is detected or fees are levied, and liquidation is initiated. Once this is invoked, a small percentage of all assets covered by the system will be able to be liquidated. Considering the Armor RCA is expected to cover hundreds of derivative assets (e.g., staked Yearn assets), maintaining a reasonable price of all assets will require a substantial amount of (centralized) infrastructure, and is hard to get right. The other main issue is with “Zaps” (H1), which once implemented could potentially allow attackers to steal some withdrawals through classic flashloan attacks.

The audit’s main target is security threats, i.e., what the community understanding would likely call "hacking", rather than regular use of the protocol. Functional correctness (i.e., issues in "regular use") is a secondary consideration. Typically it can only be covered if we are provided with unambiguous (i.e., full-detail) specifications of what is the expected, correct behavior. In terms of functional correctness, we often trusted the code’s calculations and interactions, in the absence of any other specification. Functional correctness relative to low-level calculations (including units, scaling, quantities returned from external protocols) is generally most effectively done through thorough testing.


This section details issues that affect the functionality of the contract. Dedaub generally categorizes issues according to the following severities, but may also take other considerations into account such as impact or difficulty in exploitation:

Can be profitably exploited by any knowledgeable third-party attacker to drain a portion of the system’s or users’ funds OR the contract does not function as intended and severe loss of funds may result.
Third-party attackers or faulty functionality may block the system or cause the system or users to lose funds. Important system invariants can be violated.
  • User or system funds can be lost when third-party systems misbehave.
  • DoS, under specific conditions.
  • Part of the functionality becomes unusable due to a programming error.
  • Breaking important system invariants but without apparent consequences.
  • Buggy functionality for trusted users where a workaround exists.
  • Security issues which may manifest when the system evolves.

Issue resolution includes “dismissed”, by the client, or “resolved”, per the auditors.


[No critical severity issues]



Withdrawn tokens may be lost through flash loan attacks


Withdrawn tokens may be lost through flash loan attacks

The function RcaController::redeemFinalize() allows any unauthorized user to "zap" withdrawn tokens using any available zapper contract. It is presumed that, once implemented, some of these zapper contracts would swap on an AMM or stake / unstake from an LP. This allows attackers to atomically tilt respective pools used by the zapper contract to extract value out of these withdrawals.

Note: Upon our re-review of the code, the described flash loan attack is fixed. However, it should be noted that the user is responsible for calling a zapper contract with the appropriate parameters in order to avoid MEV attacks.


Updates can be DoS’ed via front-running


Updates can be DoS’ed via front-running

The function RcaController::_update() is invoked every time a shield calls the controller (on every user interaction with the shield), to ensure it has fresh/correct parameters:

function _update(...) internal {
IRcaShield shield = IRcaShield(msg.sender);
uint32 lastUpdate = uint32(lastShieldUpdate[msg.sender]);

// Seems kinda messy but not too bad on gas.
SystemUpdates memory updates = systemUpdates;

if (lastUpdate < updates.treasuryUpdate) shield.setTreasury(treasury);
// Update shield here to account for interim period where APR was changed but shield had not updated.
if (lastUpdate < updates.aprUpdate) {
shield.controllerUpdate(apr, uint256(updates.aprUpdate));
lastShieldUpdate[msg.sender] = uint32(block.timestamp);

However, its current implementation allows an attacker to front-run an update transaction, by placing a transaction interacting with the shield before the controller’s update. In this case, the next time _update() is called, the relevant update action will not take place as lastShieldUpdate[msg.sender] will be equal to the respective field in the SystemUpdates variable.



Insufficient capacity check


Insufficient capacity check

In RcaShieldBase::mintTo, the controller is called to verify that the shield has the required capacity before minting the tokens. However, the following check:

require(_uAmount < _capacity...)

Does not take into consideration the currently “active” underlying tokens. Thus, with the current checks in place, a user can bypass this capacity limit by calling mintTo() multiple times.


Liquidatable amounts cannot be lowered due to underflow


Liquidatable amounts cannot be lowered due to underflow

In RcaShieldBase::setLiq, if the amount to be liquidated needs to be lowered (for

instance because the hacked amount was overvalued) the contract call reverts due

to an underflow:

function setLiq(
uint256 _newCumLiq,
uint256 _updateTime
) external onlyController
// Do this here rather than on controller for slight savings.
uint256 addForSale = _newCumLiq - cumLiq;
amtForSale += addForSale;
cumLiq = _newCumLiq;


Use of centralized oracle can be problematic


Use of centralized oracle can be problematic

The system maintains the root of a merkle tree that stores the prices of all the covered assets in eth. These prices are verified in the purchase() method of the RcaController which is used when the purchaseU() and purchaseRca() methods of the RcaShieldBase contract are called.

Given the volatile nature of many of the covered assets, stale price values can deviate significantly from their actual prices, allowing arbitrageurs to make profit by reducing the Shield’s profits. This loss is bound by the fact that the available amount is the shield’s cumulative liquidity (amount lost in hacks) and the protocol’s profits (if apr > 0).



Real APR hard to calculate with liquidations


Real APR hard to calculate with liquidations

The protocol’s fees are collected by setting a small percentage of tokens to be liquidated every time each shield is utilized. We believe this will be inefficient. Since these amounts will be low, arbitrage bots will only liquidate these tokens when there’s a significant upside, and only when the price is generous. For this reason the real APR is expected to be different to the advertised one.


New system parameter values are not sanity checked


New system parameter values are not sanity checked

The RcaShieldBase contract contains a number of setter functions, callable only by the RcaController, setting some important system parameters. Many of these functions (setPercentPaused(), setDiscount(), setApr()) accept/set values that represent percentages and should be checked to not be over 100% (10000) in order to avoid having a mistake in the controller damaging the system’s state. In addition, the value set in setWithdrawalDelay() could have some upper and lower bounds as well to highlight its intended use.


Incorrect comment regarding the stored capacities


Incorrect comment regarding the stored capacities

The comment on the RcaController mentions that capacities are stored in USD.

//Merkle root of the amount of capacity available for each ... (in USD)
bytes32 public capacitiesRoot;

However the actual values stored are in the native underlying token. This can be verified by looking at the RcaController::mint method, which is called by the respective shield to verify that Armor RCA has the required capacity to take on new deposits, the amount that is deposited is compared to a respective amount from a merkle tree containing all the capacities for all shields:

require(_uAmount < _capacity, "Not enough capacity available.");


This section details issues that are not thought to directly affect the functionality of the project, but we recommend considering them.


RcaController modifiers can be merged.


RcaController modifiers can be merged.

The modifiers RcaController::onlyShield and RcaController::update can be merged into a single modifier.


Unused arguments in RcaController::redeemFinalize


Unused arguments in RcaController::redeemFinalize

The function RcaController::redeemFinalize returns whether a contract is a

zapper contract. However, this currently only uses a single argument out of the

5 passed to determine this.


Users lose funds when withdrawing while shield is paused


Users lose funds when withdrawing while shield is paused

There are two mechanisms in the shield contracts that calculate the amount of

underlying token (_uValue()) or the amount of rca tokens (_rcaValue()) when withdrawing or depositing, respectively. The calculations however are inconsistent with each other, at the expense of the user since a lower amount of underlying tokens is returned when withdrawing, if the shield is paused. This is a concern, especially if the protocol is going to be paused for a week for every single hack.

The assumption here is that the amount of tokens to be paused is conservative,

higher than the amount required to cover the hacked shields.


Compiler known issues


Compiler known issues

The contracts were compiled with the Solidity compiler 0.8.11 which, at the time of writing, has no known bugs.


The audited contracts have been analyzed using automated techniques and extensive human inspection in accordance with state-of-the-art practices as of the date of this report. The audit makes no statements or warranties on the security of the code. On its own, it cannot be considered a sufficient assessment of the correctness of the contract. While we have conducted an analysis to the best of our ability, it is our recommendation for high-value contracts to commission several independent audits, a public bug bounty program, as well as continuous security auditing and monitoring through Dedaub Security Suite.


Dedaub offers significant security expertise combined with cutting-edge program analysis technology to secure some of the most prominent protocols in DeFi. The founders, as well as many of Dedaub's auditors, have a strong academic research background together with a real-world hacker mentality to secure code. Protocol blockchain developers hire us for our foundational analysis tools and deep expertise in program analysis, reverse engineering, DeFi exploits, cryptography and financial mathematics.