Protocol integration security checklist: what to check before you plug into another protocol

connecting protocols
Table of Contents

On 17 July 1981 two suspended walkways in the atrium of the Hyatt Regency hotel in Kansas City collapsed, killing 111 people. Two of the injured later died. The National Bureau of Standards investigation found two causes. The connection between the box beams and the hanger rods was inadequate as originally designed. Then, during construction, the continuous rods on the contract drawings were replaced by two sets of shorter rods, a change that “essentially doubled the load” on the fourth-floor connection.

The beams were beams and the rods were rods. What failed was the joint, and the change that broke it was made at the joint after the design had been signed off.

A DeFi integration is a joint. Your contracts were reviewed. The lending market, oracle, bridge or token you plug into was probably reviewed too. The place where the two meet is the part nobody was asked to look at, and it is also the part that keeps changing after launch.

Quick answer

Before you integrate another protocol, check seven things: who can change it (upgrades, admin keys, pause), what its price data really measures, whether calling it hands control back to someone else, how its token actually behaves on transfer, what happens to your users when it stops, which version was audited and whether your use of it was in scope, and what you will watch after the integration ships. Each line below comes from a real post-mortem in which the integration itself was the entry point.

Why the integration is the part nobody reviewed

OpenZeppelin has published a retrospective on a Compound-TUSD integration issue. ChainSecurity had found it during an audit of Compound’s cToken contracts. TUSD had two live addresses: a legacy contract that forwarded calls to a newer one. Compound’s sweepToken function refused to sweep the underlying token, but it checked only one address. Called with the other address, it could move the whole TUSD balance of the market into the Timelock.

About $88 million sat in that market. Exploited fully, OpenZeppelin estimated it “would have approximately halved the value of any redemptions” made with existing cTUSD. A patch shipped on 23 February 2022 and no funds were lost. The sentence worth keeping is theirs: the behaviour “exists within the context of the interaction between two separately audited smart contracts”. They added that as many as 30 DeFi protocols may have been similarly affected.

Two audits, and the bug lived in neither codebase. It lived in the assumption one made about the other.

The checklist

#AreaWhat to check before you integrateWhere it went wrong
1ControlWho can upgrade, pause, mint or change parameters on the contract you depend on. One key, a multisig, a timelock? Will you see a change before it takes effect?Ankr aBNBc and Helio, December 2022
2PriceWhich oracle, which asset it actually prices, how stale it can be, what bounds you enforce, how deep the market behind it isInverse Finance, April 2022. Helio, December 2022
3CallbacksDoes any call into it, or any transfer of its token, run someone else’s code before your state is updated?Lendf.Me, April 2020. C.R.E.A.M. Finance, August 2021
4Token behaviourFee on transfer, rebasing, missing return values, more than one address, blocklists, pausing, decimalsBalancer, June 2020. Compound-TUSD, 2022
5Failure pathsWhat your protocol does when it pauses, blocklists you, goes stale, or its chain’s sequencer stops. Can your users still exit?Every incident above, at the moment it was contained
6Audit coverageWhich commit was audited, whether the deployed bytecode matches it, and whether your pattern of use was in scopeCompound-TUSD: both sides audited, the joint was not
7After launchWhich change on their side would invalidate your assumptions, and who is watching for itC.R.E.A.M.: listed in February, exploited in August

The sections below take each line in turn. For the token half there are two good existing lists, Trail of Bits’ token integration checklist and the weird-erc20 catalogue. Use them. This page covers the rest of the protocol around the token.

Who can change the contract you are integrating?

Every contract you integrate has an owner of some kind, even when the docs say “decentralised”. Read the contract itself. Is it behind a proxy? Who holds the upgrade role? Who can pause it, mint from it, change its fee or swap out its oracle? Is that role one key, a multisig with a real threshold, or a timelock long enough for you to react?

At the start of December 2022 Ankr’s aBNBc token was changed by someone who should not have been able to. Ankr’s after-action report says a former team member used a social engineering and supply chain attack to compromise the private key, and that the exploit “was possible partly because there was a single point of failure in our developer key”.

