Skip to main content
EbisuEbisu ~ CCIP Integration - Sep 19, 2025

Ebisu ~ CCIP Integration

Smart Contract Security Assessment

September 19, 2025

Ebisu

SUMMARY


ABSTRACT

Dedaub was commissioned to perform a security audit of Ebisu’s integration of Chainlink’s CCIP into the protocol. The core of the changes can be located to the protocol-native ebUSD token. The token now introduces functionality that can be used by standard CCIP pools in order to burn tokens from one network and mint them to a different one. Ebisu will additionally provide users with the ability to use the bridged tokens to the protocol instance running on the destination chain.


SETTING & CAVEATS

This audit report mainly covers the changes introduced to the at-the-time private repository https://github.com/ebisufinance/ebisu-money/ at https://github.com/ebisufinance/ebisu-money/pull/139.


Although all CCIP-related contracts are assumed to be safe for this engagement, the auditors also consulted the CCIP codebase (https://github.com/smartcontractkit/chainlink-ccip/tree/contracts-ccip-release/1.6.0/chains/evm/contracts) when considering the low-level implementation details of the integration and assessing the soundness of the protocol’s use of CCIP.

2 auditors worked on the codebase for 1 day.

The audit’s main target is security threats, i.e., what the community understanding would likely call "hacking", rather than the 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 and quantities returned from external protocols) is generally most effectively done through thorough testing rather than human auditing.

While the set of changes introduced in the PR does not affect a large number of lines in the codebase, the ability to perform cross-network ebUSD transfers and use the tokens in a different running instance of the protocol (regardless of where the debt was created) introduces a set of semantic differences compared to the core of LiquityV2. Therefore the auditors also spent considerable time focusing on the following areas:

  • How economic incentives might shift when liquidity is available in a cross-network manner
  • Whether there could be any unwanted consequences when parts of CCIP or the protocol’s pools are upgraded
  • How the network conditions on deployed networks affect the overall integration
  • Whether important protocol invariants can be violated because of the cross-network liquidity
  • How users’ experience might be negatively affected

On November 17th, 2025, there was a request to review an additional small change that would enable the use of LayerZero’s OFT standard instead of CCIP: https://github.com/ebisufinance/ebisu-money/pull/145

The team spent an additional 0.5 days reviewing the above change. In order to ensure the correctness of the LayerZero integration, the auditors consulted the devtools repository at: https://github.com/LayerZero-Labs/devtools , commit 2c3a0b2c6a4589078e405d6e3b79ed745adda16f.

Although no major issue was identified, we provide some informatory items in the form of P5 and P6.


VULNERABILITIES & FUNCTIONAL ISSUES

This section details issues affecting 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:

CATEGORY
DESCRIPTION
CRITICAL
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.
HIGH
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.
MEDIUM
Examples:
  • 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
LOW
Examples:
  • 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” or “acknowledged” but no action taken, by the client, or “resolved”, per the auditors.


PROTOCOL-LEVEL CONSIDERATIONS

P1

Economic incentives for providing cross-chain Stability Pool liquidity may be circumstantially weakened

PROTOCOL-LEVEL-CONSIDERATION
info

Providing to a cross-chain Stability Pool consists of:

  • Minting ebUSD is in one chain
  • Bridging minted ebUSD through CCIP
  • Once ebUSD arrive at the destination chain, tokens are deposited into the Stability Pool of the other network

With all other things equal, this scheme somewhat weakens the economic incentive of making a cross-network branch's debt more backed:

For the branch where ebUSD originates from, the branch remains as backed as before (no additional deposits are made by the user to the originating networks SP) but it now has more debt so it becomes more unbacked. Even though the destination branch becomes more backed, the original branch becomes more prone to redemptions. As a result, users following the above mentioned scheme can end up losing exposure to their collateral by having more redemptions take place against their troves.

For instance, assume that someone crafts a typical (leveraged) long position on a collateral of their choice using a portion of their ebUSD:

  • Users mint ebUSD using a desired collateral
  • Users sell the ebUSD to get hold of more collateral tokens.

In such cases, those positions have no incentive to provide a cross-chain SP since a decreased exposure could negatively affect the PNL of their long position. Unless different economic incentives arise on the destination chain (e.g., arbitrage) Stability Pool liquidity does not seem to be inherently benefiting from the potential unification of ebUSD liquidity.

P2

Recovering from <1$ ebUSD depeggs via redemptions might be slower due to CCIP latency

PROTOCOL-LEVEL-CONSIDERATION
info

