You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
In the AtomicBridgeInitiatorMOVE.sol contract, the poolBalance variable that is supposed to track the current amount of locked tokens in the bridge is used in withdrawMOVEand can prevent a withdrawal (moveToken.transfer).
function withdrawMOVE(addressrecipient, uint256amount) external {
if (msg.sender!= counterpartyAddress) revertUnauthorized();
if (poolBalance < amount) revertInsufficientMOVEBalance();
poolBalance -= amount;
if (!moveToken.transfer(recipient, amount)) revertMOVETransferFailed();
}
Warning
We have no guarantee that the poolBalance tracks the number of tokens locked in the bridge via bridge transfers. The moveToken is an ERC-20 token, and there could be transfers to and from the account that do not go through the bridge. Examples are if the bridge is compromised we may have to move MOVE tokens in or out manually.
as a result, the test poolBalance >= amount does not guarantee that the transfer (next line) will be successful.
the fact that poolBalance and the actual balance in the bridge moveToken.Balance may be different could create some issues: assume the bridge is compromised, the poolBalance is zero, and we have to manually refill with say $1B MOVE tokens. Because amount is zero, users won't be able to withdraw (bridge back from L2) even if there is $1B MOVE in the bridge, which is probably not what is expected as we just refilled the bridge.
function completeBridgeTransfer(bytes32bridgeTransferId, bytes32preImage) external {
BridgeTransferDetails storage details = bridgeTransfers[bridgeTransferId];
if (details.state != MessageState.PENDING) revertBridgeTransferStateNotPending();
bytes32 computedHash =keccak256(abi.encodePacked(preImage));
if (computedHash != details.hashLock) revertInvalidSecret();
if (block.timestamp> details.timeLock) revertTimeLockExpired();
details.state = MessageState.COMPLETED;
atomicBridgeInitiatorMOVE.withdrawMOVE(details.recipient, details.amount);
emitBridgeTransferCompleted(bridgeTransferId, preImage);
}
This results in at least two issues:
[Issue 1] the guard poolBalance >= amount may prevent use from fixing a compromised bridge
[Issue 2] the AtomicBridgeCounterpartyMOVE.sol contract uses a contract call to unlock the tokens:
a) this is not gas-efficient and
b) creates a dependency between the two contracts; if withdraw in AtomicBridgeInitiatorMOVE.sol is modified, this impacts the behaviour of the counter party.
Possible changes
One thing that should probably be done is: remove the lineif (poolBalance < amount) revert InsufficientMOVEBalance(); in the AtomicBridgeInitiatorMOVE.sol contract as it does not serve any purpose and can be dangerous (Issue 1 above).
Note
We can keep the poolBalance variable to track what has been locked and unlocked in the bridge (which may be different to the balance of the contract). Option 2 below proposes an alternative solution.
Once this is done we have two options to address the cross-contract call:
keep the poolBalanceand merge the code of the two contracts
Option 1 has one advantage: it is easy to implement, but a serious drawback as we may not be able to track the bridge poolBalance (or anything related to how much went in and out the bridge).
Option 2 above has two advantages:
we are able to track what goes in and out of the bridge, and the bridge poolBalance can be reconstructed from poolLocked - poolUnlocked
// Total MOVE token transferred out of the bridge on L1 (unlocked)uint256public poolUnlocked;
function completeBridgeTransfer(bytes32bridgeTransferId, bytes32preImage) external {
// Only bridge transfer operator can perform the unlockrequires(msg.sender== counterpartyAddress, Unauthorized());
// Check bridge transfer details
BridgeTransferDetails storage details = bridgeTransfers[bridgeTransferId];
// Must not have been completed yetrequires(details.state == MessageState.PENDING, BridgeTransferStateNotPending();
// Secret is known and deadline (timeLock) not elapsedbytes32 computedHash =keccak256(abi.encodePacked(preImage));
requires(computedHash == details.hashLock, InvalidSecret());
requires(block.timestamp<= details.timeLock, TimeLockExpired());
// Mark transfer as complete
details.state = MessageState.COMPLETED;
// Update amount of unlock
poolUnlocked += amount;
// Try to transfer to recipient or revertif (!moveToken.transfer(recipient, amount)) revertMOVETransferFailed();
emitBridgeTransferCompleted(bridgeTransferId, preImage);
}
Validation
We need to add some unit tests to test that:
reverting conditions are correctly triggered (the calls revert when we expect them to)
the poolLocked and poolUnlocked variables are updated properly (and this for each function in each contract).
The text was updated successfully, but these errors were encountered:
franck44
changed the title
[Bridge] [Solidity contracts] Pool balance used to guard withdrawMOVE function
[Bridge] [Solidity] Pool balance used to guard withdrawMOVE function
Nov 11, 2024
On L1:
poolUnlocked is the difference between 10B (max supply) and current balance and poolLocked is the balance of bridgeInitiator Contract
On L2:
poolUnlocked is the current supply and poolLocked is the difference between 10B (max supply) and the current supply. Don't think adding more params helps us, we could these constants for extra care.
One issue I can foresee is if there's an exploit, using poolUnlocked and poolLocked will lead to bugs as invalid minting or removal of tokens would lead to an unbalance between the tokens.
note: poolBalance has already shown to bug and ERC20() _transfer already handles the logic it was meant to serve. removal of poolBalance has been merged into Solidity Bridge Deployment Scripts
One issue I can foresee is if there's an exploit, using poolUnlocked and poolLocked will lead to bugs as invalid minting or removal of tokens would lead to an unbalance between the tokens.
How can variables that are just updated but do not change the control flow create bugs?
The poolLocked and poolUnlocked are not related to minting (this is in the ERC-20).
This comment does not seem relevant here.
note: poolBalance has already shown to bug and ERC20() _transfer already handles the logic it was meant to serve. > removal of poolBalance has been merged into #681
How can a variable in a bridge contract "bug" another contract (an ERC-20)?
it bugs, it doesn't bug another contract, it's buggy by itself because we are not implementing a full logic and the full logic is simply implemented by ERC20. They are attempting to perform the same logic, but currently do not do so.
It's redudant logic.
Problem
In the AtomicBridgeInitiatorMOVE.sol contract, the
poolBalance
variable that is supposed to track the current amount of locked tokens in the bridge is used in withdrawMOVEand can prevent a withdrawal (moveToken.transfer
).Warning
poolBalance
tracks the number of tokens locked in the bridge via bridge transfers. ThemoveToken
is an ERC-20 token, and there could be transfers to and from the account that do not go through the bridge. Examples are if the bridge is compromised we may have to move MOVE tokens in or out manually.poolBalance >= amount
does not guarantee that the transfer (next line) will be successful.poolBalance
and the actual balance in the bridgemoveToken.Balance
may be different could create some issues: assume the bridge is compromised, thepoolBalance
is zero, and we have to manually refill with say $1B MOVE tokens. Becauseamount
is zero, users won't be able towithdraw
(bridge back from L2) even if there is $1B MOVE in the bridge, which is probably not what is expected as we just refilled the bridge.withdraw
function is in the AtomicBridgeInitiatorMOVE.sol contract but is not used in this contract, only in the AtomicBridgeCounterpartyMOVE.sol.Here is the completeBridgeTransfer function in AtomicBridgeCounterpartyMOVE.sol:
This results in at least two issues:
poolBalance >= amount
may prevent use from fixing a compromised bridgea) this is not gas-efficient and
b) creates a dependency between the two contracts; if
withdraw
inAtomicBridgeInitiatorMOVE.sol
is modified, this impacts the behaviour of the counter party.Possible changes
One thing that should probably be done is: remove the line
if (poolBalance < amount) revert InsufficientMOVEBalance();
in the AtomicBridgeInitiatorMOVE.sol contract as it does not serve any purpose and can be dangerous (Issue 1 above).Note
We can keep the
poolBalance
variable to track what has been locked and unlocked in the bridge (which may be different to the balance of the contract). Option 2 below proposes an alternative solution.Once this is done we have two options to address the cross-contract call:
poolBalance
and merge the code of the two contractspoolBalance
intopoolLocked
andpoolUnlocked
, withpoolLocked
(respectivelypoolUnlocked
) tracking what has been locked in the bridge (via initiateBridgeTransfer) andpoolUnlocked
tracking what has been unlocked from the bridge (via completeBridgeTransfer in AtomicBridgeCounterpartyMOVE.sol. Concretely, we removepoolBalance
and replace withpoolLocked
in AtomicBridgeInitiatorMOVE.sol and we addpoolUnlocked
in AtomicBridgeCounterpartyMOVE.sol.Option 1 has one advantage: it is easy to implement, but a serious drawback as we may not be able to track the bridge
poolBalance
(or anything related to how much went in and out the bridge).Option 2 above has two advantages:
poolBalance
can be reconstructed frompoolLocked - poolUnlocked
Initiator
(bridging L1 to L2) and theCounterParty
(bridging L2 to L1) indepedent as we can move thewithdraw
function out of AtomicBridgeInitiatorMOVE.sol into in completeBridgeTransfer (or inline it) in AtomicBridgeCounterpartyMOVE.sol.Implementation
One we have decided whether or not we want to fix issues 1 and 2, and which option is preferred, we can implement the changes.
This requires:
poolLocked
andpoolUnlocked
in the two contractsIn practice, in AtomicBridgeInitiatorMOVE.sol, we rename
poolBalance
intopoolLocked
, and in AtomicBridgeCounterpartyMOVE.sol we add a variablepoolUnlocked
and modify the functioncompleteBridgeTransfer to:
Validation
We need to add some unit tests to test that:
poolLocked
andpoolUnlocked
variables are updated properly (and this for each function in each contract).The text was updated successfully, but these errors were encountered: