#38342 [SC-Medium] Interchanging `offchainTokenData` between two valid messages
Submitted on Dec 31st 2024 at 13:14:22 UTC by @security for Audit Comp | Lombard
Report ID: #38342
Report Type: Smart Contract
Report severity: Medium
Target: https://github.com/lombard-finance/evm-smart-contracts/blob/main/contracts/bridge/adapters/TokenPool.sol
Impacts:
Protocol insolvency
Temporary freezing of funds for at least 30 days
Griefing (e.g. no profit motive for an attacker, but damage to the users or the protocol (not lower than $1K))
Description
Brief/Intro
A vulnerability exists in the bridge message processing flow where offchainTokenData
is not strictly tied to the corresponding bridged message. This allows a malicious actor to interchange offchainTokenData
between two valid messages (e.g., using offchainTokenData
from message1 for message2), leading to unintended behaviour in the token release or minting process.
Vulnerability Details
During the bridging, the hash of required payload is attached to the transferred data. https://github.com/lombard-finance/evm-smart-contracts/blob/edd557006050ee5b847fa1cc67c1c4e19079437e/contracts/bridge/adapters/TokenPool.sol#L44
On the destination chain, the OffRamp
contract in Chainlink handles message delivery by invoking either the manuallyExecute
or execute
function. https://github.com/smartcontractkit/chainlink/blob/develop/contracts/src/v0.8/ccip/offRamp/OffRamp.sol#L274 https://github.com/smartcontractkit/chainlink/blob/develop/contracts/src/v0.8/ccip/offRamp/OffRamp.sol#L331
This triggers a series of internal calls to validate and process the message. The full flow of message delivery on the destination is as follows:
https://github.com/smartcontractkit/chainlink/blob/develop/contracts/src/v0.8/ccip/offRamp/OffRamp.sol#L274 https://github.com/smartcontractkit/chainlink/blob/develop/contracts/src/v0.8/ccip/offRamp/OffRamp.sol#L331 https://github.com/smartcontractkit/chainlink/blob/develop/contracts/src/v0.8/ccip/offRamp/OffRamp.sol#L345 https://github.com/smartcontractkit/chainlink/blob/develop/contracts/src/v0.8/ccip/offRamp/OffRamp.sol#L367 https://github.com/smartcontractkit/chainlink/blob/develop/contracts/src/v0.8/ccip/offRamp/OffRamp.sol#L540 https://github.com/smartcontractkit/chainlink/blob/develop/contracts/src/v0.8/ccip/offRamp/OffRamp.sol#L562 https://github.com/smartcontractkit/chainlink/blob/develop/contracts/src/v0.8/ccip/offRamp/OffRamp.sol#L744 https://github.com/smartcontractkit/chainlink/blob/develop/contracts/src/v0.8/ccip/offRamp/OffRamp.sol#L637 https://github.com/lombard-finance/evm-smart-contracts/blob/edd557006050ee5b847fa1cc67c1c4e19079437e/contracts/bridge/adapters/TokenPool.sol#L57 https://github.com/lombard-finance/evm-smart-contracts/blob/edd557006050ee5b847fa1cc67c1c4e19079437e/contracts/bridge/adapters/CLAdapter.sol#L200 https://github.com/lombard-finance/evm-smart-contracts/blob/edd557006050ee5b847fa1cc67c1c4e19079437e/contracts/bridge/Bridge.sol#L178 https://github.com/lombard-finance/evm-smart-contracts/blob/edd557006050ee5b847fa1cc67c1c4e19079437e/contracts/bridge/Bridge.sol#L220 https://github.com/lombard-finance/evm-smart-contracts/blob/edd557006050ee5b847fa1cc67c1c4e19079437e/contracts/bridge/Bridge.sol#L263
Both of these functions manuallyExecute
or execute
will end in verification phase to be sure that the bridged message is valid: https://github.com/smartcontractkit/chainlink/blob/develop/contracts/src/v0.8/ccip/offRamp/OffRamp.sol#L389-L424
The important thing is that the verification is done on report.messages
, while the report
has also another important piece of data offchainTokenData
. https://github.com/smartcontractkit/chainlink/blob/develop/contracts/src/v0.8/ccip/offRamp/OffRamp.sol#L389-L424
https://github.com/smartcontractkit/chainlink/blob/develop/contracts/src/v0.8/ccip/libraries/Internal.sol#L68 https://github.com/smartcontractkit/chainlink/blob/develop/contracts/src/v0.8/ccip/offRamp/OffRamp.sol#L368
This data offchainTokenData
carries off-chain data to process the release or mint in the TokenPool
. As you see, when releaseOrMint
is called in the TokenPool
, the offchainTokenData
is forwarded as a field in the struct ReleaseOrMintInV1
. https://github.com/smartcontractkit/chainlink/blob/develop/contracts/src/v0.8/ccip/offRamp/OffRamp.sol#L664-L683
https://github.com/lombard-finance/evm-smart-contracts/blob/edd557006050ee5b847fa1cc67c1c4e19079437e/contracts/bridge/adapters/TokenPool.sol#L57
https://github.com/smartcontractkit/chainlink/blob/498b0b8579ad52a8c394fe3cbf55d3a86a8e29a0/contracts/src/v0.8/ccip/libraries/Pool.sol#L48
Please note the comment: "offchainTokenData is untrusted data", showing that this data is not verified through Chainlink verification. So, it is the responsibility of the receiver protocol to validate it.
Its validation is done in the adapter through help of authority notarization by calling authNotary
. https://github.com/lombard-finance/evm-smart-contracts/blob/edd557006050ee5b847fa1cc67c1c4e19079437e/contracts/bridge/adapters/TokenPool.sol#L62
https://github.com/lombard-finance/evm-smart-contracts/blob/edd557006050ee5b847fa1cc67c1c4e19079437e/contracts/bridge/adapters/CLAdapter.sol#L200-L212
Issue
The issue is that the offchainTokenData
is not enforced to be related to the bridged message. In other words, suppose two valid messages are bridged and two corresponding offchainTokenData
are generated and validated by the authority. On the destination chain, if offchainTokenData
related to the first message is attached to the second message, and offchainTokenData
related to the second message is attached to the first message, both of these messages will be delivered and validated successfully. The key issue is that when the first message is processed on the destination chain, the offchainTokenData
associated with the second message is forwarded to the TokenPool
and subsequently processed on the Bridge. As a result, processing the first message ends up minting LBTC associated with the second message, while processing the second message mints LBTC associated with the first message.
Please note that, the action of swapping offchainTokenData
can be done easily by calling OffRamp::manuallyExecute
. In other words, one can call this function with the following parameters:
So that, the first message would be validated on Chainlink with provided proof. So, the status of the first message would be set as successful on Chainlink.
https://github.com/smartcontractkit/chainlink/blob/develop/contracts/src/v0.8/ccip/offRamp/OffRamp.sol#L499
But, since offchainTokenData
associated the second message is forwarded to TokenPool
, the intended amount of LBTC will be minted to the receiver address set in the offchainTokenData
associated with the second message.
It means that swapping offchainTokenData
does not change the intended amount sent to the receiver, but it changes the protocol procedure significantly in handling the messages, impacting on the user’s intended actions.
For better understand, please consider the following example:
Suppose Alice calls brdige::deposit
to bridge 1 LBTC to chainX. Bob calls brdige::deposit
to bridge 100 LBTC to chainX. The bridge is using CLAdapter
to handle the bridging mechanism.
Alice deposits 1 LBTC to be bridged
Bob deposits 100 LBTC to be bridged
The payload generated for Alice and Bob would be:
Alice's payload:
Bob's payload:
On chainX, Alice calls the function OffRamp::manuallyExecute
with the following parameters:
reports: [ ExecutionReport{ sourceChainSelector: // source chain messages: // the message related to Bob offchainTokenData: // the valid off-chain data related to Alice's message proofs: // the valid proof for Bob's message so that Chainlink can verify that Bob's message is delivered and processed securely proofFlagBits: // related to the provided proof for the Bob's message } ]
gasLimitOverrides
: // enough gas for executing the message delivery
Then, in the function OffRamp::_executeSingleReport
, the Bob's message will be verified with the provided proof, so that Chainlink ensures that the bridging is done securely. https://github.com/smartcontractkit/chainlink/blob/develop/contracts/src/v0.8/ccip/offRamp/OffRamp.sol#L424
Then, the function TokenPool::releaseOrMint
is called with the forwarded offchainTokenData
related to Alice's message. https://github.com/smartcontractkit/chainlink/blob/develop/contracts/src/v0.8/ccip/offRamp/OffRamp.sol#L676 https://github.com/lombard-finance/evm-smart-contracts/blob/edd557006050ee5b847fa1cc67c1c4e19079437e/contracts/bridge/adapters/TokenPool.sol#L64
Since the provided offchainTokenData
is valid, it will pass all the checks during the calls to functions Bridge::receivePayload
, Bridge::authNotary
, and Bridge::withdraw
. Finally, 1 LBTC would be minted to Alice on chainX, and Alice's message would be flagged as withdrawn
, so that it can not be retired on chainX. https://github.com/lombard-finance/evm-smart-contracts/blob/edd557006050ee5b847fa1cc67c1c4e19079437e/contracts/bridge/Bridge.sol#L312-L314
On OffRamp
, since the execution was successful, it sets the status of Bob's message as successful, so that it can not be later retried. https://github.com/smartcontractkit/chainlink/blob/develop/contracts/src/v0.8/ccip/offRamp/OffRamp.sol#L499
The final status would be:
Alice receives her 1 LBTC on chainX successfully.
offchainTokenData
associated with the Alice's message is consumed on theBridge
.Alice's message is not consumed on
OffRamp
.offchainTokenData
associated with the Bob's message is not consumed on theBridge
.Bob's message is consumed on
OffRamp
.Bob has not received his 100 LBTC.
Bob does not lose his 100 LBTC, but if OffRamp::manuallyExecute
or OffRamp::execute
are invoked with Bob's message and offchainTokenData
associated with Bob's message as parameters, its execution would be skipped with the event SkippedAlreadyExecutedMessage
as Bob's proof and message is already consumed in Chainlink by Alice. https://github.com/smartcontractkit/chainlink/blob/develop/contracts/src/v0.8/ccip/offRamp/OffRamp.sol#L437-L448
The root cause of this issue is that the offchainTokenData
is not enforced to be associated with the bridged message. The following check is missing:
https://github.com/lombard-finance/evm-smart-contracts/blob/edd557006050ee5b847fa1cc67c1c4e19079437e/contracts/bridge/adapters/TokenPool.sol#L57
https://github.com/lombard-finance/evm-smart-contracts/blob/edd557006050ee5b847fa1cc67c1c4e19079437e/contracts/bridge/adapters/TokenPool.sol#L57
Impact Details
Alice has consumed Bob's proof and message in Chainlink while supplying offchainTokenData
associated with her own message. As a result, Alice successfully receives her intended amount on the destination chain. However, Bob is now unable to use his own proof and message in Chainlink, as they have already been consumed by Alice. To recover his amounts, Bob would need to use Alice's proof and message in Chainlink while providing offchainTokenData
associated with his own message. This attack disrupts the intended functionality, particularly if Bob's transaction involved using CCIP to transfer tokens and interact with a contract on the destination chain (calling ccipReceive
), preventing him from executing the intended operation.
Protocol Insolvency
Temporary freezing of funds for at least 30 days
Griefing (e.g. no profit motive for an attacker, but damage to the users or the protocol (not lower than $1K))
References
It is worth noting that a similar issue was previously identified in an older audit conducted by Veridise on 17.12.2024, titled 4.1.5 V-CSC-VUL-005: Attacker can DoS CCIP messages that include LBTC transfers due to missing offchainTokenData validation
. This issue was resolved in commit 080220c
. However, due to extensive refactoring in the current protocol version, a similar vulnerability has re-emerged, and the risk is available.
Proof of Concept
PoC
Running the test offchain data not related to the payload
shows that offchainTokenData
is not enforced to be associated with the payload (i.e. the bridged message). Thus, it is possible to use offchainTokenData
for an unrelated message, to consume the message in OffRamp
.
Was this helpful?