In scenarios where most of ebUSD liquidity has been unified in one chain, the latency of CCIP might make restoring <1$ depeggs with redemptions slower for the chain with the least liquidity.

Since CCIP messages are executed based on when the originating blockchain’s state is considered finalized, it might take several minutes until bridging takes place (e.g. for Ethereum that is ~15 mins). In such cases, relying on redemptions to restore the 1$ peg might take a little bit longer compared to when ebUSD are not bridged because of the latency introduced by CCIP.

This should not be a likely occurrence, since as noted at P1 there are reasons that should not allow for the complete unification of liquidity.

P3

Rare redemption DOS can be circumstantially easier

PROTOCOL-LEVEL-CONSIDERATION
info

As noted in P1 users will now be able to mint ebUSD tokens in one chain and bridge them to another to provide them as SP liquidity. There's a rare attack targeting the redemption routing logic of CollateralRegistry that can be easier with unified liquidity :

CollateralRegistry::redeemCollateral:204
...
} else {
// Don't allow redeeming more than the total unbacked in one go, as that would result in a disproportionate
// redemption (see CS-BOLD-013). Instead, truncate the redemption to total unbacked. If this happens, the
// redeemer can call `redeemCollateral()` a second time to redeem the remainder of their BOLD.
if (_boldAmount > totals.unbacked) {
_boldAmount = totals.unbacked;
}
}
...


If we assume two instances of Ebisu running on different networks with a comparable amount of debt being created in each chain, then if the major ebUSD holder(s) of one chain provide to the SP of the other chain.

In this scenario it becomes easier to have the unbacked debt amounts of a branch be a few units of debt ( but not 0 ). Because this would trigger the above-mentioned snippet, this would mean that all ebUSD redemptions for that branch will be effectively limited to a few debt units — so in practical terms redemptions would be paused.

Under normal protocol conditions it’s not expected for this attack to ever occur since the cost of pulling it can become prohibitive. There should therefore be no practical security consequences on account of this attack becoming easier.

P4

Redemption rate semantics differ from LiquityV2

PROTOCOL-LEVEL-CONSIDERATION
open

The developers of the protocol promptly identified during integration that the following snippet in CollateralRegistry can lead to inconsistent debt accounting during redemption routing:

CollateralRegistry::redeemCollateral:208
...
totals.boldSupplyAtStart = boldToken.totalSupply();
...

Since CCIP bridging can increase/reduce the total supply of ebUSD without affecting the recorded debt, the above line was not suitable for calculating the percentage out of the total debt that is being redeemed. The snippet was changed to:

CollateralRegistry::redeemCollateral:208
...
totals.boldSupplyAtStart = getTotalSystemDebt();
...

function getTotalSystemDebt() public view virtual returns (uint256) {
uint256 totalDebt = 0;

for (uint256 i = 0; i < totalCollaterals; i++) {
IEbisuBranchManager branchManager = getBranchManager(i);
totalDebt += branchManager.getEntireBranchDebt();
}

return totalDebt;
}
LiquityBase::getEntireBranchDebt:48
function getEntireBranchDebt() public view returns (uint256 entireSystemDebt) {
uint256 activeDebt = activePool.getBoldDebt();
uint256 closedDebt = defaultPool.getBoldDebt();

return activeDebt + closedDebt;
}
ActivePool::getBoldDebt:177
// Returns sum of agg.recorded debt plus agg. pending interest. Excludes pending redist. gains.
function getBoldDebt() external view virtual returns (uint256) {
return aggRecordedDebt + calcPendingAggInterest() + aggBatchManagementFees + calcPendingAggBatchManagementFee();
}

As it becomes apparent, ActivePool::getBoldDebt does only include the created minted debt at the time of being called, but it also includes the pending interest that has not yet been accounted for at the time of computation.

As a result, the calculation of the redemption rate that depends on the total debt will end up being greater compared to if totalSupply() had been used at that instance:

ActivePool::getBoldDebt:177
...
// get the fraction of total supply that was redeemed
uint256 redeemedBoldFraction = _redeemAmount * DECIMAL_PRECISION / _totalBoldSupply;
...


In the scenario where a large enough interest gets aggregated prior to a redemption, the fee being paid to the borrower the trove of which gets redeemed against will end up being smaller. This reduction in fees theoretically makes the short-term loss incurred by the reduced collateral exposure greater.

Under normal protocol operations – where Troves are frequently interacted with, the difference between totalSupply()and getEntireBranchDebt()should not be significant. However, this could come into play if for whatever reason enough debt interest is aggregated.

P5

LayerZero bridging operations are implicitly capped

PROTOCOL-LEVEL-CONSIDERATION
info

