diff --git a/processor/ethereum/router/build.rs b/processor/ethereum/router/build.rs index 1ce6d4f5..c931b865 100644 --- a/processor/ethereum/router/build.rs +++ b/processor/ethereum/router/build.rs @@ -27,7 +27,7 @@ fn main() { } build_solidity_contracts::build( - &["../../../networks/ethereum/schnorr/contracts", "../erc20/contracts"], + &["../../../networks/ethereum/schnorr/contracts", "../erc20/contracts", "contracts"], "contracts", &artifacts_path, ) @@ -36,7 +36,11 @@ fn main() { // This cannot be handled with the sol! macro. The Solidity requires an import // https://github.com/alloy-rs/core/issues/602 sol( - &["../../../networks/ethereum/schnorr/contracts/Schnorr.sol", "contracts/Router.sol"], + &[ + "../../../networks/ethereum/schnorr/contracts/Schnorr.sol", + "contracts/IRouter.sol", + "contracts/Router.sol", + ], &(artifacts_path + "/router.rs"), ); } diff --git a/processor/ethereum/router/contracts/IRouter.sol b/processor/ethereum/router/contracts/IRouter.sol new file mode 100644 index 00000000..347553c1 --- /dev/null +++ b/processor/ethereum/router/contracts/IRouter.sol @@ -0,0 +1,147 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.26; + +/// @title Serai Router +/// @author Luke Parker +/// @notice Intakes coins for the Serai network and handles relaying batches of transfers out +interface IRouter { + /// @title A signature + /// @dev Thin wrapper around `c, s` to simplify the API + struct Signature { + bytes32 c; + bytes32 s; + } + + /// @title The type of destination + /// @dev A destination is either an address or a blob of code to deploy and call + enum DestinationType { + Address, + Code + } + + /// @title A code destination + /** + * @dev If transferring an ERC20 to this destination, it will be transferred to the address the + * code will be deployed to. If transferring ETH, it will be transferred with the deployment of + * the code. `code` is deployed with CREATE (calling its constructor). The entire deployment + * (and associated sandboxing) must consume less than `gasLimit` units of gas or it will revert. + */ + struct CodeDestination { + uint32 gasLimit; + bytes code; + } + + /// @title An instruction to transfer coins out + /// @dev Specifies a destination and amount but not the coin as that's assumed to be contextual + struct OutInstruction { + DestinationType destinationType; + bytes destination; + uint256 amount; + } + + /// @notice Emitted when the key for Serai's Ethereum validators is updated + /// @param nonce The nonce consumed to update this key + /// @param key The key updated to + event SeraiKeyUpdated(uint256 indexed nonce, bytes32 indexed key); + + /// @notice Emitted when an InInstruction occurs + /// @param from The address which called `inInstruction` and caused this event to be emitted + /// @param coin The coin transferred in + /// @param amount The amount of the coin transferred in + /// @param instruction The Shorthand-encoded InInstruction for Serai to decode and handle + event InInstruction( + address indexed from, address indexed coin, uint256 amount, bytes instruction + ); + + /// @notice Emitted when a batch of `OutInstruction`s occurs + /// @param nonce The nonce consumed to execute this batch of transactions + /// @param messageHash The hash of the message signed for the executed batch + event Executed(uint256 indexed nonce, bytes32 indexed messageHash); + + /// @notice Emitted when `escapeHatch` is invoked + /// @param escapeTo The address to escape to + event EscapeHatch(address indexed escapeTo); + + /// @notice Emitted when coins escape through the escape hatch + /// @param coin The coin which escaped + event Escaped(address indexed coin); + + /// @notice The contract has had its escape hatch invoked and won't accept further actions + error EscapeHatchInvoked(); + /// @notice The signature was invalid + error InvalidSignature(); + /// @notice The amount specified didn't match `msg.value` + error AmountMismatchesMsgValue(); + /// @notice The call to an ERC20's `transferFrom` failed + error TransferFromFailed(); + + /// @notice An invalid address to escape to was specified. + error InvalidEscapeAddress(); + /// @notice Escaping when escape hatch wasn't invoked. + error EscapeHatchNotInvoked(); + + /// @notice Update the key representing Serai's Ethereum validators + /// @dev This assumes the key is correct. No checks on it are performed + /// @param signature The signature by the current key authorizing this update + /// @param newSeraiKey The key to update to + function updateSeraiKey(Signature calldata signature, bytes32 newSeraiKey) external; + + /// @notice Transfer coins into Serai with an instruction + /// @param coin The coin to transfer in (address(0) if Ether) + /// @param amount The amount to transfer in (msg.value if Ether) + /** + * @param instruction The Shorthand-encoded InInstruction for Serai to associate with this + * transfer in + */ + // Re-entrancy doesn't bork this function + // slither-disable-next-line reentrancy-events + function inInstruction(address coin, uint256 amount, bytes memory instruction) external payable; + + /// @notice Execute some arbitrary code within a secure sandbox + /** + * @dev This performs sandboxing by deploying this code with `CREATE`. This is an external + * function as we can't meter `CREATE`/internal functions. We work around this by calling this + * function with `CALL` (which we can meter). This does forward `msg.value` to the newly + * deployed contract. + */ + /// @param code The code to execute + function executeArbitraryCode(bytes memory code) external payable; + + /// @notice Execute a batch of `OutInstruction`s + /** + * @dev All `OutInstruction`s in a batch are only for a single coin to simplify handling of the + * fee + */ + /// @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 fee The fee to pay (in coin) to the caller for their relaying of this batch + /// @param outs The `OutInstruction`s to act on + function execute( + Signature calldata signature, + address coin, + uint256 fee, + OutInstruction[] calldata outs + ) external; + + /// @notice Escapes to a new smart contract + /// @dev This should be used upon an invariant being reached or new functionality being needed + /// @param signature The signature by the current key for Serai's Ethereum validators + /// @param escapeTo The address to escape to + function escapeHatch(Signature calldata signature, address escapeTo) external; + + /// @notice Escape coins after the escape hatch has been invoked + /// @param coin The coin to escape + function escape(address coin) external; + + /// @notice Fetch the next nonce to use by an action published to this contract + /// return The next nonce to use by an action published to this contract + function nextNonce() external view returns (uint256); + + /// @notice Fetch the current key for Serai's Ethereum validator set + /// @return The current key for Serai's Ethereum validator set + function seraiKey() external view returns (bytes32); + + /// @notice Fetch the address escaped to + /// @return The address which was escaped to (address(0) if the escape hatch hasn't been invoked) + function escapedTo() external view returns (address); +} diff --git a/processor/ethereum/router/contracts/Router.sol b/processor/ethereum/router/contracts/Router.sol index 4ec4eb4e..4be55114 100644 --- a/processor/ethereum/router/contracts/Router.sol +++ b/processor/ethereum/router/contracts/Router.sol @@ -1,12 +1,12 @@ // SPDX-License-Identifier: AGPL-3.0-only pragma solidity ^0.8.26; -// TODO: MIT licensed interface - import "IERC20.sol"; import "Schnorr.sol"; +import "IRouter.sol"; + /* The Router directly performs low-level calls in order to fine-tune the gas settings. Since this contract is meant to relay an entire batch of transactions, the ability to exactly meter @@ -32,7 +32,7 @@ contract Router { /* We don't expose a getter for this as it shouldn't be expected to have any specific value at a given moment in time. If someone wants to know the address of their deployed contract, they can - have it emit an event and verify the emitting contract is the expected one. + have it emit IRouter.an event and verify the emitting contract is the expected one. */ uint256 private _smartContractNonce; @@ -51,87 +51,12 @@ contract Router { /// @dev The address escaped to address private _escapedTo; - /// @title The type of destination - /// @dev A destination is either an address or a blob of code to deploy and call - enum DestinationType { - Address, - Code - } - - /// @title A code destination - /** - * @dev If transferring an ERC20 to this destination, it will be transferred to the address the - * code will be deployed to. If transferring ETH, it will be transferred with the deployment of - * the code. `code` is deployed with CREATE (calling its constructor). The entire deployment - * (and associated sandboxing) must consume less than `gasLimit` units of gas or it will revert. - */ - struct CodeDestination { - uint32 gasLimit; - bytes code; - } - - /// @title An instruction to transfer coins out - /// @dev Specifies a destination and amount but not the coin as that's assumed to be contextual - struct OutInstruction { - DestinationType destinationType; - bytes destination; - uint256 amount; - } - - /// @title A signature - /// @dev Thin wrapper around `c, s` to simplify the API - struct Signature { - bytes32 c; - bytes32 s; - } - - /// @notice Emitted when the key for Serai's Ethereum validators is updated - /// @param nonce The nonce consumed to update this key - /// @param key The key updated to - event SeraiKeyUpdated(uint256 indexed nonce, bytes32 indexed key); - - /// @notice Emitted when an InInstruction occurs - /// @param from The address which called `inInstruction` and caused this event to be emitted - /// @param coin The coin transferred in - /// @param amount The amount of the coin transferred in - /// @param instruction The Shorthand-encoded InInstruction for Serai to decode and handle - event InInstruction( - address indexed from, address indexed coin, uint256 amount, bytes instruction - ); - - /// @notice Emitted when a batch of `OutInstruction`s occurs - /// @param nonce The nonce consumed to execute this batch of transactions - /// @param messageHash The hash of the message signed for the executed batch - event Executed(uint256 indexed nonce, bytes32 indexed messageHash); - - /// @notice Emitted when `escapeHatch` is invoked - /// @param escapeTo The address to escape to - event EscapeHatch(address indexed escapeTo); - - /// @notice Emitted when coins escape through the escape hatch - /// @param coin The coin which escaped - event Escaped(address indexed coin); - - /// @notice The contract has had its escape hatch invoked and won't accept further actions - error EscapeHatchInvoked(); - /// @notice The signature was invalid - error InvalidSignature(); - /// @notice The amount specified didn't match `msg.value` - error AmountMismatchesMsgValue(); - /// @notice The call to an ERC20's `transferFrom` failed - error TransferFromFailed(); - - /// @notice An invalid address to escape to was specified. - error InvalidEscapeAddress(); - /// @notice Escaping when escape hatch wasn't invoked. - error EscapeHatchNotInvoked(); - /// @dev Updates the Serai key. This does not update `_nextNonce` /// @param nonceUpdatedWith The nonce used to update the key /// @param newSeraiKey The key updated to function _updateSeraiKey(uint256 nonceUpdatedWith, bytes32 newSeraiKey) private { _seraiKey = newSeraiKey; - emit SeraiKeyUpdated(nonceUpdatedWith, newSeraiKey); + emit IRouter.SeraiKeyUpdated(nonceUpdatedWith, newSeraiKey); } /// @notice The constructor for the relayer @@ -153,9 +78,9 @@ contract Router { /** * @dev - * Verify a signature of the calldata, placed immediately after the function selector. The calldata - * should be signed with the nonce taking the place of the signature's commitment to its nonce, and - * the signature solution zeroed. + * Verify a signature of the calldata, placed immediately after the function selector. The + * calldata should be signed with the nonce taking the place of the signature's commitment to + * its nonce, and the signature solution zeroed. */ function verifySignature() private @@ -163,7 +88,7 @@ contract Router { { // If the escape hatch was triggered, reject further signatures if (_escapedTo != address(0)) { - revert EscapeHatchInvoked(); + revert IRouter.EscapeHatchInvoked(); } message = msg.data; @@ -175,7 +100,7 @@ contract Router { (triggering undefined behavior). */ if (messageLen < 68) { - revert InvalidSignature(); + revert IRouter.InvalidSignature(); } // Read _nextNonce into memory as the nonce we'll use @@ -202,7 +127,7 @@ contract Router { // Verify the signature if (!Schnorr.verify(_seraiKey, messageHash, signatureC, signatureS)) { - revert InvalidSignature(); + revert IRouter.InvalidSignature(); } // Set the next nonce @@ -251,6 +176,10 @@ contract Router { // @param newSeraiKey The key to update to function updateSeraiKey() external { (uint256 nonceUsed, bytes memory args,) = verifySignature(); + /* + We could replace this with a length check (if we don't simply assume the calldata is valid as + it was properly signed) + mload to save 24 gas but it's not worth the complexity. + */ (,, bytes32 newSeraiKey) = abi.decode(args, (bytes32, bytes32, bytes32)); _updateSeraiKey(nonceUsed, newSeraiKey); } @@ -267,7 +196,7 @@ contract Router { function inInstruction(address coin, uint256 amount, bytes memory instruction) external payable { // Check the transfer if (coin == address(0)) { - if (amount != msg.value) revert AmountMismatchesMsgValue(); + if (amount != msg.value) revert IRouter.AmountMismatchesMsgValue(); } else { (bool success, bytes memory res) = address(coin).call( abi.encodeWithSelector(IERC20.transferFrom.selector, msg.sender, address(this), amount) @@ -278,7 +207,7 @@ contract Router { ERC20 contract did in fact return true */ bool nonStandardResOrTrue = (res.length == 0) || abi.decode(res, (bool)); - if (!(success && nonStandardResOrTrue)) revert TransferFromFailed(); + if (!(success && nonStandardResOrTrue)) revert IRouter.TransferFromFailed(); } /* @@ -303,7 +232,7 @@ contract Router { It is the Serai network's role not to add support for any non-standard implementations. */ - emit InInstruction(msg.sender, coin, amount, instruction); + emit IRouter.InInstruction(msg.sender, coin, amount, instruction); } /// @dev Perform an ERC20 transfer out @@ -422,11 +351,11 @@ contract Router { // slither-disable-next-line calls-loop function execute() external { (uint256 nonceUsed, bytes memory args, bytes32 message) = verifySignature(); - (,, address coin, uint256 fee, OutInstruction[] memory outs) = - abi.decode(args, (bytes32, bytes32, address, uint256, OutInstruction[])); + (,, address coin, uint256 fee, IRouter.OutInstruction[] memory outs) = + abi.decode(args, (bytes32, bytes32, address, uint256, IRouter.OutInstruction[])); // TODO: Also include a bit mask here - emit Executed(nonceUsed, message); + emit IRouter.Executed(nonceUsed, message); /* Since we don't have a re-entrancy guard, it is possible for instructions from later batches to @@ -439,9 +368,9 @@ contract Router { // slither-disable-next-line reentrancy-events for (uint256 i = 0; i < outs.length; i++) { // If the destination is an address, we perform a direct transfer - if (outs[i].destinationType == DestinationType.Address) { + if (outs[i].destinationType == IRouter.DestinationType.Address) { /* - This may cause a revert if the destination isn't actually a valid address. Serai is + This may cause a revert if the destination isn't actually a valid address. Serai is trusted to not pass a malformed destination, yet if it ever did, it could simply re-sign a corrected batch using this nonce. */ @@ -465,7 +394,8 @@ contract Router { erc20TransferOut(nextAddress, coin, outs[i].amount); } - (CodeDestination memory destination) = abi.decode(outs[i].destination, (CodeDestination)); + (IRouter.CodeDestination memory destination) = + abi.decode(outs[i].destination, (IRouter.CodeDestination)); /* Perform the deployment with the defined gas budget. @@ -498,7 +428,7 @@ contract Router { (,, address escapeTo) = abi.decode(args, (bytes32, bytes32, address)); if (escapeTo == address(0)) { - revert InvalidEscapeAddress(); + revert IRouter.InvalidEscapeAddress(); } /* We want to define the escape hatch so coins here now, and latently received, can be forwarded. @@ -506,21 +436,21 @@ contract Router { received coins without penalty (if they update the escape hatch after unstaking). */ if (_escapedTo != address(0)) { - revert EscapeHatchInvoked(); + revert IRouter.EscapeHatchInvoked(); } _escapedTo = escapeTo; - emit EscapeHatch(escapeTo); + emit IRouter.EscapeHatch(escapeTo); } /// @notice Escape coins after the escape hatch has been invoked /// @param coin The coin to escape function escape(address coin) external { if (_escapedTo == address(0)) { - revert EscapeHatchNotInvoked(); + revert IRouter.EscapeHatchNotInvoked(); } - emit Escaped(coin); + emit IRouter.Escaped(coin); // Fetch the amount to escape uint256 amount = address(this).balance; diff --git a/processor/ethereum/router/src/lib.rs b/processor/ethereum/router/src/lib.rs index 9e2d880a..b2e78b96 100644 --- a/processor/ethereum/router/src/lib.rs +++ b/processor/ethereum/router/src/lib.rs @@ -31,7 +31,13 @@ use serai_client::networks::ethereum::Address as SeraiAddress; mod _abi { include!(concat!(env!("OUT_DIR"), "/serai-processor-ethereum-router/router.rs")); } -use _abi::Router as abi; +mod abi { + pub use super::_abi::IRouter::{ + Signature, DestinationType, CodeDestination, OutInstruction, SeraiKeyUpdated, InInstruction, + Executed, EscapeHatch, Escaped, + }; + pub use super::_abi::Router::*; +} use abi::{ SeraiKeyUpdated as SeraiKeyUpdatedEvent, InInstruction as InInstructionEvent, Executed as ExecutedEvent, @@ -326,7 +332,7 @@ impl Router { ] .concat() .into(), - gas_limit: 40927 * 120 / 100, + gas_limit: 40_889 * 120 / 100, ..Default::default() } } @@ -353,7 +359,7 @@ impl Router { .concat() .into(), // TODO - gas_limit: 100_000 + ((200_000 + 10_000) * u128::try_from(outs_len).unwrap()), + gas_limit: (45_501 + ((200_000 + 10_000) * u128::try_from(outs_len).unwrap())) * 120 / 100, ..Default::default() } }