mirror of
https://github.com/serai-dex/serai.git
synced 2024-12-26 21:50:26 +00:00
221 lines
8.7 KiB
Solidity
221 lines
8.7 KiB
Solidity
// SPDX-License-Identifier: AGPL-3.0-only
|
|
pragma solidity ^0.8.26;
|
|
|
|
import "IERC20.sol";
|
|
|
|
import "Schnorr.sol";
|
|
|
|
// _ is used as a prefix for internal functions and smart-contract-scoped variables
|
|
contract Router {
|
|
// Nonce is incremented for each command executed, preventing replays
|
|
uint256 private _nonce;
|
|
|
|
// The nonce which will be used for the smart contracts we deploy, enabling
|
|
// predicting their addresses
|
|
uint256 private _smartContractNonce;
|
|
|
|
// The current public key, defined as per the Schnorr library
|
|
bytes32 private _seraiKey;
|
|
|
|
enum DestinationType {
|
|
Address,
|
|
Code
|
|
}
|
|
|
|
struct AddressDestination {
|
|
address destination;
|
|
}
|
|
|
|
struct CodeDestination {
|
|
uint32 gas;
|
|
bytes code;
|
|
}
|
|
|
|
struct OutInstruction {
|
|
DestinationType destinationType;
|
|
bytes destination;
|
|
address coin;
|
|
uint256 value;
|
|
}
|
|
|
|
struct Signature {
|
|
bytes32 c;
|
|
bytes32 s;
|
|
}
|
|
|
|
event SeraiKeyUpdated(uint256 indexed nonce, bytes32 indexed key);
|
|
event InInstruction(
|
|
address indexed from, address indexed coin, uint256 amount, bytes instruction
|
|
);
|
|
event Executed(uint256 indexed nonce, bytes32 indexed message_hash);
|
|
|
|
error InvalidSignature();
|
|
error InvalidAmount();
|
|
error FailedTransfer();
|
|
|
|
// Update the Serai key at the end of the current function.
|
|
modifier _updateSeraiKeyAtEndOfFn(uint256 nonceUpdatedWith, bytes32 newSeraiKey) {
|
|
// Run the function itself.
|
|
_;
|
|
|
|
// Update the key.
|
|
_seraiKey = newSeraiKey;
|
|
emit SeraiKeyUpdated(nonceUpdatedWith, newSeraiKey);
|
|
}
|
|
|
|
constructor(bytes32 initialSeraiKey) _updateSeraiKeyAtEndOfFn(0, initialSeraiKey) {
|
|
// We consumed nonce 0 when setting the initial Serai key
|
|
_nonce = 1;
|
|
// 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;
|
|
}
|
|
|
|
// updateSeraiKey validates the given Schnorr signature against the current public key, and if
|
|
// successful, updates the contract's public key to the one specified.
|
|
function updateSeraiKey(bytes32 newSeraiKey, Signature calldata signature)
|
|
external
|
|
_updateSeraiKeyAtEndOfFn(_nonce, newSeraiKey)
|
|
{
|
|
// This DST needs a length prefix as well to prevent DSTs potentially being substrings of each
|
|
// other, yet this fine for our very well-defined, limited use
|
|
bytes32 message = keccak256(abi.encodePacked("updateSeraiKey", block.chainid, _nonce, newSeraiKey));
|
|
_nonce++;
|
|
|
|
if (!Schnorr.verify(_seraiKey, message, signature.c, signature.s)) {
|
|
revert InvalidSignature();
|
|
}
|
|
}
|
|
|
|
function inInstruction(address coin, uint256 amount, bytes memory instruction) external payable {
|
|
if (coin == address(0)) {
|
|
if (amount != msg.value) {
|
|
revert InvalidAmount();
|
|
}
|
|
} else {
|
|
(bool success, bytes memory res) = address(coin).call(
|
|
abi.encodeWithSelector(IERC20.transferFrom.selector, msg.sender, address(this), amount)
|
|
);
|
|
|
|
// Require there was nothing returned, which is done by some non-standard tokens, or that the
|
|
// ERC20 contract did in fact return true
|
|
bool nonStandardResOrTrue = (res.length == 0) || abi.decode(res, (bool));
|
|
if (!(success && nonStandardResOrTrue)) {
|
|
revert FailedTransfer();
|
|
}
|
|
}
|
|
|
|
/*
|
|
Due to fee-on-transfer tokens, emitting the amount directly is frowned upon. The amount
|
|
instructed to be transferred may not actually be the amount transferred.
|
|
|
|
If we add nonReentrant to every single function which can effect the balance, we can check the
|
|
amount exactly matches. This prevents transfers of less value than expected occurring, at
|
|
least, not without an additional transfer to top up the difference (which isn't routed through
|
|
this contract and accordingly isn't trying to artificially create events from this contract).
|
|
|
|
If we don't add nonReentrant, a transfer can be started, and then a new transfer for the
|
|
difference can follow it up (again and again until a rounding error is reached). This contract
|
|
would believe all transfers were done in full, despite each only being done in part (except
|
|
for the last one).
|
|
|
|
Given fee-on-transfer tokens aren't intended to be supported, the only token actively planned
|
|
to be supported is Dai and it doesn't have any fee-on-transfer logic, and how fee-on-transfer
|
|
tokens aren't even able to be supported at this time by the larger Serai network, we simply
|
|
classify this entire class of tokens as non-standard implementations which induce undefined
|
|
behavior.
|
|
|
|
It is the Serai network's role not to add support for any non-standard implementations.
|
|
*/
|
|
emit InInstruction(msg.sender, coin, amount, instruction);
|
|
}
|
|
|
|
// Perform a transfer out
|
|
function _transferOut(address to, address coin, uint256 value) private {
|
|
/*
|
|
We on purposely do not check if these calls succeed. A call either succeeded, and there's no
|
|
problem, or the call failed due to:
|
|
A) An insolvency
|
|
B) A malicious receiver
|
|
C) A non-standard token
|
|
A is an invariant, B should be dropped, C is something out of the control of this contract.
|
|
It is again the Serai's network role to not add support for any non-standard tokens,
|
|
*/
|
|
if (coin == address(0)) {
|
|
// Enough gas to service the transfer and a minimal amount of logic
|
|
// TODO: If we're constructing a contract, we can do this at the same time as construction
|
|
to.call{ value: value, gas: 5_000 }("");
|
|
} else {
|
|
coin.call{ gas: 100_000 }(abi.encodeWithSelector(IERC20.transfer.selector, msg.sender, value));
|
|
}
|
|
}
|
|
|
|
/*
|
|
Serai supports arbitrary calls out via deploying smart contracts (with user-specified code),
|
|
letting them execute whatever calls they're coded for. Since we can't meter CREATE, we call
|
|
CREATE from this function which we call not internally, but with CALL (which we can meter).
|
|
*/
|
|
function arbitaryCallOut(bytes memory code) external {
|
|
// Because we're creating a contract, increment our nonce
|
|
_smartContractNonce += 1;
|
|
|
|
address contractAddress;
|
|
assembly {
|
|
contractAddress := create(0, add(code, 0x20), mload(code))
|
|
}
|
|
}
|
|
|
|
// Execute a list of transactions if they were signed by the current key with the current nonce
|
|
function execute(OutInstruction[] calldata transactions, Signature calldata signature) external {
|
|
// Verify the signature
|
|
// We hash the message here as we need the message's hash for the Executed event
|
|
// Since we're already going to hash it, hashing it prior to verifying the signature reduces the
|
|
// amount of words hashed by its challenge function (reducing our gas costs)
|
|
bytes32 message = keccak256(abi.encode("execute", block.chainid, _nonce, transactions));
|
|
if (!Schnorr.verify(_seraiKey, message, signature.c, signature.s)) {
|
|
revert InvalidSignature();
|
|
}
|
|
|
|
// Since the signature was verified, perform execution
|
|
emit Executed(_nonce, message);
|
|
// While this is sufficient to prevent replays, it's still technically possible for instructions
|
|
// from later batches to be executed before these instructions upon re-entrancy
|
|
_nonce++;
|
|
|
|
for (uint256 i = 0; i < transactions.length; i++) {
|
|
// If the destination is an address, we perform a direct transfer
|
|
if (transactions[i].destinationType == DestinationType.Address) {
|
|
// This may cause a panic and the contract to become stuck if the destination isn't actually
|
|
// 20 bytes. Serai is trusted to not pass a malformed destination
|
|
(AddressDestination memory destination) = abi.decode(transactions[i].destination, (AddressDestination));
|
|
_transferOut(destination.destination, transactions[i].coin, transactions[i].value);
|
|
} else {
|
|
// The destination is a piece of initcode. We calculate the hash of the will-be contract,
|
|
// transfer to it, and then run the initcode
|
|
address nextAddress =
|
|
address(uint160(uint256(keccak256(abi.encode(address(this), _smartContractNonce)))));
|
|
|
|
// Perform the transfer
|
|
_transferOut(nextAddress, transactions[i].coin, transactions[i].value);
|
|
|
|
// Perform the calls with a set gas budget
|
|
(CodeDestination memory destination) = abi.decode(transactions[i].destination, (CodeDestination));
|
|
address(this).call{ gas: destination.gas }(
|
|
abi.encodeWithSelector(Router.arbitaryCallOut.selector, destination.code)
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
function nonce() external view returns (uint256) {
|
|
return _nonce;
|
|
}
|
|
|
|
function smartContractNonce() external view returns (uint256) {
|
|
return _smartContractNonce;
|
|
}
|
|
|
|
function seraiKey() external view returns (bytes32) {
|
|
return _seraiKey;
|
|
}
|
|
}
|