In order to support a wide range of programmable blockchain environments (even non-EVM ones), all bridging operations through the OFT standard cast token uint values to a uint64 type representing a monetary amount with sharedDecimals number of decimals (default is 6): https://docs.layerzero.network/v2/concepts/technical-reference/oft-reference#1-transferring-value-across-different-vms

With this in place, the maximum value that can be bridged corresponds to roughly 18 trillion whole tokens. This shouldn’t realistically pose a problem but we explicitly note it here for the sake of completeness since Liquity-like systems have the potential to continuously mint BOLD tokens.

P6

Potential incompatibilities with the default ElevatedMinterBurner implementation

PROTOCOL-LEVEL-CONSIDERATION
info

Since ebUSD is already deployed and it supports minting and burning operations, the protocol will integrate with LayerZero by using the recommended mint/burn adapter pattern: https://docs.layerzero.network/v2/developers/evm/oft/quickstart#mint--burn-oft-adapter

The adapter will handle all LayerZero-related operations and invoke an authorized party to facilitate token burns and mints:

MintBurnOFTAdapter:81
function _debit(
address _from,
uint256 _amountLD,
uint256 _minAmountLD,
uint32 _dstEid
) internal virtual override returns (uint256 amountSentLD, uint256 amountReceivedLD) {
(amountSentLD, amountReceivedLD) = _debitView(_amountLD, _minAmountLD, _dstEid);
// Burns tokens from the caller.
minterBurner.burn(_from, amountSentLD);
}

...

function _credit(
address _to,
uint256 _amountLD,
uint32 /* _srcEid */
) internal virtual override returns (uint256 amountReceivedLD) {
if (_to == address(0x0)) _to = address(0xdead); // _mint(...) does not support address(0x0)
// Mints the tokens and transfers to the recipient.
minterBurner.mint(_to, _amountLD);
// In the case of NON-default OFTAdapter, the amountLD MIGHT not be equal to amountReceivedLD.
return _amountLD;
}


However, it should be noted that the recommended elevated contract to serve as the burner and minter in the documentation is not fully compatible with ebUSD:

\ElevatedMinterBurner
contract ElevatedMinterBurner is IMintableBurnable, Ownable {
IMintableBurnable public immutable token;
mapping(address => bool) public operators;

modifier onlyOperators() {
require(operators[msg.sender] || msg.sender == owner(), "Not authorized");
_;
}

constructor(IMintableBurnable _token, address _owner) Ownable(_owner) {
token = _token;
}

function setOperator(address _operator, bool _status) external onlyOwner {
operators[_operator] = _status;
}

function burn(address _from, uint256 _amount) external override onlyOperators returns (bool) {
return token.burn(_from, _amount);
}

function mint(address _to, uint256 _amount) external override onlyOperators returns (bool) {
return token.mint(_to, _amount);
}
}
  • ebUSD’s burn and mint do not return a boolean value
  • In the form above, ElevatedMinterBurner grants its owner the power (presumably the Ebisu admin) to instantly burn any user’s balance and instantly mint tokens to anyone. This would result in a notable increase in the protocol’s centralization degree.


CRITICAL SEVERITY

[No critical severity issues]


HIGH SEVERITY

[No high severity issues]


MEDIUM SEVERITY

[No medium severity issues]


LOW SEVERITY

[No low severity issues]


CENTRALIZATION CONSIDERATIONS

It is often desirable for DeFi protocols to assume no trust in a central authority, including the protocol’s owner. Even if the owner is reputable, users are more likely to engage with a protocol that guarantees no catastrophic failure even in the case the owner gets hacked/compromised. We list considerations of this kind below. (These items should be considered in the context of usage/deployment, as they are not uncommon. Several high-profile, high-value protocols have significant centralization threats.)

N1

Some entities are considered partially trusted

CENTRALIZATION
info

The ebisuAdmin does not only carry the trust assumptions previously noted at: Ebisu smart contract audit report | Dedaub Audit Reports, but the address is now appointed as CCIP manager of the protocol.

The CCIP manager has the ability to configure and pause/resume bridging directly in the CCIP core contracts (see TokenAdminRegistry and BurnMintTokenPool in the CCIP repository). The developers have made it so that the token pools appointed for minting and burning in the ebUSD contract cannot be arbitrarily changed once set without undergoing a full contract upgrade:

