Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Evm/per chain transceivers #517

Draft
wants to merge 14 commits into
base: main
Choose a base branch
from
Draft
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
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,12 @@ The payload includes the trimmed amount, together with the decimals that trimmed

NTT supports rate-limiting both on the sending and destination chains. If a transfer is rate-limited on the source chain and queueing is enabled via `shouldQueue = true`, transfers are placed into an outbound queue and can be released after the expiry of the rate limit duration. Transfers that are rate-limited on the destination chain are added to an inbound queue with a similar release delay.

## No-Rate-Limiting

Although the rate limiting feature can be disabled during contract instantiation, it still occupies code space in the `NttManager` contract. If you wish to free up code space for custom development, you can instead instantiate the
`NttManagerNoRateLimiting` contract. This contract is built without the bulk of the rate limiting code. Note that the current immutable checks do not allow modifying the rate-limiting parameters during migration. This means migrating
from an instance of `NttManager` with rate-limiting enabled to `NttManagerNoRateLimiting` or vice versa is not officially supported.

## Cancel-Flows

If users bridge frequently between a given source chain and destination chain, the capacity could be exhausted quickly. This can leave other users rate-limited, potentially delaying their transfers. To mitigate this issue, the outbound transfer cancels the inbound rate-limit on the source chain (refills the inbound rate-limit by an amount equal to that of the outbound transfer amount) and vice-versa, the inbound transfer cancels the outbound rate-limit on the destination chain (refills the outbound rate-limit by an amount equal to the inbound transfer amount).
Expand Down
12 changes: 10 additions & 2 deletions evm/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -226,9 +226,11 @@ cp WormholeNttConfig.json.sample WormholeNttConfig.json

Configure each network to your liking (including adding/removing networks). We will eventually add the addresses of the deployed contracts to this file. Navigate back to the `evm` directory.

___
---

⚠️ **WARNING:** Ensure that if the `NttManager` on the source chain is configured to be in `LOCKING` mode, the corresponding `NttManager`s on the target chains are configured to be in `BURNING` mode. If not, transfers will NOT go through and user funds may be lost! Proceed with caution!
___

---

Currently the per-chain `inBoundLimit` is set to zero by default. This means all inbound transfers will be queued by the rate limiter. Set this value accordingly.

Expand Down Expand Up @@ -267,3 +269,9 @@ bash sh/configure_wormhole_ntt.sh -n NETWORK_TYPE -c CHAIN_NAME -k PRIVATE_KEY
Tokens powered by NTT in **burn** mode require the `burn` method to be present. This method is not present in the standard ERC20 interface, but is found in the `ERC20Burnable` interface.

The `mint` and `setMinter` methods found in the [`INttToken` Interface](src/interfaces/INttToken.sol) are not found in the standard `ERC20` interface.

#### No-Rate-Limiting

Although the rate limiting feature can be disabled during contract instantiation, it still occupies code space in the `NttManager` contract. If you wish to free up code space for custom development, you can instead instantiate the
`NttManagerNoRateLimiting` contract. This contract is built without the bulk of the rate limiting code. Note that the current immutable checks do not allow modifying the rate-limiting parameters during migration. This means migrating
from an instance of `NttManager` with rate-limiting enabled to `NttManagerNoRateLimiting` or vice versa is not officially supported.
71 changes: 65 additions & 6 deletions evm/src/NttManager/ManagerBase.sol
Original file line number Diff line number Diff line change
Expand Up @@ -64,11 +64,11 @@ abstract contract ManagerBase is
bytes32 private constant MESSAGE_SEQUENCE_SLOT =
bytes32(uint256(keccak256("ntt.messageSequence")) - 1);

bytes32 private constant THRESHOLD_SLOT = bytes32(uint256(keccak256("ntt.threshold")) - 1);
bytes32 internal constant THRESHOLD_SLOT = bytes32(uint256(keccak256("ntt.threshold")) - 1);

// =============== Storage Getters/Setters ==============================================

