Assessment reports>USDT OFT>Threat Model>Credits

Credits

The code implements a multi-adapter OFT, which allows an OFT to have multiple lock/unlock adapter pools.

Summary of operations

Disregarding fees, the effects of the possible operations are as follows:

Sending OFTs

  • Chain A:

    • decreases dstEid credits

    • increases localEid credits

    • increases contractBalance

  • Chain B:

    • decreases contractBalance

withdrawRemote

  • Chain A:

    • decreases dstEid credits

  • Chain B:

    • decreases contractBalance

withdrawLocal

  • Chain A:

    • decreases localEid credits and decreases contractBalance

depositLocal

  • Chain A:

    • increases localEid credits and increases contractBalance

An invariant exists, on each chain:

contractBalance - localEid credits = sum(on all other chains, the credits allocated to that one chain)

Fees

In the following code, amountSent is the amount the user paid, and amountReceived is the amount after subtracting fees.

function send(
    SendParam calldata _sendParam,
    MessagingFee calldata _fee,
    address _refundAddress
) external payable returns (MessagingReceipt memory msgReceipt, OFTReceipt memory oftReceipt) {
    // @dev Applies fees and determines the total amount to deduct from the sender, and the amount received
    (uint256 amountSent, uint256 amountReceived) = _debitView(_sendParam.amountLD, _sendParam.minAmountLD);

    // @dev Lock tokens by moving them into this contract from the caller.
    innerToken.safeTransferFrom(msg.sender, address(this), amountSent);

    // @dev Handle the credits
!    _decreaseCredits(_sendParam.dstEid, amountReceived);
!    _increaseCredits(LOCAL_EID, amountReceived);

When a user sends an OFT, they are essentially telling the destination chain to transfer an amount to xyz.

It decreases dstEid credits, because those basically represent the question, "how much is my chain allowed to ask the destination chain to transfer?" It does not make sense to reduce this by the fee because the fee is paid for in the local token.

It increases localEid credits. Those basically represent the question, "how much of the local token on this chain are we allowed to transfer?" If the code also increased localEid credits by the fee, then it would be possible to withdrawLocal the fee too, for example.

The TON implementation has the same behavior regarding fees. Note that on TON, however, amountSent is actually the amount after subtracting fees — which is named amountReceived on EVM.

(int, int) _getAmountAndFee(cell $storage, int totalAmount) impure inline {
    int feeBps = $storage.UsdtOFT::getFeeBps();
    ;; totalAmount is always at most 128-bits, feeBps is at most 10000 == 13 bits
    ;; so this multiplication can never overflow signed 257-bit
    int fee = ((totalAmount * feeBps) / BPS_DENOMINATOR);
    int amount = totalAmount - fee;
    return (amount, fee);
}

tuple sendOFT(cell $OFTSend) impure inline {
    (cell $storage, tuple actions) = preamble();
    ;; [...]
    try {
        (int amountSent, int fee) = _getAmountAndFee($storage, totalAmount);
    } catch {
        ;; [...]
    }
    ;; [...]
}
Zellic © 2025Back to top ↑