BoldToken::initializeCCIP:169
function initializeCCIP(address[] calldata _tokenPools) external virtual onlyEbisuAdmin {
if (ccipTokenPoolsLocked) revert CCIPTokenPoolsAlreadyLocked();

// Add all token pools
for (uint256 i = 0; i < _tokenPools.length; i++) {
require(_tokenPools[i] != address(0), "BoldToken: Invalid token pool address");
ccipTokenPools[_tokenPools[i]] = true;
emit CCIPTokenPoolAdded(_tokenPools[i]);
}

// Automatically lock to prevent future additions
ccipTokenPoolsLocked = true;
emit CCIPTokenPoolsLocked();
}


The initial configuration of CCIP has to be trusted. However, each contract upgrade can only take effect after a timelock period passes (7 days), so users will have a significant period to react to a malicious upgrade in the case of an account take-over after the initial configuration of CCIP.

With the integration of LayerZero’s OFT, we also note an additional centralization consideration in P6 if the protocol uses the recommended elevated minter/burner recommended in the LayerZero documentation.



OTHER / ADVISORY ISSUES

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

A1

Reenabling CCIP requires a full contract upgrade

ADVISORY
info

The intended flow(s) to configure CCIP minting in ebUSD consists of the following flow:

BoldToken:169
function initializeCCIP(address[] calldata _tokenPools) external virtual onlyEbisuAdmin {
if (ccipTokenPoolsLocked) revert CCIPTokenPoolsAlreadyLocked();

// Add all token pools
for (uint256 i = 0; i < _tokenPools.length; i++) {
require(_tokenPools[i] != address(0), "BoldToken: Invalid token pool address");
ccipTokenPools[_tokenPools[i]] = true;
emit CCIPTokenPoolAdded(_tokenPools[i]);
}

// Automatically lock to prevent future additions
ccipTokenPoolsLocked = true;
emit CCIPTokenPoolsLocked();
}
...
function removeCCIPTokenPool(address _tokenPool) external virtual onlyEbisuAdmin {
if (!ccipTokenPoolsLocked) revert CCIPTokenPoolsNotLocked();
ccipTokenPools[_tokenPool] = false;
emit CCIPTokenPoolRemoved(_tokenPool);
}
  • The Ebisu admin calls BoldToken::initializeCCIP and provides a set of addresses to be set as CCIP token pools. After this operation, token pools are locked as indicated by the ccipTokenPoolsLocked state variable.
  • Once pools are locked, the Ebisu admin has the ability to remove pools

Note that since ccipTokenPoolsLocked can never be unset once BoldToken::initializeCCIP is called, this means that if for whatever emergency all CCIP pools need to be removed, CCIP can only be reenabled afterwards by performing a full contract update.

The contract update will be free to reset ccipTokenPoolsLocked and use any additional logic required, but given that upgrades on Ebisu contracts cannot be instant, a delay in reenabling CCIP will be introduced at that scenario.

A2

CCIP can be initialized with no pools

ADVISORY
info

Because BoldToken::initializeCCIP does not perform any checks on the length of the _tokenPools argument, it possible to call it and have an empty array of pools be locked:

BoldToken::initializeCCIP:169
function initializeCCIP(address[] calldata _tokenPools) external virtual onlyEbisuAdmin {
if (ccipTokenPoolsLocked) revert CCIPTokenPoolsAlreadyLocked();

// Add all token pools
for (uint256 i = 0; i < _tokenPools.length; i++) {
require(_tokenPools[i] != address(0), "BoldToken: Invalid token pool address");
ccipTokenPools[_tokenPools[i]] = true;
emit CCIPTokenPoolAdded(_tokenPools[i]);
}

// Automatically lock to prevent future additions
ccipTokenPoolsLocked = true;
emit CCIPTokenPoolsLocked();
}


As noted in A1, in this scenario a full contract update will be required.

A3

Manual CCIP executions may be purposely constructed

ADVISORY
info

CCIP token pool messages are quite resilient to failures. As noted in the documentation: https://docs.chain.link/ccip/concepts/manual-execution , as long as burn/mint operations are within the 90000 gas limit execution is executed to succeed.

Even though total CCIP failures in which no CCIP messages get bridged are possible, we consider those cases to be unlikely to occur in practice.

However, it should be noted that users can craft their CCIP messages in a way that makes automatic execution on the destination chain fail.

While this should not be an issue directly affecting the protocol , it should not be assumed that all CCIP operations always succeed with respect to the application’s accounting. Otherwise, users can make their cross-chain messages fail, tricking the application’s accounting (e.g., UI tracking cross chain balances) and then use manual execution to complete the operations at a convenient time.

A4

Compiler bugs

ADVISORY
info

The code is compiled with Solidity 0.8.24. Version 0.8.24 has no known bugs.



DISCLAIMER

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.


ABOUT DEDAUB

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.