Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 28 additions & 20 deletions script/FullDeployer.s.sol
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ import {SyncDepositVaultFactory} from "../src/vaults/factories/SyncDepositVaultF

import "forge-std/Script.sol";

import {TokenBridge} from "../src/bridge/TokenBridge.sol";
import {SubsidyManager} from "../src/utils/SubsidyManager.sol";
import {AxelarAdapter} from "../src/adapters/AxelarAdapter.sol";
import {WormholeAdapter} from "../src/adapters/WormholeAdapter.sol";
Expand Down Expand Up @@ -101,6 +102,7 @@ struct FullReport {
TokenRecoverer tokenRecoverer;
ProtocolGuardian protocolGuardian;
OpsGuardian opsGuardian;
TokenBridge tokenBridge;
SubsidyManager subsidyManager;
RefundEscrowFactory refundEscrowFactory;
AsyncVaultFactory asyncVaultFactory;
Expand Down Expand Up @@ -141,6 +143,7 @@ contract FullActionBatcher is CoreActionBatcher {
) public onlyDeployer {
// Rely Root
report.tokenRecoverer.rely(address(report.root));
report.tokenBridge.rely(address(report.root));

report.subsidyManager.rely(address(report.root));
report.refundEscrowFactory.rely(address(report.root));
Expand All @@ -157,10 +160,7 @@ contract FullActionBatcher is CoreActionBatcher {

report.batchRequestManager.rely(address(report.root));

if (address(report.layerZeroAdapter) != address(0)) report.layerZeroAdapter.rely(address(report.root));
if (address(report.wormholeAdapter) != address(0)) report.wormholeAdapter.rely(address(report.root));
if (address(report.axelarAdapter) != address(0)) report.axelarAdapter.rely(address(report.root));
if (address(report.chainlinkAdapter) != address(0)) report.chainlinkAdapter.rely(address(report.root));
_relyAdapters(report, address(report.root));

// Rely spoke
report.asyncRequestManager.rely(address(report.core.spoke));
Expand All @@ -183,26 +183,15 @@ contract FullActionBatcher is CoreActionBatcher {
report.core.messageDispatcher.rely(address(report.protocolGuardian));
report.root.rely(address(report.protocolGuardian));
report.tokenRecoverer.rely(address(report.protocolGuardian));
report.tokenBridge.rely(address(report.protocolGuardian));
// Permanent ward for ongoing adapter maintenance
if (address(report.layerZeroAdapter) != address(0)) {
report.layerZeroAdapter.rely(address(report.protocolGuardian));
}
if (address(report.wormholeAdapter) != address(0)) {
report.wormholeAdapter.rely(address(report.protocolGuardian));
}
if (address(report.axelarAdapter) != address(0)) report.axelarAdapter.rely(address(report.protocolGuardian));
if (address(report.chainlinkAdapter) != address(0)) {
report.chainlinkAdapter.rely(address(report.protocolGuardian));
}
_relyAdapters(report, address(report.protocolGuardian));

// Rely opsGuardian
report.core.multiAdapter.rely(address(report.opsGuardian));
report.core.hub.rely(address(report.opsGuardian));
// Temporal ward for initial adapter wiring
if (address(report.layerZeroAdapter) != address(0)) report.layerZeroAdapter.rely(address(report.opsGuardian));
if (address(report.wormholeAdapter) != address(0)) report.wormholeAdapter.rely(address(report.opsGuardian));
if (address(report.axelarAdapter) != address(0)) report.axelarAdapter.rely(address(report.opsGuardian));
if (address(report.chainlinkAdapter) != address(0)) report.chainlinkAdapter.rely(address(report.opsGuardian));
_relyAdapters(report, address(report.opsGuardian));

// Rely tokenRecoverer
report.root.rely(address(report.tokenRecoverer));
Expand Down Expand Up @@ -261,10 +250,10 @@ contract FullActionBatcher is CoreActionBatcher {
report.batchRequestManager.file("hub", address(report.core.hub));

// Endorse methods

report.root.endorse(address(report.core.balanceSheet));
report.root.endorse(address(report.asyncRequestManager));
report.root.endorse(address(report.vaultRouter));
report.root.endorse(address(report.tokenBridge));

// Connect adapters
for (uint256 i; i < connectionList.length; i++) {
Expand Down Expand Up @@ -326,6 +315,7 @@ contract FullActionBatcher is CoreActionBatcher {
function revokeFull(FullReport memory report) public onlyDeployer {
if (report.root.wards(address(this)) == 1) report.root.deny(address(this));
report.tokenRecoverer.deny(address(this));
report.tokenBridge.deny(address(this));

report.refundEscrowFactory.deny(address(this));
report.asyncVaultFactory.deny(address(this));
Expand All @@ -348,6 +338,14 @@ contract FullActionBatcher is CoreActionBatcher {
if (address(report.chainlinkAdapter) != address(0)) report.chainlinkAdapter.deny(address(this));
}

/// @dev helper function to save some bytes
function _relyAdapters(FullReport memory report, address ward) internal {
if (address(report.layerZeroAdapter) != address(0)) report.layerZeroAdapter.rely(ward);
if (address(report.wormholeAdapter) != address(0)) report.wormholeAdapter.rely(ward);
if (address(report.axelarAdapter) != address(0)) report.axelarAdapter.rely(ward);
if (address(report.chainlinkAdapter) != address(0)) report.chainlinkAdapter.rely(ward);
}
Comment on lines +342 to +347
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice idea to reduce bytecode!


function _setLayerZeroUlnConfig(LayerZeroAdapter adapter, uint32 eid, SetConfigParam[] memory params) internal {
ILayerZeroEndpointV2Like endpoint = ILayerZeroEndpointV2Like(address(adapter.endpoint()));
address oapp = address(adapter);
Expand Down Expand Up @@ -375,6 +373,7 @@ contract FullDeployer is CoreDeployer {
TokenRecoverer public tokenRecoverer;
ProtocolGuardian public protocolGuardian;
OpsGuardian public opsGuardian;
TokenBridge public tokenBridge;

SubsidyManager public subsidyManager;
RefundEscrowFactory public refundEscrowFactory;
Expand Down Expand Up @@ -426,12 +425,19 @@ contract FullDeployer is CoreDeployer {
)
);

tokenBridge = TokenBridge(
create3(
generateSalt("tokenBridge"),
abi.encodePacked(type(TokenBridge).creationCode, abi.encode(spoke, batcher))
)
);

protocolGuardian = ProtocolGuardian(
create3(
generateSalt("protocolGuardian"),
abi.encodePacked(
type(ProtocolGuardian).creationCode,
abi.encode(ISafe(address(batcher)), root, gateway, messageDispatcher)
abi.encode(ISafe(address(batcher)), root, gateway, messageDispatcher, tokenBridge)
)
)
);
Expand Down Expand Up @@ -701,6 +707,7 @@ contract FullDeployer is CoreDeployer {
register("tokenRecoverer", address(tokenRecoverer));
register("protocolGuardian", address(protocolGuardian));
register("opsGuardian", address(opsGuardian));
register("tokenBridge", address(tokenBridge));

register("refundEscrowFactory", address(refundEscrowFactory));
register("subsidyManager", address(subsidyManager));
Expand Down Expand Up @@ -751,6 +758,7 @@ contract FullDeployer is CoreDeployer {
tokenRecoverer,
protocolGuardian,
opsGuardian,
tokenBridge,
subsidyManager,
refundEscrowFactory,
asyncVaultFactory,
Expand Down
29 changes: 27 additions & 2 deletions src/admin/ProtocolGuardian.sol
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import {PoolId} from "../core/types/PoolId.sol";
import {IGateway} from "../core/messaging/interfaces/IGateway.sol";
import {IScheduleAuthMessageSender} from "../core/messaging/interfaces/IGatewaySenders.sol";

import {ITokenBridge} from "../bridge/interfaces/ITokenBridge.sol";

/// @title ProtocolGuardian
/// @notice This contract provides emergency controls and protocol-level management including pausing,
/// permission scheduling, cross-chain upgrade coordination, and adapter configuration.
Expand All @@ -23,12 +25,20 @@ contract ProtocolGuardian is IProtocolGuardian {
ISafe public safe;
IGateway public gateway;
IScheduleAuthMessageSender public sender;

constructor(ISafe safe_, IRoot root_, IGateway gateway_, IScheduleAuthMessageSender sender_) {
ITokenBridge public tokenBridge;

constructor(
ISafe safe_,
IRoot root_,
IGateway gateway_,
IScheduleAuthMessageSender sender_,
ITokenBridge tokenBridge_
) {
safe = safe_;
root = root_;
gateway = gateway_;
sender = sender_;
tokenBridge = tokenBridge_;
}

modifier onlySafe() {
Expand All @@ -50,6 +60,7 @@ contract ProtocolGuardian is IProtocolGuardian {
if (what == "safe") safe = ISafe(data);
else if (what == "gateway") gateway = IGateway(data);
else if (what == "sender") sender = IScheduleAuthMessageSender(data);
else if (what == "tokenBridge") tokenBridge = ITokenBridge(data);
else revert FileUnrecognizedParam();
emit File(what, data);
}
Expand Down Expand Up @@ -116,6 +127,20 @@ contract ProtocolGuardian is IProtocolGuardian {
gateway.blockOutgoing(centrifugeId, GLOBAL_POOL, isBlocked);
}

//----------------------------------------------------------------------------------------------
// TokenBridge Management
//----------------------------------------------------------------------------------------------

/// @inheritdoc IProtocolGuardian
function fileTokenBridgeRelayer(address relayer) external onlySafe {
tokenBridge.file("relayer", relayer);
}

/// @inheritdoc IProtocolGuardian
function fileTokenBridgeCentrifugeId(uint256 evmChainId, uint16 centrifugeId) external onlySafe {
tokenBridge.file("centrifugeId", evmChainId, centrifugeId);
}

//----------------------------------------------------------------------------------------------
// Helpers
//----------------------------------------------------------------------------------------------
Expand Down
9 changes: 9 additions & 0 deletions src/admin/interfaces/IProtocolGuardian.sol
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,15 @@ interface IProtocolGuardian {
/// @param isBlocked True to block outgoing messages, false to unblock
function blockOutgoing(uint16 centrifugeId, bool isBlocked) external;

/// @notice Configure TokenBridge relayer address
/// @param relayer The relayer address to set
function fileTokenBridgeRelayer(address relayer) external;

/// @notice Configure TokenBridge chain ID mapping
/// @param evmChainId The EVM chain ID
/// @param centrifugeId The corresponding Centrifuge chain ID
function fileTokenBridgeCentrifugeId(uint256 evmChainId, uint16 centrifugeId) external;

/// @notice Updates a contract parameter
/// @param what Accepts a bytes32 representation of 'safe', 'gateway', or 'sender'
/// @param data New value for the parameter
Expand Down
96 changes: 96 additions & 0 deletions src/bridge/TokenBridge.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
// SPDX-License-Identifier: BUSL-1.1
pragma solidity 0.8.28;

import {ITokenBridge} from "./interfaces/ITokenBridge.sol";

import {Auth} from "../misc/Auth.sol";
import {IERC20} from "../misc/interfaces/IERC20.sol";
import {SafeTransferLib} from "../misc/libraries/SafeTransferLib.sol";

import {PoolId} from "../core/types/PoolId.sol";
import {ISpoke} from "../core/spoke/interfaces/ISpoke.sol";
import {ShareClassId} from "../core/types/ShareClassId.sol";
import {ITrustedContractUpdate} from "../core/utils/interfaces/IContractUpdate.sol";

/// @title TokenBridge
/// @notice Wrapper contract for cross-chain token transfers compatible with Glacis Airlift
contract TokenBridge is Auth, ITokenBridge {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Q for all of us: Should this contract implement Recoverable to protect against accidently sent tokens?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like it's never used as a target for receiving tokens. I would say not (?)

ISpoke public immutable spoke;

address public relayer;
mapping(PoolId => mapping(ShareClassId => GasLimits)) public gasLimits;
mapping(uint256 evmChainId => uint16 centrifugeId) public chainIdToCentrifugeId;

constructor(ISpoke spoke_, address deployer) Auth(deployer) {
spoke = spoke_;
}

//----------------------------------------------------------------------------------------------
// Administration
//----------------------------------------------------------------------------------------------

/// @inheritdoc ITokenBridge
function file(bytes32 what, address data) external auth {
if (what == "relayer") relayer = data;
else revert FileUnrecognizedParam();
emit File(what, data);
}

/// @inheritdoc ITokenBridge
function file(bytes32 what, uint256 evmChainId, uint16 centrifugeId) external auth {
if (what == "centrifugeId") chainIdToCentrifugeId[evmChainId] = centrifugeId;
else revert FileUnrecognizedParam();
emit File(what, evmChainId, centrifugeId);
}
Comment on lines +39 to +44
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

From my experience, file() methods for things other than dependencies can be somewhat messy in the end and difficult to handle. Could we use here a "normal" name like setDestination(evmChainId, centrifugeId) or something similar?


/// @inheritdoc ITrustedContractUpdate
function trustedCall(PoolId poolId, ShareClassId scId, bytes memory payload) external auth {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: We can use calldata payload. I discovered this as part of optimizing ARM size code. We still have several code paths in the repo using memory despite the interface declaration of calldata.

Suggested change
function trustedCall(PoolId poolId, ShareClassId scId, bytes memory payload) external auth {
function trustedCall(PoolId poolId, ShareClassId scId, bytes calldata payload) external auth {

uint8 kindValue = abi.decode(payload, (uint8));
require(kindValue <= uint8(type(TrustedCall).max), UnknownTrustedCall());

TrustedCall kind = TrustedCall(kindValue);
if (kind == TrustedCall.SetGasLimits) {
(, uint128 extraGasLimit, uint128 remoteExtraGasLimit) = abi.decode(payload, (uint8, uint128, uint128));

require(address(spoke.shareToken(poolId, scId)) != address(0), ShareTokenDoesNotExist());

gasLimits[poolId][scId] = GasLimits(extraGasLimit, remoteExtraGasLimit);
emit UpdateGasLimits(poolId, scId, extraGasLimit, remoteExtraGasLimit);
}
}

//----------------------------------------------------------------------------------------------
// Bridging
//----------------------------------------------------------------------------------------------

/// @inheritdoc ITokenBridge
function send(address token, uint256 amount, bytes32 receiver, uint256 destinationChainId, address refundAddress)
public
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this really be public or instead external?

payable
returns (bytes memory)
{
uint16 centrifugeId = chainIdToCentrifugeId[destinationChainId];
require(centrifugeId != 0, InvalidChainId());

SafeTransferLib.safeTransferFrom(token, msg.sender, address(this), amount);
if (IERC20(token).allowance(address(this), address(spoke)) == 0) {
SafeTransferLib.safeApprove(token, address(spoke), type(uint256).max);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am curious why this is necessary. I might be missing something but AFAICT the Spoke moves TokenBridge's tokens only using its ward privilege not via a standard ERC20 allowance and because it passes msg.sender as both the sender and from arguments, the allowance is never checked?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I also think the allowance is never checked. I think approval is something the user of TokenBridge should do before call to TokenBridge for leave TokenBridge to handle the transfer funds.

}

(PoolId poolId, ShareClassId scId) = spoke.shareTokenDetails(token);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: We should move this up before the actual transfer because it is theoretically fallible such that we can save costs in case this check fails.

GasLimits memory limits = gasLimits[poolId][scId];

spoke.crosschainTransferShares{value: msg.value}(
centrifugeId,
poolId,
scId,
receiver,
uint128(amount),
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CR: Should use safe method from Mathlib to improve debugging in case this edge case occurs

Suggested change
uint128(amount),
amount.toUint128(),

limits.extraGasLimit,
limits.remoteExtraGasLimit,
relayer != address(0) ? relayer : refundAddress // Transfer remaining ETH to relayer if set
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not always to the refundAddress? No fully understand the need of the relayer, could you extend?

);

return bytes("");
}
}
72 changes: 72 additions & 0 deletions src/bridge/interfaces/ITokenBridge.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
// SPDX-License-Identifier: GPL-2.0-or-later
pragma solidity >=0.5.0;

import {PoolId} from "../../core/types/PoolId.sol";
import {ShareClassId} from "../../core/types/ShareClassId.sol";
import {ITrustedContractUpdate} from "../../core/utils/interfaces/IContractUpdate.sol";

interface ITokenBridge is ITrustedContractUpdate {
event File(bytes32 indexed what, address data);
event File(bytes32 indexed what, uint256 evmChainId, uint16 centrifugeId);
event UpdateGasLimits(
PoolId indexed poolId, ShareClassId indexed scId, uint128 extraGasLimit, uint128 remoteExtraGasLimit
);

error FileUnrecognizedParam();
error InvalidChainId();
error InvalidRelayer();
Comment on lines +16 to +17
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wanted to flag these are not yet used in the codebase

error InvalidToken();
error UnknownTrustedCall();
error ShareTokenDoesNotExist();
error FailedToTransferToRelayer();
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here


enum TrustedCall {
SetGasLimits
}

struct GasLimits {
uint128 extraGasLimit;
uint128 remoteExtraGasLimit;
}

//----------------------------------------------------------------------------------------------
// Administration
//----------------------------------------------------------------------------------------------

/// @notice Configure contract parameters
/// @param what The parameter name to configure
/// @param data The address value to set
function file(bytes32 what, address data) external;

/// @notice Configure chain ID mapping
/// @param what Must be "centrifugeId"
/// @param evmChainId The EVM chain ID
/// @param centrifugeId The corresponding Centrifuge chain ID
function file(bytes32 what, uint256 evmChainId, uint16 centrifugeId) external;

//----------------------------------------------------------------------------------------------
// Bridging
//----------------------------------------------------------------------------------------------

/// @notice Send a token from chain A to chain B after approving this contract with the token
/// @dev This function transfers tokens from the caller and initiates a cross-chain transfer
/// @dev These methods match the expected interface from Glacis Airlift for cross-chain token transfers
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe then we should move send(..) to its own interface like IGlacisAirliftTransfer or so? to reflect that and ensure this method signature remains immutable.

Do you have a reference for where this method is defined?

/// @param token The address of the token sending across chains
/// @param amount The amount of the token to send across chains
/// @param receiver The target address that should receive the funds on the destination chain
/// @param destinationChainId The Ethereum chain ID of the destination chain
/// @param refundAddress The address that should receive any funds if the cross-chain gas value is too high
/// @return sendResponse The response from the token's handler function (not standardized)
function send(address token, uint256 amount, bytes32 receiver, uint256 destinationChainId, address refundAddress)
external
payable
returns (bytes memory);

//----------------------------------------------------------------------------------------------
// View methods
//----------------------------------------------------------------------------------------------

/// @notice Returns the relayer address
/// @return The relayer address
function relayer() external view returns (address);
}
Loading
Loading