Helio, a borrowing protocol that accepted aBNBc as collateral for its HAY stablecoin, was not the contract under attack and still ended up with the bill. Its own account says the leaked keys let the attacker “alter the aBNBc smart contract and allow infinite minting of aBNBc”, and records a total bad debt of 19,046,819 HAY, about $17 million. Helio’s risk was a key it did not hold, on a contract it did not control.

If you cannot answer “who can change this, and how much warning do I get”, you have not finished the integration. The same reading applies to any token you list; our rug pull red flags guide walks through the owner powers to look for.

What does its price actually measure?

Helio’s loss had a second cause, and it is the more common one. Its account explains that the oracle “was only monitoring the price of BNB and not aBNBc”. When aBNBc collapsed, the collateral was still valued as if it were BNB. Chainlink’s own documentation warns about exactly this: feeds for wrapped or liquid staking assets “might have different heartbeat and deviation thresholds than that of the underlying asset”. A wrapped asset is not its underlying, and an oracle for one is not an oracle for the other.

The other failure is manipulation. On 2 April 2022 an attacker pushed up the price of INV on a thinly traded SushiSwap pool. Inverse Finance’s post-mortem says the manipulation “was not a flash loan attack and was not related to Inverse’s smart contract or front end code, but rather an error in the TWAP oracle sampling method”. The mispriced INV was posted as collateral and $15.6 million was borrowed against it. The oracle was a dependency, and its weakness became Inverse’s loss.

What to check:

  • Which asset the feed prices, down to the exact token.
  • How old an answer can get. Chainlink updates on a deviation threshold or a heartbeat, and some heartbeats run for hours.
  • What bounds you enforce. Chainlink says plainly that “you must decide what limits are acceptable for your application”.
  • How deep the liquidity is behind any on-chain price, and how long a TWAP window has to be before moving it costs more than it earns.
  • On an L2, whether you read the sequencer uptime feed before trusting a price.

A minimal read that enforces the first three looks like this:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import {AggregatorV3Interface} from "@chainlink/contracts/src/v0.8/shared/interfaces/AggregatorV3Interface.sol";

contract PriceReader {
    AggregatorV3Interface public immutable feed;
    uint256 public immutable maxAge;  // from the feed's heartbeat, plus a margin you choose
    int256 public immutable minPrice; // your own bounds for this asset
    int256 public immutable maxPrice;

    error StalePrice(uint256 updatedAt);
    error PriceOutOfBounds(int256 answer);

    constructor(AggregatorV3Interface feed_, uint256 maxAge_, int256 minPrice_, int256 maxPrice_) {
        feed = feed_;
        maxAge = maxAge_;
        minPrice = minPrice_;
        maxPrice = maxPrice_;
    }

    function price() external view returns (int256) {
        (int256 answer, uint256 updatedAt, ) = feed.latestRoundData();
        if (block.timestamp - updatedAt > maxAge) revert StalePrice(updatedAt);
        if (answer <= minPrice || answer >= maxPrice) revert PriceOutOfBounds(answer);
        return answer;
    }
}

A revert is a decision too. If a stale price blocks liquidations as well as borrowing, you have swapped one failure for another, which is why line 5 of the checklist exists.

Does calling it hand control to someone else?

Reentrancy inside one contract is a well-known lesson. Reentrancy across an integration keeps coming back, because the callback lives in code you did not write.

On 19 April 2020 Lendf.Me, dForce’s lending market, lost about $25 million. dForce’s summary of the attack is precise about the mechanism: imBTC is an ERC777 token, and “the callback mechanism enabled the hacker to supply and withdraw ERC777 tokens repeatedly before the balance was updated”. ERC777 calls hooks on the sender and the recipient during a transfer. A lending market written for plain ERC20 did not expect a transfer to run someone else’s code halfway through.

Sixteen months later it happened again. C.R.E.A.M. Finance lost 462,079,976 AMP and 2,804.96 ETH on 31 August 2021. Its post-mortem is unusually direct: AMP’s contracts “worked exactly as designed”, and “the root cause of the exploit was an error in the way C.R.E.A.M. Finance integrated AMP into our protocol”.