function _getThresholdStorage() private pure returns (_Threshold storage $) {
function _getThresholdStorage() internal pure returns (_Threshold storage $) {
uint256 slot = uint256(THRESHOLD_SLOT);
assembly ("memory-safe") {
$.slot := slot
Expand Down Expand Up @@ -122,6 +122,9 @@ abstract contract ManagerBase is
uint256 totalPriceQuote = 0;
for (uint256 i = 0; i < numEnabledTransceivers; i++) {
address transceiverAddr = enabledTransceivers[i];
if (!_isSendTransceiverEnabledForChain(transceiverAddr, recipientChain)) {
continue;
}
uint8 registeredTransceiverIndex = transceiverInfos[transceiverAddr].index;
uint256 transceiverPriceQuote = ITransceiver(transceiverAddr).quoteDeliveryPrice(
recipientChain, transceiverInstructions[registeredTransceiverIndex]
Expand Down Expand Up @@ -150,6 +153,7 @@ abstract contract ManagerBase is
) {
revert TransceiverAlreadyAttestedToMessage(nttManagerMessageHash);
}
_getMessageAttestationsStorage()[nttManagerMessageHash].sourceChainId = sourceChainId;
_setTransceiverAttestedToMessage(nttManagerMessageHash, msg.sender);

return nttManagerMessageHash;
Expand Down Expand Up @@ -200,6 +204,9 @@ abstract contract ManagerBase is
// call into transceiver contracts to send the message
for (uint256 i = 0; i < numEnabledTransceivers; i++) {
address transceiverAddr = enabledTransceivers[i];
if (!_isSendTransceiverEnabledForChain(transceiverAddr, recipientChain)) {
continue;
}

// send it to the recipient nttManager based on the chain
ITransceiver(transceiverAddr).sendMessage{value: priceQuotes[i]}(
Expand Down Expand Up @@ -284,11 +291,19 @@ abstract contract ManagerBase is
return _getThresholdStorage().num;
}

/// @inheritdoc IManagerBase
function getPerChainThreshold(
uint16 // forChainId
) public view virtual returns (uint8) {
return _getThresholdStorage().num;
}

/// @inheritdoc IManagerBase
function isMessageApproved(
bytes32 digest
) public view returns (bool) {
uint8 threshold = getThreshold();
uint16 sourceChainId = _getMessageAttestationsStorage()[digest].sourceChainId;
uint8 threshold = getPerChainThreshold(sourceChainId);
return messageAttestations(digest) >= threshold && threshold > 0;
}

Expand Down Expand Up @@ -396,6 +411,22 @@ abstract contract ManagerBase is
_checkThresholdInvariants();
}

/// @inheritdoc IManagerBase
function enableSendTransceiverForChain(
address, // transceiver,
uint16 // forChainId
) external virtual onlyOwner {
revert NotImplemented();
}

/// @inheritdoc IManagerBase
function enableRecvTransceiverForChain(
address, // transceiver,
uint16 // forChainId
) external virtual onlyOwner {
revert NotImplemented();
}

/// @inheritdoc IManagerBase
function setThreshold(
uint8 threshold
Expand All @@ -413,6 +444,14 @@ abstract contract ManagerBase is
emit ThresholdChanged(oldThreshold, threshold);
}

/// @inheritdoc IManagerBase
function setPerChainThreshold(
uint16, // forChainId,
uint8 // threshold
) external virtual onlyOwner {
revert NotImplemented();
}

// =============== Internal ==============================================================

function _setTransceiverAttestedToMessage(bytes32 digest, uint8 index) internal {
Expand All @@ -432,8 +471,10 @@ abstract contract ManagerBase is
bytes32 digest
) internal view returns (uint64) {
uint64 enabledTransceiverBitmap = _getEnabledTransceiversBitmap();
return
_getMessageAttestationsStorage()[digest].attestedTransceivers & enabledTransceiverBitmap;
uint16 sourceChainId = _getMessageAttestationsStorage()[digest].sourceChainId;
uint64 enabledTransceiversForChain = _getEnabledRecvTransceiversForChain(sourceChainId);
return _getMessageAttestationsStorage()[digest].attestedTransceivers
& enabledTransceiverBitmap & enabledTransceiversForChain;
}

function _getEnabledTransceiverAttestedToMessage(
Expand Down Expand Up @@ -464,6 +505,19 @@ abstract contract ManagerBase is
_getMessageSequenceStorage().num++;
}

function _isSendTransceiverEnabledForChain(
address, // transceiver,
uint16 // chainId
) internal view virtual returns (bool) {
return true;
}

function _getEnabledRecvTransceiversForChain(
uint16 // forChainId
) internal view virtual returns (uint64 bitmap) {
return type(uint64).max;
}

/// ============== Invariants =============================================

/// @dev When we add new immutables, this function should be updated
Expand All @@ -482,7 +536,12 @@ abstract contract ManagerBase is
}

function _checkThresholdInvariants() internal view {
uint8 threshold = _getThresholdStorage().num;
_checkThresholdInvariants(_getThresholdStorage().num);
}

function _checkThresholdInvariants(
uint8 threshold
) internal pure {
_NumTransceivers memory numTransceivers = _getNumTransceiversStorage();

// invariant: threshold <= enabledTransceivers.length
Expand Down
150 changes: 95 additions & 55 deletions evm/src/NttManager/NttManager.sol
Original file line number Diff line number Diff line change
Expand Up @@ -223,33 +223,47 @@ contract NttManager is INttManager, RateLimiter, ManagerBase {

address transferRecipient = fromWormholeFormat(nativeTokenTransfer.to);

{
// Check inbound rate limits
bool isRateLimited = _isInboundAmountRateLimited(nativeTransferAmount, sourceChainId);
if (isRateLimited) {
// queue up the transfer
_enqueueInboundTransfer(digest, nativeTransferAmount, transferRecipient);

// end execution early
return;
}
bool enqueued = _enqueueOrConsumeInboundRateLimit(
digest, sourceChainId, nativeTransferAmount, transferRecipient
);

if (enqueued) {
return;
}

_mintOrUnlockToRecipient(digest, transferRecipient, nativeTransferAmount, false);
}

function _enqueueOrConsumeInboundRateLimit(
bytes32 digest,
uint16 sourceChainId,
TrimmedAmount nativeTransferAmount,
address transferRecipient
) internal virtual returns (bool) {
// Check inbound rate limits
bool isRateLimited = _isInboundAmountRateLimited(nativeTransferAmount, sourceChainId);
if (isRateLimited) {
// queue up the transfer
_enqueueInboundTransfer(digest, nativeTransferAmount, transferRecipient);

// end execution early
return true;
}

// consume the amount for the inbound rate limit
_consumeInboundAmount(nativeTransferAmount, sourceChainId);
// When receiving a transfer, we refill the outbound rate limit
// by the same amount (we call this "backflow")
_backfillOutboundAmount(nativeTransferAmount);

_mintOrUnlockToRecipient(digest, transferRecipient, nativeTransferAmount, false);
return false;
}

/// @inheritdoc INttManager
function completeInboundQueuedTransfer(
bytes32 digest
) external nonReentrant whenNotPaused {
) external virtual nonReentrant whenNotPaused {
// find the message in the queue
InboundQueuedTransfer memory queuedTransfer = getInboundQueuedTransfer(digest);
InboundQueuedTransfer memory queuedTransfer = RateLimiter.getInboundQueuedTransfer(digest);
if (queuedTransfer.txTimestamp == 0) {
revert InboundQueuedTransferNotFound(digest);
}
Expand All @@ -269,7 +283,7 @@ contract NttManager is INttManager, RateLimiter, ManagerBase {
/// @inheritdoc INttManager
function completeOutboundQueuedTransfer(
uint64 messageSequence
) external payable nonReentrant whenNotPaused returns (uint64) {
) external payable virtual nonReentrant whenNotPaused returns (uint64) {
// find the message in the queue
OutboundQueuedTransfer memory queuedTransfer = _getOutboundQueueStorage()[messageSequence];
if (queuedTransfer.txTimestamp == 0) {
Expand Down Expand Up @@ -299,7 +313,7 @@ contract NttManager is INttManager, RateLimiter, ManagerBase {
/// @inheritdoc INttManager
function cancelOutboundQueuedTransfer(
uint64 messageSequence
) external nonReentrant whenNotPaused {
) external virtual nonReentrant whenNotPaused {
// find the message in the queue
OutboundQueuedTransfer memory queuedTransfer = _getOutboundQueueStorage()[messageSequence];
if (queuedTransfer.txTimestamp == 0) {
Expand Down Expand Up @@ -382,50 +396,24 @@ contract NttManager is INttManager, RateLimiter, ManagerBase {

// trim amount after burning to ensure transfer amount matches (amount - fee)
TrimmedAmount trimmedAmount = _trimTransferAmount(amount, recipientChain);
TrimmedAmount internalAmount = trimmedAmount.shift(tokenDecimals());

// get the sequence for this transfer
uint64 sequence = _useMessageSequence();

{
// now check rate limits
bool isAmountRateLimited = _isOutboundAmountRateLimited(internalAmount);
if (!shouldQueue && isAmountRateLimited) {
revert NotEnoughCapacity(getCurrentOutboundCapacity(), amount);
}
if (shouldQueue && isAmountRateLimited) {
// verify chain has not forked
checkFork(evmChainId);

// emit an event to notify the user that the transfer is rate limited
emit OutboundTransferRateLimited(
msg.sender, sequence, amount, getCurrentOutboundCapacity()
);

// queue up and return
_enqueueOutboundTransfer(
sequence,
trimmedAmount,
recipientChain,
recipient,
refundAddress,
msg.sender,
transceiverInstructions
);

// refund price quote back to sender
_refundToSender(msg.value);

// return the sequence in the queue
return sequence;
}
}
bool enqueued = _enqueueOrConsumeOutboundRateLimit(
amount,
recipientChain,
recipient,
refundAddress,
shouldQueue,
transceiverInstructions,
trimmedAmount,
sequence
);

// otherwise, consume the outbound amount
_consumeOutboundAmount(internalAmount);
// When sending a transfer, we refill the inbound rate limit for
// that chain by the same amount (we call this "backflow")
_backfillInboundAmount(internalAmount, recipientChain);
if (enqueued) {
return sequence;
}

return _transfer(
sequence,
Expand All @@ -438,6 +426,58 @@ contract NttManager is INttManager, RateLimiter, ManagerBase {
);
}

function _enqueueOrConsumeOutboundRateLimit(
uint256 amount,
uint16 recipientChain,
bytes32 recipient,
bytes32 refundAddress,
bool shouldQueue,
bytes memory transceiverInstructions,
TrimmedAmount trimmedAmount,
uint64 sequence
) internal virtual returns (bool enqueued) {
TrimmedAmount internalAmount = trimmedAmount.shift(tokenDecimals());

// now check rate limits
bool isAmountRateLimited = _isOutboundAmountRateLimited(internalAmount);
if (!shouldQueue && isAmountRateLimited) {
revert NotEnoughCapacity(getCurrentOutboundCapacity(), amount);
}
if (shouldQueue && isAmountRateLimited) {
// verify chain has not forked
checkFork(evmChainId);

// emit an event to notify the user that the transfer is rate limited
emit OutboundTransferRateLimited(
msg.sender, sequence, amount, getCurrentOutboundCapacity()
);

// queue up and return
_enqueueOutboundTransfer(
sequence,
trimmedAmount,
recipientChain,
recipient,
refundAddress,
msg.sender,
transceiverInstructions
);

// refund price quote back to sender
_refundToSender(msg.value);

// return that the transfer has been enqued
return true;
}

// otherwise, consume the outbound amount
_consumeOutboundAmount(internalAmount);
// When sending a transfer, we refill the inbound rate limit for
// that chain by the same amount (we call this "backflow")
_backfillInboundAmount(internalAmount, recipientChain);
return false;
}

function _transfer(
uint64 sequence,
TrimmedAmount amount,
Expand Down
Loading
Loading