#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.
offchainTokenDataassociated with the Alice's message is consumed on theBridge.Alice's message is not consumed on
OffRamp.offchainTokenDataassociated 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.
Last updated
Was this helpful?