mirror of
https://github.com/serai-dex/serai.git
synced 2025-12-12 14:09:25 +00:00
Compare commits
4 Commits
6a520a7412
...
26230377b0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
26230377b0 | ||
|
|
2f5c0c68d0 | ||
|
|
8de42cc2d4 | ||
|
|
cf4123b0f8 |
1
Cargo.lock
generated
1
Cargo.lock
generated
@@ -8724,6 +8724,7 @@ dependencies = [
|
||||
"alloy-rpc-client",
|
||||
"alloy-rpc-types-eth",
|
||||
"alloy-simple-request-transport",
|
||||
"alloy-sol-macro",
|
||||
"alloy-sol-macro-expander",
|
||||
"alloy-sol-macro-input",
|
||||
"alloy-sol-types",
|
||||
|
||||
@@ -20,7 +20,9 @@ workspace = true
|
||||
group = { version = "0.13", default-features = false }
|
||||
|
||||
alloy-core = { version = "0.8", default-features = false }
|
||||
|
||||
alloy-sol-types = { version = "0.8", default-features = false }
|
||||
alloy-sol-macro = { version = "0.8", default-features = false }
|
||||
|
||||
alloy-consensus = { version = "0.3", default-features = false }
|
||||
|
||||
|
||||
@@ -26,17 +26,14 @@ fn main() {
|
||||
fs::create_dir(&artifacts_path).unwrap();
|
||||
}
|
||||
|
||||
build_solidity_contracts::build(
|
||||
&["../../../networks/ethereum/schnorr/contracts", "../erc20/contracts"],
|
||||
"contracts",
|
||||
&artifacts_path,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// This cannot be handled with the sol! macro. The Solidity requires an import
|
||||
// This cannot be handled with the sol! macro. The Router 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"),
|
||||
);
|
||||
}
|
||||
|
||||
152
processor/ethereum/router/contracts/IRouter.sol
Normal file
152
processor/ethereum/router/contracts/IRouter.sol
Normal file
@@ -0,0 +1,152 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
pragma solidity ^0.8.26;
|
||||
|
||||
/// @title Serai Router (without functions overriden by selector collisions)
|
||||
/// @author Luke Parker <lukeparker@serai.exchange>
|
||||
/// @notice Intakes coins for the Serai network and handles relaying batches of transfers out
|
||||
interface IRouterWithoutCollisions {
|
||||
/// @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 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 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);
|
||||
}
|
||||
|
||||
/// @title Serai Router
|
||||
/// @author Luke Parker <lukeparker@serai.exchange>
|
||||
/// @notice Intakes coins for the Serai network and handles relaying batches of transfers out
|
||||
interface IRouter is IRouterWithoutCollisions {
|
||||
/// @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 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 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;
|
||||
}
|
||||
@@ -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
|
||||
@@ -24,7 +24,7 @@ import "Schnorr.sol";
|
||||
/// @title Serai Router
|
||||
/// @author Luke Parker <lukeparker@serai.exchange>
|
||||
/// @notice Intakes coins for the Serai network and handles relaying batches of transfers out
|
||||
contract Router {
|
||||
contract Router is IRouterWithoutCollisions {
|
||||
/**
|
||||
* @dev The next nonce used to determine the address of contracts deployed with CREATE. This is
|
||||
* used to predict the addresses of deployed contracts ahead of time.
|
||||
@@ -51,151 +51,141 @@ 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 at the end of the current function. Executing at the end of the
|
||||
* current function allows verifying a signature with the current key. This does not update
|
||||
* `_nextNonce`
|
||||
*/
|
||||
/// @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
|
||||
modifier updateSeraiKeyAtEndOfFn(uint256 nonceUpdatedWith, bytes32 newSeraiKey) {
|
||||
// Run the function itself
|
||||
_;
|
||||
|
||||
// Update the key
|
||||
function _updateSeraiKey(uint256 nonceUpdatedWith, bytes32 newSeraiKey) private {
|
||||
_seraiKey = newSeraiKey;
|
||||
emit SeraiKeyUpdated(nonceUpdatedWith, newSeraiKey);
|
||||
}
|
||||
|
||||
/// @notice The constructor for the relayer
|
||||
/// @param initialSeraiKey The initial key for Serai's Ethereum validators
|
||||
constructor(bytes32 initialSeraiKey) updateSeraiKeyAtEndOfFn(0, initialSeraiKey) {
|
||||
constructor(bytes32 initialSeraiKey) {
|
||||
// Nonces are incremented by 1 upon account creation, prior to any code execution, per EIP-161
|
||||
// This is incompatible with any networks which don't have their nonces start at 0
|
||||
_smartContractNonce = 1;
|
||||
|
||||
// We consumed nonce 0 when setting the initial Serai key
|
||||
// Set the Serai key
|
||||
_updateSeraiKey(0, initialSeraiKey);
|
||||
|
||||
// We just consumed nonce 0 when setting the initial Serai key
|
||||
_nextNonce = 1;
|
||||
|
||||
// We haven't escaped to any address yet
|
||||
_escapedTo = address(0);
|
||||
}
|
||||
|
||||
/// @dev Verify a signature
|
||||
/// @param message The message to pass to the Schnorr contract
|
||||
/// @param signature The signature by the current key for this message
|
||||
function verifySignature(bytes32 message, Signature calldata signature) private {
|
||||
/**
|
||||
* @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.
|
||||
*/
|
||||
function verifySignature()
|
||||
private
|
||||
returns (uint256 nonceUsed, bytes memory message, bytes32 messageHash)
|
||||
{
|
||||
// If the escape hatch was triggered, reject further signatures
|
||||
if (_escapedTo != address(0)) {
|
||||
revert EscapeHatchInvoked();
|
||||
}
|
||||
// Verify the signature
|
||||
if (!Schnorr.verify(_seraiKey, message, signature.c, signature.s)) {
|
||||
|
||||
message = msg.data;
|
||||
uint256 messageLen = message.length;
|
||||
/*
|
||||
function selector, signature
|
||||
|
||||
This check means we don't read memory, and as we attempt to clear portions, write past it
|
||||
(triggering undefined behavior).
|
||||
*/
|
||||
if (messageLen < 68) {
|
||||
revert InvalidSignature();
|
||||
}
|
||||
|
||||
// Read _nextNonce into memory as the nonce we'll use
|
||||
nonceUsed = _nextNonce;
|
||||
|
||||
// Declare memory to copy the signature out to
|
||||
bytes32 signatureC;
|
||||
bytes32 signatureS;
|
||||
|
||||
// slither-disable-next-line assembly
|
||||
assembly {
|
||||
// Read the signature (placed after the function signature)
|
||||
signatureC := mload(add(message, 36))
|
||||
signatureS := mload(add(message, 68))
|
||||
|
||||
// Overwrite the signature challenge with the nonce
|
||||
mstore(add(message, 36), nonceUsed)
|
||||
// Overwrite the signature response with 0
|
||||
mstore(add(message, 68), 0)
|
||||
|
||||
// Calculate the message hash
|
||||
messageHash := keccak256(add(message, 32), messageLen)
|
||||
}
|
||||
|
||||
// Verify the signature
|
||||
if (!Schnorr.verify(_seraiKey, messageHash, signatureC, signatureS)) {
|
||||
revert InvalidSignature();
|
||||
}
|
||||
|
||||
// Set the next nonce
|
||||
unchecked {
|
||||
_nextNonce++;
|
||||
_nextNonce = nonceUsed + 1;
|
||||
}
|
||||
|
||||
/*
|
||||
Advance the message past the function selector, enabling decoding the arguments. Ideally, we'd
|
||||
also advance past the signature (to simplify decoding arguments and save some memory). This
|
||||
would transfrom message from:
|
||||
|
||||
message (pointer)
|
||||
v
|
||||
------------------------------------------------------------
|
||||
| 32-byte length | 4-byte selector | Signature | Arguments |
|
||||
------------------------------------------------------------
|
||||
|
||||
to:
|
||||
|
||||
message (pointer)
|
||||
v
|
||||
----------------------------------------------
|
||||
| Junk 68 bytes | 32-byte length | Arguments |
|
||||
----------------------------------------------
|
||||
|
||||
Unfortunately, doing so corrupts the offsets defined within the ABI itself. We settle for a
|
||||
transform to:
|
||||
|
||||
message (pointer)
|
||||
v
|
||||
---------------------------------------------------------
|
||||
| Junk 4 bytes | 32-byte length | Signature | Arguments |
|
||||
---------------------------------------------------------
|
||||
*/
|
||||
// slither-disable-next-line assembly
|
||||
assembly {
|
||||
message := add(message, 4)
|
||||
mstore(message, sub(messageLen, 4))
|
||||
}
|
||||
}
|
||||
|
||||
/// @notice Update the key representing Serai's Ethereum validators
|
||||
/// @dev This assumes the key is correct. No checks on it are performed
|
||||
/// @param newSeraiKey The key to update to
|
||||
/// @param signature The signature by the current key authorizing this update
|
||||
function updateSeraiKey(bytes32 newSeraiKey, Signature calldata signature)
|
||||
external
|
||||
updateSeraiKeyAtEndOfFn(_nextNonce, newSeraiKey)
|
||||
{
|
||||
/**
|
||||
* @dev This assumes the key is correct. No checks on it are performed.
|
||||
*
|
||||
* The hex bytes are to cause a collision with `IRouter.updateSeraiKey`.
|
||||
*/
|
||||
// @param signature The signature by the current key authorizing this update
|
||||
// @param newSeraiKey The key to update to
|
||||
function updateSeraiKey5A8542A2() external {
|
||||
(uint256 nonceUsed, bytes memory args,) = verifySignature();
|
||||
/*
|
||||
This DST needs a length prefix as well to prevent DSTs potentially being substrings of each
|
||||
other, yet this is fine for our well-defined, extremely-limited use.
|
||||
|
||||
We don't encode the chain ID as Serai generates independent keys for each integration. If
|
||||
Ethereum L2s are integrated, and they reuse the Ethereum validator set, we would use the
|
||||
existing Serai key yet we'd apply an off-chain derivation scheme to bind it to specific
|
||||
networks. This also lets Serai identify EVMs per however it wants, solving the edge case where
|
||||
two instances of the EVM share a chain ID for whatever horrific reason.
|
||||
|
||||
This uses encodePacked as all items present here are of fixed length.
|
||||
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 message = keccak256(abi.encodePacked("updateSeraiKey", _nextNonce, newSeraiKey));
|
||||
verifySignature(message, signature);
|
||||
(,, bytes32 newSeraiKey) = abi.decode(args, (bytes32, bytes32, bytes32));
|
||||
_updateSeraiKey(nonceUsed, newSeraiKey);
|
||||
}
|
||||
|
||||
/// @notice Transfer coins into Serai with an instruction
|
||||
@@ -355,28 +345,23 @@ contract Router {
|
||||
/// @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
|
||||
* fee.
|
||||
*
|
||||
* The hex bytes are to cause a function selector collision with `IRouter.execute`.
|
||||
*/
|
||||
/// @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
|
||||
/// @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 fee The fee to pay (in coin) to the caller for their relaying of this batch
|
||||
// @param outs The `OutInstruction`s to act on
|
||||
// Each individual call is explicitly metered to ensure there isn't a DoS here
|
||||
// slither-disable-next-line calls-loop
|
||||
function execute(
|
||||
address coin,
|
||||
uint256 fee,
|
||||
OutInstruction[] calldata outs,
|
||||
Signature calldata signature
|
||||
) external {
|
||||
// Verify the signature
|
||||
// This uses `encode`, not `encodePacked`, as `outs` is of variable length
|
||||
// TODO: Use a custom encode in verifySignature here with assembly (benchmarking before/after)
|
||||
bytes32 message = keccak256(abi.encode("execute", _nextNonce, coin, fee, outs));
|
||||
verifySignature(message, signature);
|
||||
function execute4DE42904() external {
|
||||
(uint256 nonceUsed, bytes memory args, bytes32 message) = verifySignature();
|
||||
(,, 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(_nextNonce, message);
|
||||
emit Executed(nonceUsed, message);
|
||||
|
||||
/*
|
||||
Since we don't have a re-entrancy guard, it is possible for instructions from later batches to
|
||||
@@ -389,9 +374,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.
|
||||
*/
|
||||
@@ -415,7 +400,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.
|
||||
@@ -438,10 +424,19 @@ contract Router {
|
||||
}
|
||||
|
||||
/// @notice Escapes to a new smart contract
|
||||
/// @dev This should be used upon an invariant being reached or new functionality being needed
|
||||
/// @param escapeTo The address to escape to
|
||||
/// @param signature The signature by the current key for Serai's Ethereum validators
|
||||
function escapeHatch(address escapeTo, Signature calldata signature) external {
|
||||
/**
|
||||
* @dev This should be used upon an invariant being reached or new functionality being needed.
|
||||
*
|
||||
* The hex bytes are to cause a collision with `IRouter.escapeHatch`.
|
||||
*/
|
||||
// @param signature The signature by the current key for Serai's Ethereum validators
|
||||
// @param escapeTo The address to escape to
|
||||
function escapeHatchDCDD91CC() external {
|
||||
// Verify the signature
|
||||
(, bytes memory args,) = verifySignature();
|
||||
|
||||
(,, address escapeTo) = abi.decode(args, (bytes32, bytes32, address));
|
||||
|
||||
if (escapeTo == address(0)) {
|
||||
revert InvalidEscapeAddress();
|
||||
}
|
||||
@@ -454,10 +449,6 @@ contract Router {
|
||||
revert EscapeHatchInvoked();
|
||||
}
|
||||
|
||||
// Verify the signature
|
||||
bytes32 message = keccak256(abi.encodePacked("escapeHatch", _nextNonce, escapeTo));
|
||||
verifySignature(message, signature);
|
||||
|
||||
_escapedTo = escapeTo;
|
||||
emit EscapeHatch(escapeTo);
|
||||
}
|
||||
|
||||
@@ -28,10 +28,25 @@ use serai_client::networks::ethereum::Address as SeraiAddress;
|
||||
#[expect(clippy::all)]
|
||||
#[expect(clippy::ignored_unit_patterns)]
|
||||
#[expect(clippy::redundant_closure_for_method_calls)]
|
||||
mod _abi {
|
||||
mod _irouter_abi {
|
||||
alloy_sol_macro::sol!("contracts/IRouter.sol");
|
||||
}
|
||||
|
||||
#[rustfmt::skip]
|
||||
#[expect(warnings)]
|
||||
#[expect(needless_pass_by_value)]
|
||||
#[expect(clippy::all)]
|
||||
#[expect(clippy::ignored_unit_patterns)]
|
||||
#[expect(clippy::redundant_closure_for_method_calls)]
|
||||
mod _router_abi {
|
||||
include!(concat!(env!("OUT_DIR"), "/serai-processor-ethereum-router/router.rs"));
|
||||
}
|
||||
use _abi::Router as abi;
|
||||
|
||||
mod abi {
|
||||
pub use super::_router_abi::IRouterWithoutCollisions::*;
|
||||
pub use super::_router_abi::IRouter::*;
|
||||
pub use super::_router_abi::Router::constructorCall;
|
||||
}
|
||||
use abi::{
|
||||
SeraiKeyUpdated as SeraiKeyUpdatedEvent, InInstruction as InInstructionEvent,
|
||||
Executed as ExecutedEvent,
|
||||
@@ -309,26 +324,37 @@ impl Router {
|
||||
|
||||
/// Get the message to be signed in order to update the key for Serai.
|
||||
pub fn update_serai_key_message(nonce: u64, key: &PublicKey) -> Vec<u8> {
|
||||
("updateSeraiKey", U256::try_from(nonce).expect("couldn't convert u64 to u256"), key.eth_repr())
|
||||
.abi_encode_packed()
|
||||
abi::updateSeraiKeyCall::new((
|
||||
abi::Signature { c: U256::try_from(nonce).unwrap().into(), s: U256::ZERO.into() },
|
||||
key.eth_repr().into(),
|
||||
))
|
||||
.abi_encode()
|
||||
}
|
||||
|
||||
/// Construct a transaction to update the key representing Serai.
|
||||
pub fn update_serai_key(&self, public_key: &PublicKey, sig: &Signature) -> TxLegacy {
|
||||
TxLegacy {
|
||||
to: TxKind::Call(self.1),
|
||||
input: abi::updateSeraiKeyCall::new((public_key.eth_repr().into(), sig.into()))
|
||||
.abi_encode()
|
||||
.into(),
|
||||
gas_limit: 40748 * 120 / 100,
|
||||
input: abi::updateSeraiKeyCall::new((
|
||||
abi::Signature::from(sig),
|
||||
public_key.eth_repr().into(),
|
||||
))
|
||||
.abi_encode()
|
||||
.into(),
|
||||
gas_limit: 40_889 * 120 / 100,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the message to be signed in order to execute a series of `OutInstruction`s.
|
||||
pub fn execute_message(nonce: u64, coin: Coin, fee: U256, outs: OutInstructions) -> Vec<u8> {
|
||||
("execute".to_string(), U256::try_from(nonce).unwrap(), coin.address(), fee, outs.0)
|
||||
.abi_encode_sequence()
|
||||
abi::executeCall::new((
|
||||
abi::Signature { c: U256::try_from(nonce).unwrap().into(), s: U256::ZERO.into() },
|
||||
coin.address(),
|
||||
fee,
|
||||
outs.0,
|
||||
))
|
||||
.abi_encode()
|
||||
}
|
||||
|
||||
/// Construct a transaction to execute a batch of `OutInstruction`s.
|
||||
@@ -336,9 +362,11 @@ impl Router {
|
||||
let outs_len = outs.0.len();
|
||||
TxLegacy {
|
||||
to: TxKind::Call(self.1),
|
||||
input: abi::executeCall::new((coin.address(), fee, outs.0, sig.into())).abi_encode().into(),
|
||||
input: abi::executeCall::new((abi::Signature::from(sig), coin.address(), fee, outs.0))
|
||||
.abi_encode()
|
||||
.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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ use alloy_sol_types::SolCall;
|
||||
|
||||
use alloy_consensus::TxLegacy;
|
||||
|
||||
use alloy_rpc_types_eth::BlockNumberOrTag;
|
||||
use alloy_rpc_types_eth::{BlockNumberOrTag, TransactionReceipt};
|
||||
use alloy_simple_request_transport::SimpleRequest;
|
||||
use alloy_rpc_client::ClientBuilder;
|
||||
use alloy_provider::RootProvider;
|
||||
@@ -22,6 +22,18 @@ use ethereum_deployer::Deployer;
|
||||
|
||||
use crate::{Coin, OutInstructions, Router};
|
||||
|
||||
#[test]
|
||||
fn selector_collisions() {
|
||||
assert_eq!(
|
||||
crate::_irouter_abi::IRouter::executeCall::SELECTOR,
|
||||
crate::_router_abi::Router::execute4DE42904Call::SELECTOR
|
||||
);
|
||||
assert_eq!(
|
||||
crate::_irouter_abi::IRouter::updateSeraiKeyCall::SELECTOR,
|
||||
crate::_router_abi::Router::updateSeraiKey5A8542A2Call::SELECTOR
|
||||
);
|
||||
}
|
||||
|
||||
pub(crate) fn test_key() -> (Scalar, PublicKey) {
|
||||
loop {
|
||||
let key = Scalar::random(&mut OsRng);
|
||||
@@ -154,8 +166,16 @@ async fn test_erc20_in_instruction() {
|
||||
todo!("TODO")
|
||||
}
|
||||
|
||||
async fn publish_outs(key: (Scalar, PublicKey), nonce: u64, coin: Coin, fee: U256, outs: OutInstructions) -> TransactionReceipt {
|
||||
let msg = Router::execute_message(nonce, coin, fee, instructions.clone());
|
||||
async fn publish_outs(
|
||||
provider: &RootProvider<SimpleRequest>,
|
||||
router: &Router,
|
||||
key: (Scalar, PublicKey),
|
||||
nonce: u64,
|
||||
coin: Coin,
|
||||
fee: U256,
|
||||
outs: OutInstructions,
|
||||
) -> TransactionReceipt {
|
||||
let msg = Router::execute_message(nonce, coin, fee, outs.clone());
|
||||
|
||||
let nonce = Scalar::random(&mut OsRng);
|
||||
let c = Signature::challenge(ProjectivePoint::GENERATOR * nonce, &key.1, &msg);
|
||||
@@ -163,10 +183,10 @@ async fn publish_outs(key: (Scalar, PublicKey), nonce: u64, coin: Coin, fee: U25
|
||||
|
||||
let sig = Signature::new(c, s).unwrap();
|
||||
|
||||
let mut tx = router.execute(coin, fee, instructions, &sig);
|
||||
let mut tx = router.execute(coin, fee, outs, &sig);
|
||||
tx.gas_price = 100_000_000_000u128;
|
||||
let tx = ethereum_primitives::deterministically_sign(&tx);
|
||||
ethereum_test_primitives::publish_tx(&provider, tx).await
|
||||
ethereum_test_primitives::publish_tx(provider, tx).await
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
@@ -182,7 +202,7 @@ async fn test_eth_address_out_instruction() {
|
||||
ethereum_test_primitives::fund_account(&provider, router.address(), amount).await;
|
||||
|
||||
let instructions = OutInstructions::from([].as_slice());
|
||||
let receipt = publish_outs(key, 1, Coin::Ether, fee, instructions);
|
||||
let receipt = publish_outs(&provider, &router, key, 1, Coin::Ether, fee, instructions).await;
|
||||
assert!(receipt.status());
|
||||
println!("empty execute used {} gas:", receipt.gas_used);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user