mirror of
https://github.com/serai-dex/serai.git
synced 2025-12-13 14:39: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-client",
|
||||||
"alloy-rpc-types-eth",
|
"alloy-rpc-types-eth",
|
||||||
"alloy-simple-request-transport",
|
"alloy-simple-request-transport",
|
||||||
|
"alloy-sol-macro",
|
||||||
"alloy-sol-macro-expander",
|
"alloy-sol-macro-expander",
|
||||||
"alloy-sol-macro-input",
|
"alloy-sol-macro-input",
|
||||||
"alloy-sol-types",
|
"alloy-sol-types",
|
||||||
|
|||||||
@@ -20,7 +20,9 @@ workspace = true
|
|||||||
group = { version = "0.13", default-features = false }
|
group = { version = "0.13", default-features = false }
|
||||||
|
|
||||||
alloy-core = { version = "0.8", default-features = false }
|
alloy-core = { version = "0.8", default-features = false }
|
||||||
|
|
||||||
alloy-sol-types = { 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 }
|
alloy-consensus = { version = "0.3", default-features = false }
|
||||||
|
|
||||||
|
|||||||
@@ -26,17 +26,14 @@ fn main() {
|
|||||||
fs::create_dir(&artifacts_path).unwrap();
|
fs::create_dir(&artifacts_path).unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
build_solidity_contracts::build(
|
// This cannot be handled with the sol! macro. The Router requires an import
|
||||||
&["../../../networks/ethereum/schnorr/contracts", "../erc20/contracts"],
|
|
||||||
"contracts",
|
|
||||||
&artifacts_path,
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
// This cannot be handled with the sol! macro. The Solidity requires an import
|
|
||||||
// https://github.com/alloy-rs/core/issues/602
|
// https://github.com/alloy-rs/core/issues/602
|
||||||
sol(
|
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"),
|
&(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
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
pragma solidity ^0.8.26;
|
pragma solidity ^0.8.26;
|
||||||
|
|
||||||
// TODO: MIT licensed interface
|
|
||||||
|
|
||||||
import "IERC20.sol";
|
import "IERC20.sol";
|
||||||
|
|
||||||
import "Schnorr.sol";
|
import "Schnorr.sol";
|
||||||
|
|
||||||
|
import "IRouter.sol";
|
||||||
|
|
||||||
/*
|
/*
|
||||||
The Router directly performs low-level calls in order to fine-tune the gas settings. Since this
|
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
|
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
|
/// @title Serai Router
|
||||||
/// @author Luke Parker <lukeparker@serai.exchange>
|
/// @author Luke Parker <lukeparker@serai.exchange>
|
||||||
/// @notice Intakes coins for the Serai network and handles relaying batches of transfers out
|
/// @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
|
* @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.
|
* used to predict the addresses of deployed contracts ahead of time.
|
||||||
@@ -51,151 +51,141 @@ contract Router {
|
|||||||
/// @dev The address escaped to
|
/// @dev The address escaped to
|
||||||
address private _escapedTo;
|
address private _escapedTo;
|
||||||
|
|
||||||
/// @title The type of destination
|
/// @dev Updates the Serai key. This does not update `_nextNonce`
|
||||||
/// @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`
|
|
||||||
*/
|
|
||||||
/// @param nonceUpdatedWith The nonce used to update the key
|
/// @param nonceUpdatedWith The nonce used to update the key
|
||||||
/// @param newSeraiKey The key updated to
|
/// @param newSeraiKey The key updated to
|
||||||
modifier updateSeraiKeyAtEndOfFn(uint256 nonceUpdatedWith, bytes32 newSeraiKey) {
|
function _updateSeraiKey(uint256 nonceUpdatedWith, bytes32 newSeraiKey) private {
|
||||||
// Run the function itself
|
|
||||||
_;
|
|
||||||
|
|
||||||
// Update the key
|
|
||||||
_seraiKey = newSeraiKey;
|
_seraiKey = newSeraiKey;
|
||||||
emit SeraiKeyUpdated(nonceUpdatedWith, newSeraiKey);
|
emit SeraiKeyUpdated(nonceUpdatedWith, newSeraiKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// @notice The constructor for the relayer
|
/// @notice The constructor for the relayer
|
||||||
/// @param initialSeraiKey The initial key for Serai's Ethereum validators
|
/// @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
|
// 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
|
// This is incompatible with any networks which don't have their nonces start at 0
|
||||||
_smartContractNonce = 1;
|
_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;
|
_nextNonce = 1;
|
||||||
|
|
||||||
// We haven't escaped to any address yet
|
// We haven't escaped to any address yet
|
||||||
_escapedTo = address(0);
|
_escapedTo = address(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// @dev Verify a signature
|
/**
|
||||||
/// @param message The message to pass to the Schnorr contract
|
* @dev
|
||||||
/// @param signature The signature by the current key for this message
|
* Verify a signature of the calldata, placed immediately after the function selector. The
|
||||||
function verifySignature(bytes32 message, Signature calldata signature) private {
|
* 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 the escape hatch was triggered, reject further signatures
|
||||||
if (_escapedTo != address(0)) {
|
if (_escapedTo != address(0)) {
|
||||||
revert EscapeHatchInvoked();
|
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();
|
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
|
// Set the next nonce
|
||||||
unchecked {
|
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
|
/// @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
|
* @dev This assumes the key is correct. No checks on it are performed.
|
||||||
/// @param signature The signature by the current key authorizing this update
|
*
|
||||||
function updateSeraiKey(bytes32 newSeraiKey, Signature calldata signature)
|
* The hex bytes are to cause a collision with `IRouter.updateSeraiKey`.
|
||||||
external
|
|
||||||
updateSeraiKeyAtEndOfFn(_nextNonce, newSeraiKey)
|
|
||||||
{
|
|
||||||
/*
|
|
||||||
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.
|
|
||||||
*/
|
*/
|
||||||
bytes32 message = keccak256(abi.encodePacked("updateSeraiKey", _nextNonce, newSeraiKey));
|
// @param signature The signature by the current key authorizing this update
|
||||||
verifySignature(message, signature);
|
// @param newSeraiKey The key to update to
|
||||||
|
function updateSeraiKey5A8542A2() 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);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// @notice Transfer coins into Serai with an instruction
|
/// @notice Transfer coins into Serai with an instruction
|
||||||
@@ -355,28 +345,23 @@ contract Router {
|
|||||||
/// @notice Execute a batch of `OutInstruction`s
|
/// @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
|
* @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 signature The signature by the current key for Serai's Ethereum validators
|
||||||
/// @param fee The fee to pay (in coin) to the caller for their relaying of this batch
|
// @param coin The coin all of these `OutInstruction`s are for
|
||||||
/// @param outs The `OutInstruction`s to act on
|
// @param fee The fee to pay (in coin) to the caller for their relaying of this batch
|
||||||
/// @param signature The signature by the current key for Serai's Ethereum validators
|
// @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
|
// slither-disable-next-line calls-loop
|
||||||
function execute(
|
function execute4DE42904() external {
|
||||||
address coin,
|
(uint256 nonceUsed, bytes memory args, bytes32 message) = verifySignature();
|
||||||
uint256 fee,
|
(,, address coin, uint256 fee, IRouter.OutInstruction[] memory outs) =
|
||||||
OutInstruction[] calldata outs,
|
abi.decode(args, (bytes32, bytes32, address, uint256, IRouter.OutInstruction[]));
|
||||||
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);
|
|
||||||
|
|
||||||
// TODO: Also include a bit mask here
|
// 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
|
Since we don't have a re-entrancy guard, it is possible for instructions from later batches to
|
||||||
@@ -389,7 +374,7 @@ contract Router {
|
|||||||
// slither-disable-next-line reentrancy-events
|
// slither-disable-next-line reentrancy-events
|
||||||
for (uint256 i = 0; i < outs.length; i++) {
|
for (uint256 i = 0; i < outs.length; i++) {
|
||||||
// If the destination is an address, we perform a direct transfer
|
// 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
|
trusted to not pass a malformed destination, yet if it ever did, it could simply re-sign a
|
||||||
@@ -415,7 +400,8 @@ contract Router {
|
|||||||
erc20TransferOut(nextAddress, coin, outs[i].amount);
|
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.
|
Perform the deployment with the defined gas budget.
|
||||||
@@ -438,10 +424,19 @@ contract Router {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// @notice Escapes to a new smart contract
|
/// @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
|
* @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
|
*
|
||||||
function escapeHatch(address escapeTo, Signature calldata signature) external {
|
* 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)) {
|
if (escapeTo == address(0)) {
|
||||||
revert InvalidEscapeAddress();
|
revert InvalidEscapeAddress();
|
||||||
}
|
}
|
||||||
@@ -454,10 +449,6 @@ contract Router {
|
|||||||
revert EscapeHatchInvoked();
|
revert EscapeHatchInvoked();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Verify the signature
|
|
||||||
bytes32 message = keccak256(abi.encodePacked("escapeHatch", _nextNonce, escapeTo));
|
|
||||||
verifySignature(message, signature);
|
|
||||||
|
|
||||||
_escapedTo = escapeTo;
|
_escapedTo = escapeTo;
|
||||||
emit EscapeHatch(escapeTo);
|
emit EscapeHatch(escapeTo);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,10 +28,25 @@ use serai_client::networks::ethereum::Address as SeraiAddress;
|
|||||||
#[expect(clippy::all)]
|
#[expect(clippy::all)]
|
||||||
#[expect(clippy::ignored_unit_patterns)]
|
#[expect(clippy::ignored_unit_patterns)]
|
||||||
#[expect(clippy::redundant_closure_for_method_calls)]
|
#[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"));
|
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::{
|
use abi::{
|
||||||
SeraiKeyUpdated as SeraiKeyUpdatedEvent, InInstruction as InInstructionEvent,
|
SeraiKeyUpdated as SeraiKeyUpdatedEvent, InInstruction as InInstructionEvent,
|
||||||
Executed as ExecutedEvent,
|
Executed as ExecutedEvent,
|
||||||
@@ -309,26 +324,37 @@ impl Router {
|
|||||||
|
|
||||||
/// Get the message to be signed in order to update the key for Serai.
|
/// 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> {
|
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::updateSeraiKeyCall::new((
|
||||||
.abi_encode_packed()
|
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.
|
/// Construct a transaction to update the key representing Serai.
|
||||||
pub fn update_serai_key(&self, public_key: &PublicKey, sig: &Signature) -> TxLegacy {
|
pub fn update_serai_key(&self, public_key: &PublicKey, sig: &Signature) -> TxLegacy {
|
||||||
TxLegacy {
|
TxLegacy {
|
||||||
to: TxKind::Call(self.1),
|
to: TxKind::Call(self.1),
|
||||||
input: abi::updateSeraiKeyCall::new((public_key.eth_repr().into(), sig.into()))
|
input: abi::updateSeraiKeyCall::new((
|
||||||
|
abi::Signature::from(sig),
|
||||||
|
public_key.eth_repr().into(),
|
||||||
|
))
|
||||||
.abi_encode()
|
.abi_encode()
|
||||||
.into(),
|
.into(),
|
||||||
gas_limit: 40748 * 120 / 100,
|
gas_limit: 40_889 * 120 / 100,
|
||||||
..Default::default()
|
..Default::default()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get the message to be signed in order to execute a series of `OutInstruction`s.
|
/// 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> {
|
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::executeCall::new((
|
||||||
.abi_encode_sequence()
|
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.
|
/// Construct a transaction to execute a batch of `OutInstruction`s.
|
||||||
@@ -336,9 +362,11 @@ impl Router {
|
|||||||
let outs_len = outs.0.len();
|
let outs_len = outs.0.len();
|
||||||
TxLegacy {
|
TxLegacy {
|
||||||
to: TxKind::Call(self.1),
|
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
|
// 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()
|
..Default::default()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ use alloy_sol_types::SolCall;
|
|||||||
|
|
||||||
use alloy_consensus::TxLegacy;
|
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_simple_request_transport::SimpleRequest;
|
||||||
use alloy_rpc_client::ClientBuilder;
|
use alloy_rpc_client::ClientBuilder;
|
||||||
use alloy_provider::RootProvider;
|
use alloy_provider::RootProvider;
|
||||||
@@ -22,6 +22,18 @@ use ethereum_deployer::Deployer;
|
|||||||
|
|
||||||
use crate::{Coin, OutInstructions, Router};
|
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) {
|
pub(crate) fn test_key() -> (Scalar, PublicKey) {
|
||||||
loop {
|
loop {
|
||||||
let key = Scalar::random(&mut OsRng);
|
let key = Scalar::random(&mut OsRng);
|
||||||
@@ -154,8 +166,16 @@ async fn test_erc20_in_instruction() {
|
|||||||
todo!("TODO")
|
todo!("TODO")
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn publish_outs(key: (Scalar, PublicKey), nonce: u64, coin: Coin, fee: U256, outs: OutInstructions) -> TransactionReceipt {
|
async fn publish_outs(
|
||||||
let msg = Router::execute_message(nonce, coin, fee, instructions.clone());
|
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 nonce = Scalar::random(&mut OsRng);
|
||||||
let c = Signature::challenge(ProjectivePoint::GENERATOR * nonce, &key.1, &msg);
|
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 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;
|
tx.gas_price = 100_000_000_000u128;
|
||||||
let tx = ethereum_primitives::deterministically_sign(&tx);
|
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]
|
#[tokio::test]
|
||||||
@@ -182,7 +202,7 @@ async fn test_eth_address_out_instruction() {
|
|||||||
ethereum_test_primitives::fund_account(&provider, router.address(), amount).await;
|
ethereum_test_primitives::fund_account(&provider, router.address(), amount).await;
|
||||||
|
|
||||||
let instructions = OutInstructions::from([].as_slice());
|
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());
|
assert!(receipt.status());
|
||||||
println!("empty execute used {} gas:", receipt.gas_used);
|
println!("empty execute used {} gas:", receipt.gas_used);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user