What to check:

  • Every external call and every token transfer in your flow, and whether state is final before each one.
  • Token standards with hooks: ERC777, ERC1363, and the onERC721Received and onERC1155Received callbacks on NFT transfers.
  • Read-only reentrancy: a view function on the other protocol that returns a half-updated value while a callback is running, which your contract then trusts as a price or a balance.
  • Whether a reentrancy guard actually covers the path. A guard on your contract does not stop a callback from reading your state through a function you left unguarded.

How does the token actually behave?

At the end of June 2020 two Balancer pools holding STA and STONK were drained. Balancer’s incident note gives the cause in one line: “On each trade, STA has a transfer fee and the pool expects it receive a balance without the fee.” Repeated swaps pushed the pool’s STA balance close to zero, which in Balancer’s words made its price “extremely high” and let the attacker swap it for the pool’s other assets “extremely cheaply”. Balancer also wrote the sentence every permissionless integrator should read: “broken or malicious tokens will always be able to be added at the contract level.”

The known quirks, with the post-mortem or reference behind each:

BehaviourWhat it breaksExample
Fee on transferAccounting that credits the amount sent instead of the amount receivedSTA in Balancer, 2020
Rebasing or balance changes outside transfersAny contract that caches a balanceAmpleforth-style tokens, listed in weird-erc20
Transfer hooksState updated after a callbackimBTC in Lendf.Me, AMP in C.R.E.A.M.
No return value on transferCalls that expect a bool revert or get misreadUSDT, listed in weird-erc20
More than one addressChecks that compare against a single token addressTUSD in Compound
Upgradeable, pausable or blocklistingYour funds or your users’ funds changed or frozen by someone else’s adminUSDC and USDT (upgradeable, blocklists), listed in weird-erc20

For accounting, the defensive default is to measure what arrived rather than trust what was requested, and to use a wrapper that tolerates missing return values:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import {ReentrancyGuard} from "@openzeppelin/contracts/utils/ReentrancyGuard.sol";

contract Vault is ReentrancyGuard {
    using SafeERC20 for IERC20;

    IERC20 public immutable asset;
    mapping(address => uint256) public credited;

    constructor(IERC20 asset_) {
        asset = asset_;
    }

    function deposit(uint256 amount) external nonReentrant {
        uint256 balanceBefore = asset.balanceOf(address(this));
        asset.safeTransferFrom(msg.sender, address(this), amount);
        // Credit what actually arrived, not what was asked for.
        uint256 received = asset.balanceOf(address(this)) - balanceBefore;
        credited[msg.sender] += received;
    }

    function withdraw(uint256 amount) external nonReentrant {
        // Effects before the external call.
        credited[msg.sender] -= amount;
        asset.safeTransfer(msg.sender, amount);
    }
}

This handles fee-on-transfer deposits and missing return values. It does not make a rebasing token safe, and it does not stop an attacker who reads your state from another contract mid-callback. Some tokens should simply not be accepted, and an allowlist is a legitimate security control.

What happens to your users when it stops?

Every incident above was contained the same way: someone paused something. dForce paused Lendf.Me. C.R.E.A.M. paused AMP supply and borrowing. Inverse paused borrowing on Anchor. Helio paused all protocol functions. Ankr moved to a new key.

So the question for your integration is what your protocol does when the thing it depends on is paused, frozen or stale. Can users still withdraw what is theirs? Do liquidations stop, and if they stop, who absorbs the bad debt? If the token you hold can blocklist addresses, what happens if your contract is on the list? If you are on an L2 and the sequencer goes down, does your protocol keep pricing positions off a frozen feed?

Write the answer down before launch, and test the pause on your side as a path your users will actually take.

Which version was audited, and was your use of it in scope?

“It’s audited” is a sentence about one commit. Before you integrate, find the report, find the commit hash it covers, and check that the deployed bytecode behind the proxy matches it. Then read the scope. An audit of a lending market will cover the market. It will not usually cover your vault calling that market in a loop, with a token the auditors never saw.

That was the Compound-TUSD lesson in one line. Both sides had been audited, and the joint had not. If your integration is where your protocol’s value sits, the integration belongs in your own audit scope, named explicitly, with the external contract’s addresses and version. The manual audits we run at Fidesium cover logic and economic risk, with two rounds of fix verification included, and cross-protocol assumptions are exactly the kind of logic that needs a human reading both sides. Our published audit reports show what that looks like in practice.

