mirror of
https://github.com/serai-dex/serai.git
synced 2025-12-08 12:19:24 +00:00
Make a proper nonReentrant modifier
A transaction couldn't call execute twice within a single TX prior. Now, it can. Also adds a bit more context to the escape hatch events/errors.
This commit is contained in:
@@ -42,7 +42,8 @@ interface IRouterWithoutCollisions {
|
|||||||
|
|
||||||
/// @notice Emitted when coins escape through the escape hatch
|
/// @notice Emitted when coins escape through the escape hatch
|
||||||
/// @param coin The coin which escaped
|
/// @param coin The coin which escaped
|
||||||
event Escaped(address indexed coin);
|
/// @param amount The amount which escaped
|
||||||
|
event Escaped(address indexed coin, uint256 amount);
|
||||||
|
|
||||||
/// @notice The key for Serai was invalid
|
/// @notice The key for Serai was invalid
|
||||||
/// @dev This is incomplete and not always guaranteed to be thrown upon an invalid key
|
/// @dev This is incomplete and not always guaranteed to be thrown upon an invalid key
|
||||||
@@ -57,13 +58,17 @@ interface IRouterWithoutCollisions {
|
|||||||
/// @notice The call to an ERC20's `transferFrom` failed
|
/// @notice The call to an ERC20's `transferFrom` failed
|
||||||
error TransferFromFailed();
|
error TransferFromFailed();
|
||||||
|
|
||||||
/// @notice `execute` was re-entered
|
/// @notice A non-reentrant function was re-entered
|
||||||
error ReenteredExecute();
|
error Reentered();
|
||||||
|
|
||||||
/// @notice An invalid address to escape to was specified.
|
/// @notice An invalid address to escape to was specified.
|
||||||
error InvalidEscapeAddress();
|
error InvalidEscapeAddress();
|
||||||
|
/// @notice The escape address wasn't a contract.
|
||||||
|
error EscapeAddressWasNotAContract();
|
||||||
/// @notice Escaping when escape hatch wasn't invoked.
|
/// @notice Escaping when escape hatch wasn't invoked.
|
||||||
error EscapeHatchNotInvoked();
|
error EscapeHatchNotInvoked();
|
||||||
|
/// @notice Escaping failed to transfer out.
|
||||||
|
error EscapeFailed();
|
||||||
|
|
||||||
/// @notice Transfer coins into Serai with an instruction
|
/// @notice Transfer coins into Serai with an instruction
|
||||||
/// @param coin The coin to transfer in (address(0) if Ether)
|
/// @param coin The coin to transfer in (address(0) if Ether)
|
||||||
|
|||||||
@@ -29,8 +29,8 @@ contract Router is IRouterWithoutCollisions {
|
|||||||
bytes32 constant ACCOUNT_WITHOUT_CODE_CODEHASH = keccak256("");
|
bytes32 constant ACCOUNT_WITHOUT_CODE_CODEHASH = keccak256("");
|
||||||
|
|
||||||
/// @dev The address in transient storage used for the reentrancy guard
|
/// @dev The address in transient storage used for the reentrancy guard
|
||||||
bytes32 constant EXECUTE_REENTRANCY_GUARD_SLOT =
|
bytes32 constant REENTRANCY_GUARD_SLOT =
|
||||||
bytes32(uint256(keccak256("ReentrancyGuard Router.execute")) - 1);
|
bytes32(uint256(keccak256("ReentrancyGuard Router")) - 1);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @dev The next nonce used to determine the address of contracts deployed with CREATE. This is
|
* @dev The next nonce used to determine the address of contracts deployed with CREATE. This is
|
||||||
@@ -64,6 +64,28 @@ contract Router is IRouterWithoutCollisions {
|
|||||||
/// @dev The address escaped to
|
/// @dev The address escaped to
|
||||||
address private _escapedTo;
|
address private _escapedTo;
|
||||||
|
|
||||||
|
/// @dev Acquire the re-entrancy lock for the lifetime of this transaction
|
||||||
|
modifier nonReentrant() {
|
||||||
|
bytes32 reentrancyGuardSlot = REENTRANCY_GUARD_SLOT;
|
||||||
|
bytes32 priorEntered;
|
||||||
|
// slither-disable-next-line assembly
|
||||||
|
assembly {
|
||||||
|
priorEntered := tload(reentrancyGuardSlot)
|
||||||
|
tstore(reentrancyGuardSlot, 1)
|
||||||
|
}
|
||||||
|
if (priorEntered != bytes32(0)) {
|
||||||
|
revert Reentered();
|
||||||
|
}
|
||||||
|
|
||||||
|
_;
|
||||||
|
|
||||||
|
// Clear the re-entrancy guard to allow multiple transactions to non-re-entrant functions within
|
||||||
|
// a transaction
|
||||||
|
assembly {
|
||||||
|
tstore(reentrancyGuardSlot, 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// @dev Set the next Serai key. This does not read from/write to `_nextNonce`
|
/// @dev Set the next Serai key. This does not read from/write to `_nextNonce`
|
||||||
/// @param nonceUpdatedWith The nonce used to set the next key
|
/// @param nonceUpdatedWith The nonce used to set the next key
|
||||||
/// @param nextSeraiKeyVar The key to set as next
|
/// @param nextSeraiKeyVar The key to set as next
|
||||||
@@ -139,15 +161,16 @@ contract Router is IRouterWithoutCollisions {
|
|||||||
bytes32 signatureS;
|
bytes32 signatureS;
|
||||||
|
|
||||||
// slither-disable-next-line assembly
|
// slither-disable-next-line assembly
|
||||||
|
uint256 chainID = block.chainid;
|
||||||
assembly {
|
assembly {
|
||||||
// Read the signature (placed after the function signature)
|
// Read the signature (placed after the function signature)
|
||||||
signatureC := mload(add(message, 36))
|
signatureC := mload(add(message, 36))
|
||||||
signatureS := mload(add(message, 68))
|
signatureS := mload(add(message, 68))
|
||||||
|
|
||||||
// Overwrite the signature challenge with the nonce
|
// Overwrite the signature challenge with the chain ID
|
||||||
mstore(add(message, 36), nonceUsed)
|
mstore(add(message, 36), chainID)
|
||||||
// Overwrite the signature response with 0
|
// Overwrite the signature response with the nonce
|
||||||
mstore(add(message, 68), 0)
|
mstore(add(message, 68), nonceUsed)
|
||||||
|
|
||||||
// Calculate the message hash
|
// Calculate the message hash
|
||||||
messageHash := keccak256(add(message, 32), messageLen)
|
messageHash := keccak256(add(message, 32), messageLen)
|
||||||
@@ -404,6 +427,12 @@ contract Router is IRouterWithoutCollisions {
|
|||||||
* fee.
|
* fee.
|
||||||
*
|
*
|
||||||
* The hex bytes are to cause a function selector collision with `IRouter.execute`.
|
* The hex bytes are to cause a function selector collision with `IRouter.execute`.
|
||||||
|
*
|
||||||
|
* Re-entrancy is prevented because we emit a bitmask of which `OutInstruction`s succeeded. Doing
|
||||||
|
* that requires executing the `OutInstruction`s, which may re-enter here. While our application
|
||||||
|
* of CEI with `verifySignature` prevents replays, re-entrancy would allow out-of-order
|
||||||
|
* completion for the execution of batches (despite their in-order start of execution) which
|
||||||
|
* isn't a headache worth dealing with.
|
||||||
*/
|
*/
|
||||||
// @param signature The signature by the current key for Serai's Ethereum validators
|
// @param signature The signature by the current key for Serai's Ethereum validators
|
||||||
// @param coin The coin all of these `OutInstruction`s are for
|
// @param coin The coin all of these `OutInstruction`s are for
|
||||||
@@ -411,26 +440,7 @@ contract Router is IRouterWithoutCollisions {
|
|||||||
// @param outs The `OutInstruction`s to act on
|
// @param outs The `OutInstruction`s to act on
|
||||||
// Each individual call is explicitly metered to ensure there isn't a DoS here
|
// Each individual call is explicitly metered to ensure there isn't a DoS here
|
||||||
// slither-disable-next-line calls-loop,reentrancy-events
|
// slither-disable-next-line calls-loop,reentrancy-events
|
||||||
function execute4DE42904() external {
|
function execute4DE42904() external nonReentrant {
|
||||||
/*
|
|
||||||
Prevent re-entrancy.
|
|
||||||
|
|
||||||
We emit a bitmask of which `OutInstruction`s succeeded. Doing that requires executing the
|
|
||||||
`OutInstruction`s, which may re-enter here. While our application of CEI with verifySignature
|
|
||||||
prevents replays, re-entrancy would allow out-of-order execution of batches (despite their
|
|
||||||
in-order start of execution) which isn't a headache worth dealing with.
|
|
||||||
*/
|
|
||||||
bytes32 executeReentrancyGuardSlot = EXECUTE_REENTRANCY_GUARD_SLOT;
|
|
||||||
bytes32 priorEntered;
|
|
||||||
// slither-disable-next-line assembly
|
|
||||||
assembly {
|
|
||||||
priorEntered := tload(executeReentrancyGuardSlot)
|
|
||||||
tstore(executeReentrancyGuardSlot, 1)
|
|
||||||
}
|
|
||||||
if (priorEntered != bytes32(0)) {
|
|
||||||
revert ReenteredExecute();
|
|
||||||
}
|
|
||||||
|
|
||||||
(uint256 nonceUsed, bytes memory args, bytes32 message) = verifySignature(_seraiKey);
|
(uint256 nonceUsed, bytes memory args, bytes32 message) = verifySignature(_seraiKey);
|
||||||
(,, address coin, uint256 fee, IRouter.OutInstruction[] memory outs) =
|
(,, address coin, uint256 fee, IRouter.OutInstruction[] memory outs) =
|
||||||
abi.decode(args, (bytes32, bytes32, address, uint256, IRouter.OutInstruction[]));
|
abi.decode(args, (bytes32, bytes32, address, uint256, IRouter.OutInstruction[]));
|
||||||
@@ -545,11 +555,15 @@ contract Router is IRouterWithoutCollisions {
|
|||||||
The only check performed accordingly (with no confirmation flow) is that the new contract is
|
The only check performed accordingly (with no confirmation flow) is that the new contract is
|
||||||
in fact a contract. This is done to confirm the contract was successfully deployed on this
|
in fact a contract. This is done to confirm the contract was successfully deployed on this
|
||||||
blockchain.
|
blockchain.
|
||||||
|
|
||||||
|
This check is also comprehensive to the zero-address case, but this function doesn't have to
|
||||||
|
be perfectly optimized and it's better to explicitly handle that due to it being its own
|
||||||
|
invariant.
|
||||||
*/
|
*/
|
||||||
{
|
{
|
||||||
bytes32 codehash = escapeTo.codehash;
|
bytes32 codehash = escapeTo.codehash;
|
||||||
if ((codehash == bytes32(0)) || (codehash == ACCOUNT_WITHOUT_CODE_CODEHASH)) {
|
if ((codehash == bytes32(0)) || (codehash == ACCOUNT_WITHOUT_CODE_CODEHASH)) {
|
||||||
revert InvalidEscapeAddress();
|
revert EscapeAddressWasNotAContract();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -573,8 +587,6 @@ contract Router is IRouterWithoutCollisions {
|
|||||||
revert EscapeHatchNotInvoked();
|
revert EscapeHatchNotInvoked();
|
||||||
}
|
}
|
||||||
|
|
||||||
emit Escaped(coin);
|
|
||||||
|
|
||||||
// Fetch the amount to escape
|
// Fetch the amount to escape
|
||||||
uint256 amount = address(this).balance;
|
uint256 amount = address(this).balance;
|
||||||
if (coin != address(0)) {
|
if (coin != address(0)) {
|
||||||
@@ -582,7 +594,13 @@ contract Router is IRouterWithoutCollisions {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Perform the transfer
|
// Perform the transfer
|
||||||
transferOut(_escapedTo, coin, amount);
|
// While this can be re-entered to try escaping our balance twice, the outer call will fail
|
||||||
|
if (!transferOut(_escapedTo, coin, amount)) {
|
||||||
|
revert EscapeFailed();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Since we successfully escaped this amount, emit the event for it
|
||||||
|
emit Escaped(coin, amount);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// @notice Fetch the next nonce to use by an action published to this contract
|
/// @notice Fetch the next nonce to use by an action published to this contract
|
||||||
|
|||||||
Reference in New Issue
Block a user