After you ship, the other side keeps moving

C.R.E.A.M. listed AMP as collateral on 10 February 2021. Its post-mortem says the flaw “has not been exploited until now due to the low amount of AMP supplied”, and that an influx of AMP five days before the attack made it profitable. The code did not change for six and a half months. The economics did.

That is the part a pre-integration checklist cannot close. The contract you integrated can be upgraded. Its admin can change. Its oracle can be swapped, and the liquidity behind its price can drain. The same review that passed on launch day describes a system that no longer exists. We have written about when an audit actually expires and about what continuous monitoring catches that a one-time audit does not. For an integration, the things worth watching are specific:

  • Upgrade, admin-change and pause events on every contract you depend on.
  • Oracle answers going stale, hitting your bounds, or diverging from a second source.
  • Liquidity changes in any market your price depends on.
  • Sudden growth in a collateral type you list, which is what turned the AMP market from dormant to profitable.
  • Your own code changes. A new function that calls an old integration is a new integration.

A worked example of post-deployment monitoring shows what those alerts look like against a real incident timeline.

The examples here are EVM. The questions carry over to Solana programs that call other programs through cross-program invocation, although the mechanics of each check differ.

Where Fidesium fits

Security is a process, and integrations are where that stops being a slogan. Our architecture review covers proxy patterns, access control, upgrade mechanisms and cross-contract interactions before launch, while those decisions are still cheap to change. The same page describes our post-deployment monitoring: detection bots tuned to your protocol’s own logic, not generic threshold alerts.

On the code side, continuous scanning re-runs deterministic analysis on every commit, pull request and deployment, so a change that touches an integration is looked at when it lands rather than at the next audit. Plans start at $399 a month, and every manual audit includes a month of it. If you want to see how scanners compare, our guide to Solidity static analysis tools covers the field. None of it makes an integration safe. It makes a change visible while you can still act on it.

Frequently asked questions

What is a protocol integration security checklist?

It is the list of things to verify about another protocol before your contracts depend on it: who can change it, what its prices measure, whether calls into it can reenter yours, how its token behaves, what happens when it pauses, which version was audited, and what you will monitor after launch.

Is an audited protocol safe to integrate?

Not on that basis alone. An audit covers a specific commit and a specific scope. OpenZeppelin’s Compound-TUSD retrospective describes a flaw that existed only in the interaction between two separately audited contracts. Your use of the other protocol needs to be in your own audit scope.

What is a fee-on-transfer token and why does it break integrations?

It is a token that deducts a fee during a transfer, so the recipient receives less than the amount sent. Contracts that credit the amount sent rather than measuring the balance change end up over-crediting. Balancer lost two pools to this, holding the STA and STONK tokens, in June 2020.

How do ERC777 tokens cause reentrancy?

ERC777 calls hook functions on the sender and recipient during a transfer, which lets a contract controlled by an attacker run code before the calling protocol has updated its state. Lendf.Me in April 2020 and C.R.E.A.M. Finance in August 2021 were both drained this way.

How do you check an oracle before integrating it?

Confirm which exact asset the feed prices, how stale its answer can get, what bounds your contract enforces, and how deep the liquidity behind any on-chain price is. On an L2, check the sequencer uptime feed too. Helio’s own account of its roughly $17 million bad debt names an oracle that priced BNB rather than the aBNBc it accepted as collateral.

What should you monitor after integrating another protocol?

Upgrade, admin-change and pause events on the contracts you depend on, oracle staleness and divergence, liquidity in the markets behind your prices, sudden growth in any collateral you list, and your own code changes that touch the integration.

Does this apply to Solana programs?

The questions do. Who can upgrade the program you call, what its price data measures, and what happens when it halts all apply to cross-program invocation. The specific checks and code differ from the EVM examples on this page.

Can Fidesium review an integration before launch?

Yes. Our architecture review covers cross-contract interactions and upgrade mechanisms before launch, and a manual audit can name the integration explicitly in scope. After launch, post-deployment monitoring watches the contracts you depend on. Request a review with the addresses and versions you plan to integrate.

Share:

More Posts

Tell us your security needs