mirror of
https://github.com/serai-dex/serai.git
synced 2025-12-11 21:49:26 +00:00
Compare commits
10 Commits
653b0e0bbc
...
ff-0.14
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e7e8fd6388 | ||
|
|
cc4a65e82a | ||
|
|
4e0c58464f | ||
|
|
205da3fd38 | ||
|
|
f7e63d4944 | ||
|
|
b5608fc3d2 | ||
|
|
33018bf6da | ||
|
|
bef90b2f1a | ||
|
|
184c02714a | ||
|
|
5a7b815e2e |
2
.github/nightly-version
vendored
2
.github/nightly-version
vendored
@@ -1 +1 @@
|
||||
nightly-2025-01-01
|
||||
nightly-2025-02-01
|
||||
|
||||
1528
Cargo.lock
generated
1528
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -208,6 +208,7 @@ directories-next = { path = "patches/directories-next" }
|
||||
[workspace.lints.clippy]
|
||||
unwrap_or_default = "allow"
|
||||
map_unwrap_or = "allow"
|
||||
needless_continue = "allow"
|
||||
borrow_as_ptr = "deny"
|
||||
cast_lossless = "deny"
|
||||
cast_possible_truncation = "deny"
|
||||
@@ -238,7 +239,6 @@ manual_string_new = "deny"
|
||||
match_bool = "deny"
|
||||
match_same_arms = "deny"
|
||||
missing_fields_in_debug = "deny"
|
||||
needless_continue = "deny"
|
||||
needless_pass_by_value = "deny"
|
||||
ptr_cast_constness = "deny"
|
||||
range_minus_one = "deny"
|
||||
|
||||
Binary file not shown.
427
audits/Trail of Bits ethereum contracts April 2025/LICENSE
Normal file
427
audits/Trail of Bits ethereum contracts April 2025/LICENSE
Normal file
@@ -0,0 +1,427 @@
|
||||
Attribution-ShareAlike 4.0 International
|
||||
|
||||
=======================================================================
|
||||
|
||||
Creative Commons Corporation ("Creative Commons") is not a law firm and
|
||||
does not provide legal services or legal advice. Distribution of
|
||||
Creative Commons public licenses does not create a lawyer-client or
|
||||
other relationship. Creative Commons makes its licenses and related
|
||||
information available on an "as-is" basis. Creative Commons gives no
|
||||
warranties regarding its licenses, any material licensed under their
|
||||
terms and conditions, or any related information. Creative Commons
|
||||
disclaims all liability for damages resulting from their use to the
|
||||
fullest extent possible.
|
||||
|
||||
Using Creative Commons Public Licenses
|
||||
|
||||
Creative Commons public licenses provide a standard set of terms and
|
||||
conditions that creators and other rights holders may use to share
|
||||
original works of authorship and other material subject to copyright
|
||||
and certain other rights specified in the public license below. The
|
||||
following considerations are for informational purposes only, are not
|
||||
exhaustive, and do not form part of our licenses.
|
||||
|
||||
Considerations for licensors: Our public licenses are
|
||||
intended for use by those authorized to give the public
|
||||
permission to use material in ways otherwise restricted by
|
||||
copyright and certain other rights. Our licenses are
|
||||
irrevocable. Licensors should read and understand the terms
|
||||
and conditions of the license they choose before applying it.
|
||||
Licensors should also secure all rights necessary before
|
||||
applying our licenses so that the public can reuse the
|
||||
material as expected. Licensors should clearly mark any
|
||||
material not subject to the license. This includes other CC-
|
||||
licensed material, or material used under an exception or
|
||||
limitation to copyright. More considerations for licensors:
|
||||
wiki.creativecommons.org/Considerations_for_licensors
|
||||
|
||||
Considerations for the public: By using one of our public
|
||||
licenses, a licensor grants the public permission to use the
|
||||
licensed material under specified terms and conditions. If
|
||||
the licensor's permission is not necessary for any reason--for
|
||||
example, because of any applicable exception or limitation to
|
||||
copyright--then that use is not regulated by the license. Our
|
||||
licenses grant only permissions under copyright and certain
|
||||
other rights that a licensor has authority to grant. Use of
|
||||
the licensed material may still be restricted for other
|
||||
reasons, including because others have copyright or other
|
||||
rights in the material. A licensor may make special requests,
|
||||
such as asking that all changes be marked or described.
|
||||
Although not required by our licenses, you are encouraged to
|
||||
respect those requests where reasonable. More considerations
|
||||
for the public:
|
||||
wiki.creativecommons.org/Considerations_for_licensees
|
||||
|
||||
=======================================================================
|
||||
|
||||
Creative Commons Attribution-ShareAlike 4.0 International Public
|
||||
License
|
||||
|
||||
By exercising the Licensed Rights (defined below), You accept and agree
|
||||
to be bound by the terms and conditions of this Creative Commons
|
||||
Attribution-ShareAlike 4.0 International Public License ("Public
|
||||
License"). To the extent this Public License may be interpreted as a
|
||||
contract, You are granted the Licensed Rights in consideration of Your
|
||||
acceptance of these terms and conditions, and the Licensor grants You
|
||||
such rights in consideration of benefits the Licensor receives from
|
||||
making the Licensed Material available under these terms and
|
||||
conditions.
|
||||
|
||||
|
||||
Section 1 -- Definitions.
|
||||
|
||||
a. Adapted Material means material subject to Copyright and Similar
|
||||
Rights that is derived from or based upon the Licensed Material
|
||||
and in which the Licensed Material is translated, altered,
|
||||
arranged, transformed, or otherwise modified in a manner requiring
|
||||
permission under the Copyright and Similar Rights held by the
|
||||
Licensor. For purposes of this Public License, where the Licensed
|
||||
Material is a musical work, performance, or sound recording,
|
||||
Adapted Material is always produced where the Licensed Material is
|
||||
synched in timed relation with a moving image.
|
||||
|
||||
b. Adapter's License means the license You apply to Your Copyright
|
||||
and Similar Rights in Your contributions to Adapted Material in
|
||||
accordance with the terms and conditions of this Public License.
|
||||
|
||||
c. BY-SA Compatible License means a license listed at
|
||||
creativecommons.org/compatiblelicenses, approved by Creative
|
||||
Commons as essentially the equivalent of this Public License.
|
||||
|
||||
d. Copyright and Similar Rights means copyright and/or similar rights
|
||||
closely related to copyright including, without limitation,
|
||||
performance, broadcast, sound recording, and Sui Generis Database
|
||||
Rights, without regard to how the rights are labeled or
|
||||
categorized. For purposes of this Public License, the rights
|
||||
specified in Section 2(b)(1)-(2) are not Copyright and Similar
|
||||
Rights.
|
||||
|
||||
e. Effective Technological Measures means those measures that, in the
|
||||
absence of proper authority, may not be circumvented under laws
|
||||
fulfilling obligations under Article 11 of the WIPO Copyright
|
||||
Treaty adopted on December 20, 1996, and/or similar international
|
||||
agreements.
|
||||
|
||||
f. Exceptions and Limitations means fair use, fair dealing, and/or
|
||||
any other exception or limitation to Copyright and Similar Rights
|
||||
that applies to Your use of the Licensed Material.
|
||||
|
||||
g. License Elements means the license attributes listed in the name
|
||||
of a Creative Commons Public License. The License Elements of this
|
||||
Public License are Attribution and ShareAlike.
|
||||
|
||||
h. Licensed Material means the artistic or literary work, database,
|
||||
or other material to which the Licensor applied this Public
|
||||
License.
|
||||
|
||||
i. Licensed Rights means the rights granted to You subject to the
|
||||
terms and conditions of this Public License, which are limited to
|
||||
all Copyright and Similar Rights that apply to Your use of the
|
||||
Licensed Material and that the Licensor has authority to license.
|
||||
|
||||
j. Licensor means the individual(s) or entity(ies) granting rights
|
||||
under this Public License.
|
||||
|
||||
k. Share means to provide material to the public by any means or
|
||||
process that requires permission under the Licensed Rights, such
|
||||
as reproduction, public display, public performance, distribution,
|
||||
dissemination, communication, or importation, and to make material
|
||||
available to the public including in ways that members of the
|
||||
public may access the material from a place and at a time
|
||||
individually chosen by them.
|
||||
|
||||
l. Sui Generis Database Rights means rights other than copyright
|
||||
resulting from Directive 96/9/EC of the European Parliament and of
|
||||
the Council of 11 March 1996 on the legal protection of databases,
|
||||
as amended and/or succeeded, as well as other essentially
|
||||
equivalent rights anywhere in the world.
|
||||
|
||||
m. You means the individual or entity exercising the Licensed Rights
|
||||
under this Public License. Your has a corresponding meaning.
|
||||
|
||||
|
||||
Section 2 -- Scope.
|
||||
|
||||
a. License grant.
|
||||
|
||||
1. Subject to the terms and conditions of this Public License,
|
||||
the Licensor hereby grants You a worldwide, royalty-free,
|
||||
non-sublicensable, non-exclusive, irrevocable license to
|
||||
exercise the Licensed Rights in the Licensed Material to:
|
||||
|
||||
a. reproduce and Share the Licensed Material, in whole or
|
||||
in part; and
|
||||
|
||||
b. produce, reproduce, and Share Adapted Material.
|
||||
|
||||
2. Exceptions and Limitations. For the avoidance of doubt, where
|
||||
Exceptions and Limitations apply to Your use, this Public
|
||||
License does not apply, and You do not need to comply with
|
||||
its terms and conditions.
|
||||
|
||||
3. Term. The term of this Public License is specified in Section
|
||||
6(a).
|
||||
|
||||
4. Media and formats; technical modifications allowed. The
|
||||
Licensor authorizes You to exercise the Licensed Rights in
|
||||
all media and formats whether now known or hereafter created,
|
||||
and to make technical modifications necessary to do so. The
|
||||
Licensor waives and/or agrees not to assert any right or
|
||||
authority to forbid You from making technical modifications
|
||||
necessary to exercise the Licensed Rights, including
|
||||
technical modifications necessary to circumvent Effective
|
||||
Technological Measures. For purposes of this Public License,
|
||||
simply making modifications authorized by this Section 2(a)
|
||||
(4) never produces Adapted Material.
|
||||
|
||||
5. Downstream recipients.
|
||||
|
||||
a. Offer from the Licensor -- Licensed Material. Every
|
||||
recipient of the Licensed Material automatically
|
||||
receives an offer from the Licensor to exercise the
|
||||
Licensed Rights under the terms and conditions of this
|
||||
Public License.
|
||||
|
||||
b. Additional offer from the Licensor -- Adapted Material.
|
||||
Every recipient of Adapted Material from You
|
||||
automatically receives an offer from the Licensor to
|
||||
exercise the Licensed Rights in the Adapted Material
|
||||
under the conditions of the Adapter's License You apply.
|
||||
|
||||
c. No downstream restrictions. You may not offer or impose
|
||||
any additional or different terms or conditions on, or
|
||||
apply any Effective Technological Measures to, the
|
||||
Licensed Material if doing so restricts exercise of the
|
||||
Licensed Rights by any recipient of the Licensed
|
||||
Material.
|
||||
|
||||
6. No endorsement. Nothing in this Public License constitutes or
|
||||
may be construed as permission to assert or imply that You
|
||||
are, or that Your use of the Licensed Material is, connected
|
||||
with, or sponsored, endorsed, or granted official status by,
|
||||
the Licensor or others designated to receive attribution as
|
||||
provided in Section 3(a)(1)(A)(i).
|
||||
|
||||
b. Other rights.
|
||||
|
||||
1. Moral rights, such as the right of integrity, are not
|
||||
licensed under this Public License, nor are publicity,
|
||||
privacy, and/or other similar personality rights; however, to
|
||||
the extent possible, the Licensor waives and/or agrees not to
|
||||
assert any such rights held by the Licensor to the limited
|
||||
extent necessary to allow You to exercise the Licensed
|
||||
Rights, but not otherwise.
|
||||
|
||||
2. Patent and trademark rights are not licensed under this
|
||||
Public License.
|
||||
|
||||
3. To the extent possible, the Licensor waives any right to
|
||||
collect royalties from You for the exercise of the Licensed
|
||||
Rights, whether directly or through a collecting society
|
||||
under any voluntary or waivable statutory or compulsory
|
||||
licensing scheme. In all other cases the Licensor expressly
|
||||
reserves any right to collect such royalties.
|
||||
|
||||
|
||||
Section 3 -- License Conditions.
|
||||
|
||||
Your exercise of the Licensed Rights is expressly made subject to the
|
||||
following conditions.
|
||||
|
||||
a. Attribution.
|
||||
|
||||
1. If You Share the Licensed Material (including in modified
|
||||
form), You must:
|
||||
|
||||
a. retain the following if it is supplied by the Licensor
|
||||
with the Licensed Material:
|
||||
|
||||
i. identification of the creator(s) of the Licensed
|
||||
Material and any others designated to receive
|
||||
attribution, in any reasonable manner requested by
|
||||
the Licensor (including by pseudonym if
|
||||
designated);
|
||||
|
||||
ii. a copyright notice;
|
||||
|
||||
iii. a notice that refers to this Public License;
|
||||
|
||||
iv. a notice that refers to the disclaimer of
|
||||
warranties;
|
||||
|
||||
v. a URI or hyperlink to the Licensed Material to the
|
||||
extent reasonably practicable;
|
||||
|
||||
b. indicate if You modified the Licensed Material and
|
||||
retain an indication of any previous modifications; and
|
||||
|
||||
c. indicate the Licensed Material is licensed under this
|
||||
Public License, and include the text of, or the URI or
|
||||
hyperlink to, this Public License.
|
||||
|
||||
2. You may satisfy the conditions in Section 3(a)(1) in any
|
||||
reasonable manner based on the medium, means, and context in
|
||||
which You Share the Licensed Material. For example, it may be
|
||||
reasonable to satisfy the conditions by providing a URI or
|
||||
hyperlink to a resource that includes the required
|
||||
information.
|
||||
|
||||
3. If requested by the Licensor, You must remove any of the
|
||||
information required by Section 3(a)(1)(A) to the extent
|
||||
reasonably practicable.
|
||||
|
||||
b. ShareAlike.
|
||||
|
||||
In addition to the conditions in Section 3(a), if You Share
|
||||
Adapted Material You produce, the following conditions also apply.
|
||||
|
||||
1. The Adapter's License You apply must be a Creative Commons
|
||||
license with the same License Elements, this version or
|
||||
later, or a BY-SA Compatible License.
|
||||
|
||||
2. You must include the text of, or the URI or hyperlink to, the
|
||||
Adapter's License You apply. You may satisfy this condition
|
||||
in any reasonable manner based on the medium, means, and
|
||||
context in which You Share Adapted Material.
|
||||
|
||||
3. You may not offer or impose any additional or different terms
|
||||
or conditions on, or apply any Effective Technological
|
||||
Measures to, Adapted Material that restrict exercise of the
|
||||
rights granted under the Adapter's License You apply.
|
||||
|
||||
|
||||
Section 4 -- Sui Generis Database Rights.
|
||||
|
||||
Where the Licensed Rights include Sui Generis Database Rights that
|
||||
apply to Your use of the Licensed Material:
|
||||
|
||||
a. for the avoidance of doubt, Section 2(a)(1) grants You the right
|
||||
to extract, reuse, reproduce, and Share all or a substantial
|
||||
portion of the contents of the database;
|
||||
|
||||
b. if You include all or a substantial portion of the database
|
||||
contents in a database in which You have Sui Generis Database
|
||||
Rights, then the database in which You have Sui Generis Database
|
||||
Rights (but not its individual contents) is Adapted Material,
|
||||
|
||||
including for purposes of Section 3(b); and
|
||||
c. You must comply with the conditions in Section 3(a) if You Share
|
||||
all or a substantial portion of the contents of the database.
|
||||
|
||||
For the avoidance of doubt, this Section 4 supplements and does not
|
||||
replace Your obligations under this Public License where the Licensed
|
||||
Rights include other Copyright and Similar Rights.
|
||||
|
||||
|
||||
Section 5 -- Disclaimer of Warranties and Limitation of Liability.
|
||||
|
||||
a. UNLESS OTHERWISE SEPARATELY UNDERTAKEN BY THE LICENSOR, TO THE
|
||||
EXTENT POSSIBLE, THE LICENSOR OFFERS THE LICENSED MATERIAL AS-IS
|
||||
AND AS-AVAILABLE, AND MAKES NO REPRESENTATIONS OR WARRANTIES OF
|
||||
ANY KIND CONCERNING THE LICENSED MATERIAL, WHETHER EXPRESS,
|
||||
IMPLIED, STATUTORY, OR OTHER. THIS INCLUDES, WITHOUT LIMITATION,
|
||||
WARRANTIES OF TITLE, MERCHANTABILITY, FITNESS FOR A PARTICULAR
|
||||
PURPOSE, NON-INFRINGEMENT, ABSENCE OF LATENT OR OTHER DEFECTS,
|
||||
ACCURACY, OR THE PRESENCE OR ABSENCE OF ERRORS, WHETHER OR NOT
|
||||
KNOWN OR DISCOVERABLE. WHERE DISCLAIMERS OF WARRANTIES ARE NOT
|
||||
ALLOWED IN FULL OR IN PART, THIS DISCLAIMER MAY NOT APPLY TO YOU.
|
||||
|
||||
b. TO THE EXTENT POSSIBLE, IN NO EVENT WILL THE LICENSOR BE LIABLE
|
||||
TO YOU ON ANY LEGAL THEORY (INCLUDING, WITHOUT LIMITATION,
|
||||
NEGLIGENCE) OR OTHERWISE FOR ANY DIRECT, SPECIAL, INDIRECT,
|
||||
INCIDENTAL, CONSEQUENTIAL, PUNITIVE, EXEMPLARY, OR OTHER LOSSES,
|
||||
COSTS, EXPENSES, OR DAMAGES ARISING OUT OF THIS PUBLIC LICENSE OR
|
||||
USE OF THE LICENSED MATERIAL, EVEN IF THE LICENSOR HAS BEEN
|
||||
ADVISED OF THE POSSIBILITY OF SUCH LOSSES, COSTS, EXPENSES, OR
|
||||
DAMAGES. WHERE A LIMITATION OF LIABILITY IS NOT ALLOWED IN FULL OR
|
||||
IN PART, THIS LIMITATION MAY NOT APPLY TO YOU.
|
||||
|
||||
c. The disclaimer of warranties and limitation of liability provided
|
||||
above shall be interpreted in a manner that, to the extent
|
||||
possible, most closely approximates an absolute disclaimer and
|
||||
waiver of all liability.
|
||||
|
||||
|
||||
Section 6 -- Term and Termination.
|
||||
|
||||
a. This Public License applies for the term of the Copyright and
|
||||
Similar Rights licensed here. However, if You fail to comply with
|
||||
this Public License, then Your rights under this Public License
|
||||
terminate automatically.
|
||||
|
||||
b. Where Your right to use the Licensed Material has terminated under
|
||||
Section 6(a), it reinstates:
|
||||
|
||||
1. automatically as of the date the violation is cured, provided
|
||||
it is cured within 30 days of Your discovery of the
|
||||
violation; or
|
||||
|
||||
2. upon express reinstatement by the Licensor.
|
||||
|
||||
For the avoidance of doubt, this Section 6(b) does not affect any
|
||||
right the Licensor may have to seek remedies for Your violations
|
||||
of this Public License.
|
||||
|
||||
c. For the avoidance of doubt, the Licensor may also offer the
|
||||
Licensed Material under separate terms or conditions or stop
|
||||
distributing the Licensed Material at any time; however, doing so
|
||||
will not terminate this Public License.
|
||||
|
||||
d. Sections 1, 5, 6, 7, and 8 survive termination of this Public
|
||||
License.
|
||||
|
||||
|
||||
Section 7 -- Other Terms and Conditions.
|
||||
|
||||
a. The Licensor shall not be bound by any additional or different
|
||||
terms or conditions communicated by You unless expressly agreed.
|
||||
|
||||
b. Any arrangements, understandings, or agreements regarding the
|
||||
Licensed Material not stated herein are separate from and
|
||||
independent of the terms and conditions of this Public License.
|
||||
|
||||
|
||||
Section 8 -- Interpretation.
|
||||
|
||||
a. For the avoidance of doubt, this Public License does not, and
|
||||
shall not be interpreted to, reduce, limit, restrict, or impose
|
||||
conditions on any use of the Licensed Material that could lawfully
|
||||
be made without permission under this Public License.
|
||||
|
||||
b. To the extent possible, if any provision of this Public License is
|
||||
deemed unenforceable, it shall be automatically reformed to the
|
||||
minimum extent necessary to make it enforceable. If the provision
|
||||
cannot be reformed, it shall be severed from this Public License
|
||||
without affecting the enforceability of the remaining terms and
|
||||
conditions.
|
||||
|
||||
c. No term or condition of this Public License will be waived and no
|
||||
failure to comply consented to unless expressly agreed to by the
|
||||
Licensor.
|
||||
|
||||
d. Nothing in this Public License constitutes or may be interpreted
|
||||
as a limitation upon, or waiver of, any privileges and immunities
|
||||
that apply to the Licensor or You, including from the legal
|
||||
processes of any jurisdiction or authority.
|
||||
|
||||
|
||||
=======================================================================
|
||||
|
||||
Creative Commons is not a party to its public
|
||||
licenses. Notwithstanding, Creative Commons may elect to apply one of
|
||||
its public licenses to material it publishes and in those instances
|
||||
will be considered the “Licensor.” The text of the Creative Commons
|
||||
public licenses is dedicated to the public domain under the CC0 Public
|
||||
Domain Dedication. Except for the limited purpose of indicating that
|
||||
material is shared under a Creative Commons public license or as
|
||||
otherwise permitted by the Creative Commons policies published at
|
||||
creativecommons.org/policies, Creative Commons does not authorize the
|
||||
use of the trademark "Creative Commons" or any other trademark or logo
|
||||
of Creative Commons without its prior written consent including,
|
||||
without limitation, in connection with any unauthorized modifications
|
||||
to any of its public licenses or any other arrangements,
|
||||
understandings, or agreements concerning use of licensed material. For
|
||||
the avoidance of doubt, this paragraph does not form part of the
|
||||
public licenses.
|
||||
|
||||
Creative Commons may be contacted at creativecommons.org.
|
||||
14
audits/Trail of Bits ethereum contracts April 2025/README.md
Normal file
14
audits/Trail of Bits ethereum contracts April 2025/README.md
Normal file
@@ -0,0 +1,14 @@
|
||||
# Trail of Bits Ethereum Contracts Audit, June 2025
|
||||
|
||||
This audit included:
|
||||
- Our Schnorr contract and associated library (/networks/ethereum/schnorr)
|
||||
- Our Ethereum primitives library (/processor/ethereum/primitives)
|
||||
- Our Deployer contract and associated library (/processor/ethereum/deployer)
|
||||
- Our ERC20 library (/processor/ethereum/erc20)
|
||||
- Our Router contract and associated library (/processor/ethereum/router)
|
||||
|
||||
It is encompassing up to commit 4e0c58464fc4673623938335f06e2e9ea96ca8dd.
|
||||
|
||||
Please see
|
||||
https://github.com/trailofbits/publications/blob/30c4fa3ebf39ff8e4d23ba9567344ec9691697b5/reviews/2025-04-serai-dex-security-review.pdf
|
||||
for provenance.
|
||||
@@ -9,7 +9,7 @@ pub(crate) fn merkle(hash_args: &[[u8; 32]]) -> [u8; 32] {
|
||||
let zero = [0; 32];
|
||||
let mut interim;
|
||||
while hashes.len() > 1 {
|
||||
interim = Vec::with_capacity((hashes.len() + 1) / 2);
|
||||
interim = Vec::with_capacity(hashes.len().div_ceil(2));
|
||||
|
||||
let mut i = 0;
|
||||
while i < hashes.len() {
|
||||
|
||||
@@ -61,7 +61,7 @@ impl Topic {
|
||||
attempt: attempt + 1,
|
||||
round: SigningProtocolRound::Preprocess,
|
||||
}),
|
||||
Topic::SlashReport { .. } => None,
|
||||
Topic::SlashReport => None,
|
||||
Topic::Sign { id, attempt, round: _ } => {
|
||||
Some(Topic::Sign { id, attempt: attempt + 1, round: SigningProtocolRound::Preprocess })
|
||||
}
|
||||
@@ -83,7 +83,7 @@ impl Topic {
|
||||
}
|
||||
SigningProtocolRound::Share => None,
|
||||
},
|
||||
Topic::SlashReport { .. } => None,
|
||||
Topic::SlashReport => None,
|
||||
Topic::Sign { id, attempt, round } => match round {
|
||||
SigningProtocolRound::Preprocess => {
|
||||
let attempt = attempt + 1;
|
||||
@@ -102,7 +102,7 @@ impl Topic {
|
||||
match self {
|
||||
Topic::RemoveParticipant { .. } => None,
|
||||
Topic::DkgConfirmation { .. } => None,
|
||||
Topic::SlashReport { .. } => None,
|
||||
Topic::SlashReport => None,
|
||||
Topic::Sign { id, attempt, round: _ } => Some(SignId { session: set.session, id, attempt }),
|
||||
}
|
||||
}
|
||||
@@ -129,7 +129,7 @@ impl Topic {
|
||||
};
|
||||
SignId { session: set.session, id, attempt }
|
||||
}),
|
||||
Topic::SlashReport { .. } => None,
|
||||
Topic::SlashReport => None,
|
||||
Topic::Sign { .. } => None,
|
||||
}
|
||||
}
|
||||
@@ -147,7 +147,7 @@ impl Topic {
|
||||
Some(Topic::DkgConfirmation { attempt, round: SigningProtocolRound::Preprocess })
|
||||
}
|
||||
},
|
||||
Topic::SlashReport { .. } => None,
|
||||
Topic::SlashReport => None,
|
||||
Topic::Sign { id, attempt, round } => match round {
|
||||
SigningProtocolRound::Preprocess => None,
|
||||
SigningProtocolRound::Share => {
|
||||
@@ -170,7 +170,7 @@ impl Topic {
|
||||
}
|
||||
SigningProtocolRound::Share => None,
|
||||
},
|
||||
Topic::SlashReport { .. } => None,
|
||||
Topic::SlashReport => None,
|
||||
Topic::Sign { id, attempt, round } => match round {
|
||||
SigningProtocolRound::Preprocess => {
|
||||
Some(Topic::Sign { id, attempt, round: SigningProtocolRound::Share })
|
||||
@@ -189,7 +189,7 @@ impl Topic {
|
||||
// We don't require recognition for the first attempt, solely the re-attempts
|
||||
Topic::DkgConfirmation { attempt, .. } => *attempt != 0,
|
||||
// We don't require recognition for the slash report
|
||||
Topic::SlashReport { .. } => false,
|
||||
Topic::SlashReport => false,
|
||||
// We do require recognition for every sign protocol
|
||||
Topic::Sign { .. } => true,
|
||||
}
|
||||
@@ -206,7 +206,7 @@ impl Topic {
|
||||
match self {
|
||||
Topic::RemoveParticipant { .. } => Participating::Everyone,
|
||||
Topic::DkgConfirmation { .. } => Participating::Participated,
|
||||
Topic::SlashReport { .. } => Participating::Everyone,
|
||||
Topic::SlashReport => Participating::Everyone,
|
||||
Topic::Sign { .. } => Participating::Participated,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,12 +17,12 @@ rustdoc-args = ["--cfg", "docsrs"]
|
||||
workspace = true
|
||||
|
||||
[dependencies]
|
||||
rand_core = "0.6"
|
||||
rand_core = "0.9"
|
||||
|
||||
subtle = "^2.4"
|
||||
|
||||
ff = { version = "0.13", features = ["bits"] }
|
||||
group = "0.13"
|
||||
ff = { version = "0.14.0-pre.0", features = ["bits"] }
|
||||
group = "0.14.0-pre.0"
|
||||
|
||||
[dev-dependencies]
|
||||
k256 = { version = "^0.13.1", default-features = false, features = ["std", "arithmetic", "bits"] }
|
||||
|
||||
@@ -31,9 +31,8 @@ fn weight<D: Send + Clone + SecureDigest, F: PrimeField>(digest: &mut DigestTran
|
||||
// Derive a scalar from enough bits of entropy that bias is < 2^128
|
||||
// This can't be const due to its usage of a generic
|
||||
// Also due to the usize::try_from, yet that could be replaced with an `as`
|
||||
// The + 7 forces it to round up
|
||||
#[allow(non_snake_case)]
|
||||
let BYTES: usize = usize::try_from(((F::NUM_BITS + 128) + 7) / 8).unwrap();
|
||||
let BYTES: usize = usize::try_from((F::NUM_BITS + 128).div_ceil(8)).unwrap();
|
||||
|
||||
let mut remaining = BYTES;
|
||||
|
||||
|
||||
@@ -21,8 +21,8 @@ tower = "0.5"
|
||||
serde_json = { version = "1", default-features = false }
|
||||
simple-request = { path = "../../../common/request", version = "0.1", default-features = false }
|
||||
|
||||
alloy-json-rpc = { version = "0.9", default-features = false }
|
||||
alloy-transport = { version = "0.9", default-features = false }
|
||||
alloy-json-rpc = { version = "0.14", default-features = false }
|
||||
alloy-transport = { version = "0.14", default-features = false }
|
||||
|
||||
[features]
|
||||
default = ["tls"]
|
||||
|
||||
@@ -29,14 +29,14 @@ rand_core = { version = "0.6", default-features = false, features = ["std"] }
|
||||
|
||||
k256 = { version = "^0.13.1", default-features = false, features = ["ecdsa"] }
|
||||
|
||||
alloy-core = { version = "0.8", default-features = false }
|
||||
alloy-sol-types = { version = "0.8", default-features = false }
|
||||
alloy-core = { version = "1", default-features = false }
|
||||
alloy-sol-types = { version = "1", default-features = false }
|
||||
|
||||
alloy-simple-request-transport = { path = "../../../networks/ethereum/alloy-simple-request-transport", default-features = false }
|
||||
alloy-rpc-types-eth = { version = "0.9", default-features = false }
|
||||
alloy-rpc-client = { version = "0.9", default-features = false }
|
||||
alloy-provider = { version = "0.9", default-features = false }
|
||||
alloy-rpc-types-eth = { version = "0.14", default-features = false }
|
||||
alloy-rpc-client = { version = "0.14", default-features = false }
|
||||
alloy-provider = { version = "0.14", default-features = false }
|
||||
|
||||
alloy-node-bindings = { version = "0.9", default-features = false }
|
||||
alloy-node-bindings = { version = "0.14", default-features = false }
|
||||
|
||||
tokio = { version = "1", default-features = false, features = ["macros"] }
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use subtle::Choice;
|
||||
use group::ff::PrimeField;
|
||||
use group::{ff::PrimeField, Group};
|
||||
use k256::{
|
||||
elliptic_curve::{
|
||||
ops::Reduce,
|
||||
@@ -22,6 +22,10 @@ impl PublicKey {
|
||||
/// bounds such as parity).
|
||||
#[must_use]
|
||||
pub fn new(A: ProjectivePoint) -> Option<PublicKey> {
|
||||
if bool::from(A.is_identity()) {
|
||||
None?;
|
||||
}
|
||||
|
||||
let affine = A.to_affine();
|
||||
|
||||
// Only allow even keys to save a word within Ethereum
|
||||
|
||||
@@ -32,7 +32,7 @@ mod abi {
|
||||
pub(crate) use TestSchnorr::*;
|
||||
}
|
||||
|
||||
async fn setup_test() -> (AnvilInstance, Arc<RootProvider<SimpleRequest>>, Address) {
|
||||
async fn setup_test() -> (AnvilInstance, Arc<RootProvider>, Address) {
|
||||
let anvil = Anvil::new().spawn();
|
||||
|
||||
let provider = Arc::new(RootProvider::new(
|
||||
@@ -61,7 +61,7 @@ async fn setup_test() -> (AnvilInstance, Arc<RootProvider<SimpleRequest>>, Addre
|
||||
}
|
||||
|
||||
async fn call_verify(
|
||||
provider: &RootProvider<SimpleRequest>,
|
||||
provider: &RootProvider,
|
||||
address: Address,
|
||||
public_key: &PublicKey,
|
||||
message: &[u8],
|
||||
@@ -80,10 +80,8 @@ async fn call_verify(
|
||||
.abi_encode()
|
||||
.into(),
|
||||
));
|
||||
let bytes = provider.call(&call).await.unwrap();
|
||||
let res = abi::verifyCall::abi_decode_returns(&bytes, true).unwrap();
|
||||
|
||||
res._0
|
||||
let bytes = provider.call(call).await.unwrap();
|
||||
abi::verifyCall::abi_decode_returns(&bytes).unwrap()
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
|
||||
@@ -18,7 +18,7 @@ use crate::{Signature, tests::test_key};
|
||||
fn ecrecover(message: Scalar, odd_y: bool, r: Scalar, s: Scalar) -> Option<[u8; 20]> {
|
||||
let sig = ecdsa::Signature::from_scalars(r, s).ok()?;
|
||||
let message: [u8; 32] = message.to_repr().into();
|
||||
alloy_core::primitives::PrimitiveSignature::from_signature_and_parity(sig, odd_y)
|
||||
alloy_core::primitives::Signature::from_signature_and_parity(sig, odd_y)
|
||||
.recover_address_from_prehash(&alloy_core::primitives::B256::from(message))
|
||||
.ok()
|
||||
.map(Into::into)
|
||||
|
||||
@@ -27,6 +27,11 @@ pub(crate) fn test_key() -> (Scalar, PublicKey) {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_identity_key() {
|
||||
assert!(PublicKey::new(ProjectivePoint::IDENTITY).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_odd_key() {
|
||||
// We generate a valid key to ensure there's not some distinct reason this key is invalid
|
||||
|
||||
@@ -31,14 +31,14 @@ frost = { package = "modular-frost", path = "../../crypto/frost", default-featur
|
||||
|
||||
k256 = { version = "^0.13.1", default-features = false, features = ["std"] }
|
||||
|
||||
alloy-core = { version = "0.8", default-features = false }
|
||||
alloy-core = { version = "1", default-features = false }
|
||||
alloy-rlp = { version = "0.3", default-features = false }
|
||||
|
||||
alloy-rpc-types-eth = { version = "0.9", default-features = false }
|
||||
alloy-transport = { version = "0.9", default-features = false }
|
||||
alloy-rpc-types-eth = { version = "0.14", default-features = false }
|
||||
alloy-transport = { version = "0.14", default-features = false }
|
||||
alloy-simple-request-transport = { path = "../../networks/ethereum/alloy-simple-request-transport", default-features = false }
|
||||
alloy-rpc-client = { version = "0.9", default-features = false }
|
||||
alloy-provider = { version = "0.9", default-features = false }
|
||||
alloy-rpc-client = { version = "0.14", default-features = false }
|
||||
alloy-provider = { version = "0.14", default-features = false }
|
||||
|
||||
serai-client = { path = "../../substrate/client", default-features = false, features = ["ethereum"] }
|
||||
|
||||
|
||||
@@ -17,17 +17,16 @@ rustdoc-args = ["--cfg", "docsrs"]
|
||||
workspace = true
|
||||
|
||||
[dependencies]
|
||||
alloy-core = { version = "0.8", default-features = false }
|
||||
alloy-core = { version = "1", default-features = false }
|
||||
|
||||
alloy-sol-types = { version = "0.8", default-features = false }
|
||||
alloy-sol-macro = { version = "0.8", default-features = false }
|
||||
alloy-sol-types = { version = "1", default-features = false }
|
||||
alloy-sol-macro = { version = "1", default-features = false }
|
||||
|
||||
alloy-consensus = { version = "0.9", default-features = false }
|
||||
alloy-consensus = { version = "0.14", default-features = false }
|
||||
|
||||
alloy-rpc-types-eth = { version = "0.9", default-features = false }
|
||||
alloy-transport = { version = "0.9", default-features = false }
|
||||
alloy-simple-request-transport = { path = "../../../networks/ethereum/alloy-simple-request-transport", default-features = false }
|
||||
alloy-provider = { version = "0.9", default-features = false }
|
||||
alloy-rpc-types-eth = { version = "0.14", default-features = false }
|
||||
alloy-transport = { version = "0.14", default-features = false }
|
||||
alloy-provider = { version = "0.14", default-features = false }
|
||||
|
||||
ethereum-primitives = { package = "serai-processor-ethereum-primitives", path = "../primitives", default-features = false }
|
||||
|
||||
@@ -35,8 +34,9 @@ ethereum-primitives = { package = "serai-processor-ethereum-primitives", path =
|
||||
build-solidity-contracts = { path = "../../../networks/ethereum/build-contracts", default-features = false }
|
||||
|
||||
[dev-dependencies]
|
||||
alloy-rpc-client = { version = "0.9", default-features = false }
|
||||
alloy-node-bindings = { version = "0.9", default-features = false }
|
||||
alloy-simple-request-transport = { path = "../../../networks/ethereum/alloy-simple-request-transport", default-features = false }
|
||||
alloy-rpc-client = { version = "0.14", default-features = false }
|
||||
alloy-node-bindings = { version = "0.14", default-features = false }
|
||||
|
||||
tokio = { version = "1.0", default-features = false, features = ["rt-multi-thread", "macros"] }
|
||||
|
||||
|
||||
@@ -11,7 +11,6 @@ use alloy_sol_types::SolCall;
|
||||
|
||||
use alloy_rpc_types_eth::{TransactionInput, TransactionRequest};
|
||||
use alloy_transport::{TransportErrorKind, RpcError};
|
||||
use alloy_simple_request_transport::SimpleRequest;
|
||||
use alloy_provider::{Provider, RootProvider};
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -44,7 +43,7 @@ const INITCODE: &[u8] = {
|
||||
/// of the EVM. It then supports retrieving the deployed contracts addresses (which aren't
|
||||
/// deterministic) using a single call.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Deployer(Arc<RootProvider<SimpleRequest>>);
|
||||
pub struct Deployer(Arc<RootProvider>);
|
||||
impl Deployer {
|
||||
/// Obtain the transaction to deploy this contract, already signed.
|
||||
///
|
||||
@@ -119,7 +118,7 @@ impl Deployer {
|
||||
///
|
||||
/// This will return `None` if the Deployer has yet to be deployed on-chain.
|
||||
pub async fn new(
|
||||
provider: Arc<RootProvider<SimpleRequest>>,
|
||||
provider: Arc<RootProvider>,
|
||||
) -> Result<Option<Self>, RpcError<TransportErrorKind>> {
|
||||
let address = Self::address();
|
||||
let code = provider.get_code_at(address).await?;
|
||||
@@ -138,16 +137,14 @@ impl Deployer {
|
||||
let call = TransactionRequest::default().to(Self::address()).input(TransactionInput::new(
|
||||
abi::Deployer::deploymentsCall::new((init_code_hash.into(),)).abi_encode().into(),
|
||||
));
|
||||
let bytes = self.0.call(&call).await?;
|
||||
let deployment = abi::Deployer::deploymentsCall::abi_decode_returns(&bytes, true)
|
||||
.map_err(|e| {
|
||||
TransportErrorKind::Custom(
|
||||
format!("node returned a non-address for function returning address: {e:?}").into(),
|
||||
)
|
||||
})?
|
||||
._0;
|
||||
let bytes = self.0.call(call).await?;
|
||||
let deployment = abi::Deployer::deploymentsCall::abi_decode_returns(&bytes).map_err(|e| {
|
||||
TransportErrorKind::Custom(
|
||||
format!("node returned a non-address for function returning address: {e:?}").into(),
|
||||
)
|
||||
})?;
|
||||
|
||||
if **deployment == [0; 20] {
|
||||
if deployment == Address::ZERO {
|
||||
return Ok(None);
|
||||
}
|
||||
Ok(Some(deployment))
|
||||
|
||||
@@ -76,9 +76,9 @@ async fn test_deployer() {
|
||||
let call = TransactionRequest::default()
|
||||
.to(Deployer::address())
|
||||
.input(TransactionInput::new(deploy_tx.tx().input.clone()));
|
||||
let call_err = provider.call(&call).await.unwrap_err();
|
||||
let call_err = provider.call(call).await.unwrap_err();
|
||||
assert!(matches!(
|
||||
call_err.as_error_resp().unwrap().as_decoded_error::<DeployerErrors>(true).unwrap(),
|
||||
call_err.as_error_resp().unwrap().as_decoded_interface_error::<DeployerErrors>().unwrap(),
|
||||
DeployerErrors::PriorDeployed(PriorDeployed {}),
|
||||
));
|
||||
}
|
||||
@@ -97,9 +97,9 @@ async fn test_deployer() {
|
||||
let call = TransactionRequest::default()
|
||||
.to(Deployer::address())
|
||||
.input(TransactionInput::new(deploy_tx.tx().input.clone()));
|
||||
let call_err = provider.call(&call).await.unwrap_err();
|
||||
let call_err = provider.call(call).await.unwrap_err();
|
||||
assert!(matches!(
|
||||
call_err.as_error_resp().unwrap().as_decoded_error::<DeployerErrors>(true).unwrap(),
|
||||
call_err.as_error_resp().unwrap().as_decoded_interface_error::<DeployerErrors>().unwrap(),
|
||||
DeployerErrors::DeploymentFailed(DeploymentFailed {}),
|
||||
));
|
||||
}
|
||||
|
||||
@@ -17,15 +17,14 @@ rustdoc-args = ["--cfg", "docsrs"]
|
||||
workspace = true
|
||||
|
||||
[dependencies]
|
||||
alloy-core = { version = "0.8", default-features = false }
|
||||
alloy-core = { version = "1", default-features = false }
|
||||
|
||||
alloy-sol-types = { version = "0.8", default-features = false }
|
||||
alloy-sol-macro = { version = "0.8", default-features = false }
|
||||
alloy-sol-types = { version = "1", default-features = false }
|
||||
alloy-sol-macro = { version = "1", default-features = false }
|
||||
|
||||
alloy-rpc-types-eth = { version = "0.9", default-features = false }
|
||||
alloy-transport = { version = "0.9", default-features = false }
|
||||
alloy-simple-request-transport = { path = "../../../networks/ethereum/alloy-simple-request-transport", default-features = false }
|
||||
alloy-provider = { version = "0.9", default-features = false }
|
||||
alloy-rpc-types-eth = { version = "0.14", default-features = false }
|
||||
alloy-transport = { version = "0.14", default-features = false }
|
||||
alloy-provider = { version = "0.14", default-features = false }
|
||||
|
||||
ethereum-primitives = { package = "serai-processor-ethereum-primitives", path = "../primitives", default-features = false }
|
||||
|
||||
|
||||
@@ -11,7 +11,6 @@ use alloy_sol_types::{SolInterface, SolEvent};
|
||||
|
||||
use alloy_rpc_types_eth::{Log, Filter, TransactionTrait};
|
||||
use alloy_transport::{TransportErrorKind, RpcError};
|
||||
use alloy_simple_request_transport::SimpleRequest;
|
||||
use alloy_provider::{Provider, RootProvider};
|
||||
|
||||
use ethereum_primitives::LogIndex;
|
||||
@@ -94,7 +93,7 @@ impl Erc20 {
|
||||
// Yielding THE top-level transfer would require tracing the transaction execution and isn't
|
||||
// worth the effort.
|
||||
async fn top_level_transfer(
|
||||
provider: &RootProvider<SimpleRequest>,
|
||||
provider: &RootProvider,
|
||||
erc20: Address,
|
||||
transaction_hash: [u8; 32],
|
||||
transfer_logs: &[Log],
|
||||
@@ -112,15 +111,13 @@ impl Erc20 {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
// Don't validate the encoding as this can't be re-encoded to an identical bytestring due
|
||||
// to the additional data appended after the call itself
|
||||
let Ok(call) = IERC20Calls::abi_decode(transaction.inner.input(), false) else {
|
||||
let Ok(call) = IERC20Calls::abi_decode(transaction.inner.input()) else {
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
// Extract the top-level call's from/to/value
|
||||
let (from, to, value) = match call {
|
||||
IERC20Calls::transfer(transferCall { to, value }) => (transaction.from, to, value),
|
||||
IERC20Calls::transfer(transferCall { to, value }) => (transaction.inner.signer(), to, value),
|
||||
IERC20Calls::transferFrom(transferFromCall { from, to, value }) => (from, to, value),
|
||||
// Treat any other function selectors as unrecognized
|
||||
_ => return Ok(None),
|
||||
@@ -149,7 +146,7 @@ impl Erc20 {
|
||||
}
|
||||
|
||||
// Read the data appended after
|
||||
let data = if let Ok(call) = SeraiIERC20Calls::abi_decode(transaction.inner.input(), true) {
|
||||
let data = if let Ok(call) = SeraiIERC20Calls::abi_decode(transaction.inner.input()) {
|
||||
match call {
|
||||
SeraiIERC20Calls::transferWithInInstruction01BB244A8A(
|
||||
transferWithInInstructionCall { inInstruction, .. },
|
||||
@@ -180,7 +177,7 @@ impl Erc20 {
|
||||
///
|
||||
/// The `transfers` in the result are unordered. The `logs` are sorted by index.
|
||||
pub async fn top_level_transfers_unordered(
|
||||
provider: &RootProvider<SimpleRequest>,
|
||||
provider: &RootProvider,
|
||||
blocks: RangeInclusive<u64>,
|
||||
erc20: Address,
|
||||
to: Address,
|
||||
|
||||
@@ -12,4 +12,30 @@ fn selector_collisions() {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn abi_decode_panic() {
|
||||
use alloy_sol_types::SolInterface;
|
||||
|
||||
/*
|
||||
The following code panics with alloy-core 0.8, when the validate flag (commented out) is set to
|
||||
`false`. This flag was removed with alloy-core 1.0, leaving the default behavior of
|
||||
`abi_decode` to be `validate = false`. This test was added to ensure when we removed our
|
||||
practice of `validate = true`, we didn't open ourselves up this as a DoS risk.
|
||||
*/
|
||||
assert!(crate::SeraiIERC20Calls::abi_decode(
|
||||
&alloy_core::primitives::hex::decode(concat!(
|
||||
"a9059cbb",
|
||||
"0000000000000000000000000000000000000000000000000000000000000000",
|
||||
"0000000000000000000000000000000000000000000000000000000000000000",
|
||||
"000000000000000000000000000000000000000000000000000000000000006f",
|
||||
"ffffffffff000000000000000000000000000000000000000000000000000023",
|
||||
"000000ffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
|
||||
"ffffff0000000000000000000000000000000000000000000000000000000000",
|
||||
))
|
||||
.unwrap(),
|
||||
// false
|
||||
)
|
||||
.is_err());
|
||||
}
|
||||
|
||||
// This is primarily tested via serai-processor-ethereum-router
|
||||
|
||||
@@ -22,5 +22,5 @@ borsh = { version = "1", default-features = false, features = ["std", "derive",
|
||||
group = { version = "0.13", default-features = false }
|
||||
k256 = { version = "^0.13.1", default-features = false, features = ["std", "arithmetic"] }
|
||||
|
||||
alloy-primitives = { version = "0.8", default-features = false }
|
||||
alloy-consensus = { version = "0.9", default-features = false, features = ["k256"] }
|
||||
alloy-primitives = { version = "1", default-features = false }
|
||||
alloy-consensus = { version = "0.14", default-features = false, features = ["k256"] }
|
||||
|
||||
@@ -7,7 +7,7 @@ use ::borsh::{BorshSerialize, BorshDeserialize};
|
||||
use group::ff::PrimeField;
|
||||
use k256::Scalar;
|
||||
|
||||
use alloy_primitives::PrimitiveSignature;
|
||||
use alloy_primitives::Signature;
|
||||
use alloy_consensus::{SignableTransaction, Signed, TxLegacy};
|
||||
|
||||
mod borsh;
|
||||
@@ -68,8 +68,7 @@ pub fn deterministically_sign(tx: TxLegacy) -> Signed<TxLegacy> {
|
||||
let s = Scalar::ONE;
|
||||
let r_bytes: [u8; 32] = r.to_repr().into();
|
||||
let s_bytes: [u8; 32] = s.to_repr().into();
|
||||
let signature =
|
||||
PrimitiveSignature::from_scalars_and_parity(r_bytes.into(), s_bytes.into(), false);
|
||||
let signature = Signature::from_scalars_and_parity(r_bytes.into(), s_bytes.into(), false);
|
||||
|
||||
let res = tx.into_signed(signature);
|
||||
debug_assert!(res.recover_signer().is_ok());
|
||||
|
||||
@@ -22,19 +22,18 @@ borsh = { version = "1", default-features = false, features = ["std", "derive",
|
||||
group = { version = "0.13", default-features = false }
|
||||
k256 = { version = "0.13", default-features = false, features = ["std", "arithmetic"] }
|
||||
|
||||
alloy-core = { version = "0.8", default-features = false }
|
||||
alloy-core = { version = "1", default-features = false }
|
||||
|
||||
alloy-sol-types = { version = "0.8", default-features = false }
|
||||
alloy-sol-macro = { version = "0.8", default-features = false }
|
||||
alloy-sol-types = { version = "1", default-features = false }
|
||||
alloy-sol-macro = { version = "1", default-features = false }
|
||||
|
||||
alloy-consensus = { version = "0.9", default-features = false }
|
||||
alloy-consensus = { version = "0.14", default-features = false }
|
||||
|
||||
alloy-rpc-types-eth = { version = "0.9", default-features = false }
|
||||
alloy-transport = { version = "0.9", default-features = false }
|
||||
alloy-simple-request-transport = { path = "../../../networks/ethereum/alloy-simple-request-transport", default-features = false }
|
||||
alloy-provider = { version = "0.9", default-features = false }
|
||||
alloy-rpc-types-eth = { version = "0.14", default-features = false }
|
||||
alloy-transport = { version = "0.14", default-features = false }
|
||||
alloy-provider = { version = "0.14", default-features = false }
|
||||
|
||||
revm = { version = "19", default-features = false, features = ["std"] }
|
||||
revm = { version = "22", default-features = false, features = ["std"] }
|
||||
|
||||
ethereum-schnorr = { package = "ethereum-schnorr-contract", path = "../../../networks/ethereum/schnorr", default-features = false }
|
||||
|
||||
@@ -52,18 +51,19 @@ build-solidity-contracts = { path = "../../../networks/ethereum/build-contracts"
|
||||
|
||||
syn = { version = "2", default-features = false, features = ["proc-macro"] }
|
||||
|
||||
syn-solidity = { version = "0.8", default-features = false }
|
||||
alloy-sol-macro-input = { version = "0.8", default-features = false }
|
||||
alloy-sol-macro-expander = { version = "0.8", default-features = false }
|
||||
syn-solidity = { version = "1", default-features = false }
|
||||
alloy-sol-macro-input = { version = "1", default-features = false }
|
||||
alloy-sol-macro-expander = { version = "1", default-features = false }
|
||||
|
||||
[dev-dependencies]
|
||||
rand_core = { version = "0.6", default-features = false, features = ["std"] }
|
||||
|
||||
k256 = { version = "0.13", default-features = false, features = ["std"] }
|
||||
|
||||
alloy-provider = { version = "0.9", default-features = false, features = ["debug-api", "trace-api"] }
|
||||
alloy-rpc-client = { version = "0.9", default-features = false }
|
||||
alloy-node-bindings = { version = "0.9", default-features = false }
|
||||
alloy-simple-request-transport = { path = "../../../networks/ethereum/alloy-simple-request-transport", default-features = false }
|
||||
alloy-provider = { version = "0.14", default-features = false, features = ["debug-api", "trace-api"] }
|
||||
alloy-rpc-client = { version = "0.14", default-features = false }
|
||||
alloy-node-bindings = { version = "0.14", default-features = false }
|
||||
|
||||
tokio = { version = "1.0", default-features = false, features = ["rt-multi-thread", "macros"] }
|
||||
|
||||
|
||||
@@ -148,8 +148,9 @@ contract Router is IRouterWithoutCollisions {
|
||||
|
||||
/**
|
||||
* @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.
|
||||
* calldata should be signed with the chain ID taking the place of the signature's challenge, and
|
||||
* the signature's response replaced by the contract's address shifted into the high bits with
|
||||
* the contract's nonce as the low bits.
|
||||
*/
|
||||
/// @param key The key to verify the signature with
|
||||
function verifySignature(bytes32 key)
|
||||
@@ -185,6 +186,10 @@ contract Router is IRouterWithoutCollisions {
|
||||
// Read _nextNonce into memory as the nonce we'll use
|
||||
nonceUsed = _nextNonce;
|
||||
|
||||
// We overwrite the signature response with the Router contract's address concatenated with the
|
||||
// nonce. This is safe until the nonce exceeds 2**96, which is infeasible to do on-chain
|
||||
uint256 signatureResponseOverwrite = (uint256(uint160(address(this))) << 96) | nonceUsed;
|
||||
|
||||
// Declare memory to copy the signature out to
|
||||
bytes32 signatureC;
|
||||
bytes32 signatureS;
|
||||
@@ -198,8 +203,8 @@ contract Router is IRouterWithoutCollisions {
|
||||
|
||||
// Overwrite the signature challenge with the chain ID
|
||||
mstore(add(message, 36), chainID)
|
||||
// Overwrite the signature response with the nonce
|
||||
mstore(add(message, 68), nonceUsed)
|
||||
// Overwrite the signature response with the contract's address, nonce
|
||||
mstore(add(message, 68), signatureResponseOverwrite)
|
||||
|
||||
// Calculate the message hash
|
||||
messageHash := keccak256(add(message, 32), messageLen)
|
||||
|
||||
@@ -1,26 +1,130 @@
|
||||
use core::convert::Infallible;
|
||||
|
||||
use k256::{Scalar, ProjectivePoint};
|
||||
|
||||
use alloy_core::primitives::{Address, U160, U256};
|
||||
use alloy_core::primitives::{Address, U256, Bytes};
|
||||
use alloy_sol_types::SolCall;
|
||||
|
||||
use revm::{
|
||||
primitives::*,
|
||||
interpreter::{gas::*, opcode::InstructionTables, *},
|
||||
db::{emptydb::EmptyDB, in_memory_db::InMemoryDB},
|
||||
Handler, Context, EvmBuilder, Evm,
|
||||
primitives::hardfork::SpecId,
|
||||
bytecode::Bytecode,
|
||||
state::AccountInfo,
|
||||
database::{empty_db::EmptyDB, in_memory_db::InMemoryDB},
|
||||
interpreter::{
|
||||
gas::calculate_initial_tx_gas,
|
||||
interpreter_action::{CallInputs, CallOutcome},
|
||||
interpreter::EthInterpreter,
|
||||
Interpreter,
|
||||
},
|
||||
handler::{
|
||||
instructions::EthInstructions, PrecompileProvider, EthPrecompiles, EthFrame, MainnetHandler,
|
||||
},
|
||||
context::{
|
||||
result::{EVMError, InvalidTransaction, ExecutionResult},
|
||||
evm::{EvmData, Evm},
|
||||
context::Context,
|
||||
*,
|
||||
},
|
||||
inspector::{Inspector, InspectorHandler},
|
||||
};
|
||||
|
||||
use ethereum_schnorr::{PublicKey, Signature};
|
||||
|
||||
use crate::*;
|
||||
|
||||
// The specification this uses
|
||||
const SPEC_ID: SpecId = SpecId::CANCUN;
|
||||
|
||||
// The chain ID used for gas estimation
|
||||
const CHAIN_ID: U256 = U256::from_be_slice(&[1]);
|
||||
|
||||
type RevmContext = Context<BlockEnv, TxEnv, CfgEnv, InMemoryDB, Journal<InMemoryDB>, ()>;
|
||||
|
||||
fn precompiles() -> EthPrecompiles {
|
||||
let mut precompiles = EthPrecompiles::default();
|
||||
PrecompileProvider::<RevmContext>::set_spec(&mut precompiles, SPEC_ID);
|
||||
precompiles
|
||||
}
|
||||
|
||||
/*
|
||||
Instead of attempting to solve the halting problem, we assume all CALLs take the worst-case
|
||||
amount of gas (as we do have bounds on the gas they're allowed to take). This assumption is
|
||||
implemented via an revm Inspector.
|
||||
|
||||
The Inspector is allowed to override the CALL directly. We don't do this due to the amount of
|
||||
side effects a CALL has. Instead, we override the result.
|
||||
|
||||
In the case the ERC20 is called, we additionally have it return `true` (as expected for compliant
|
||||
ERC20s, and as will trigger the worst-case gas consumption by the Router itself). This is done by
|
||||
hooking `call_end`.
|
||||
*/
|
||||
pub(crate) struct WorstCaseCallInspector {
|
||||
erc20: Option<Address>,
|
||||
call_depth: usize,
|
||||
unused_gas: u64,
|
||||
override_immediate_call_return_value: bool,
|
||||
}
|
||||
impl Inspector<RevmContext> for WorstCaseCallInspector {
|
||||
fn call(&mut self, _context: &mut RevmContext, _inputs: &mut CallInputs) -> Option<CallOutcome> {
|
||||
self.call_depth += 1;
|
||||
// Don't override the CALL immediately for prior described reasons
|
||||
None
|
||||
}
|
||||
|
||||
fn call_end(
|
||||
&mut self,
|
||||
_context: &mut RevmContext,
|
||||
inputs: &CallInputs,
|
||||
outcome: &mut CallOutcome,
|
||||
) {
|
||||
self.call_depth -= 1;
|
||||
|
||||
/*
|
||||
Mark the amount of gas left unused, for us to later assume will be used in practice.
|
||||
|
||||
This only runs if the call-depth is 1 (so only the Router-made calls have their gas so
|
||||
tracked), and if it's not to a precompile. This latter condition isn't solely because we can
|
||||
perfectly model precompiles (which wouldn't be worth the complexity) yet because the Router
|
||||
does call precompiles (ecrecover) and accordingly has to model the gas of that correctly.
|
||||
*/
|
||||
if (self.call_depth == 1) && (!precompiles().contains(&inputs.target_address)) {
|
||||
let unused_gas = inputs.gas_limit - outcome.result.gas.spent();
|
||||
self.unused_gas += unused_gas;
|
||||
|
||||
// Now that the CALL is over, flag we should normalize the values on the stack
|
||||
self.override_immediate_call_return_value = true;
|
||||
}
|
||||
|
||||
// If ERC20, provide the expected ERC20 return data
|
||||
if Some(inputs.target_address) == self.erc20 {
|
||||
outcome.result.output = true.abi_encode().into();
|
||||
}
|
||||
}
|
||||
|
||||
fn step(&mut self, interpreter: &mut Interpreter, _context: &mut RevmContext) {
|
||||
if self.override_immediate_call_return_value {
|
||||
// We fix this result to having succeeded, which triggers the most-expensive pathing within
|
||||
// the Router contract itself (some paths return early if a CALL fails)
|
||||
let return_value = interpreter.stack.pop().unwrap();
|
||||
assert!((return_value == U256::ZERO) || (return_value == U256::ONE));
|
||||
assert!(
|
||||
interpreter.stack.push(U256::ONE),
|
||||
"stack capacity couldn't fit item after popping an item"
|
||||
);
|
||||
self.override_immediate_call_return_value = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The object used for estimating gas.
|
||||
///
|
||||
/// Due to `execute` heavily branching, we locally simulate calls with revm.
|
||||
pub(crate) type GasEstimator = Evm<'static, (), InMemoryDB>;
|
||||
pub(crate) type GasEstimator = Evm<
|
||||
RevmContext,
|
||||
WorstCaseCallInspector,
|
||||
EthInstructions<EthInterpreter, RevmContext>,
|
||||
EthPrecompiles,
|
||||
>;
|
||||
|
||||
impl Router {
|
||||
const SMART_CONTRACT_NONCE_STORAGE_SLOT: U256 = U256::from_be_slice(&[0]);
|
||||
@@ -47,11 +151,11 @@ impl Router {
|
||||
the correct set of prices for the network they're operating on.
|
||||
*/
|
||||
/// The gas used by `confirmSeraiKey`.
|
||||
pub const CONFIRM_NEXT_SERAI_KEY_GAS: u64 = 57_736;
|
||||
pub const CONFIRM_NEXT_SERAI_KEY_GAS: u64 = 57_753;
|
||||
/// The gas used by `updateSeraiKey`.
|
||||
pub const UPDATE_SERAI_KEY_GAS: u64 = 60_045;
|
||||
pub const UPDATE_SERAI_KEY_GAS: u64 = 60_062;
|
||||
/// The gas used by `escapeHatch`.
|
||||
pub const ESCAPE_HATCH_GAS: u64 = 61_094;
|
||||
pub const ESCAPE_HATCH_GAS: u64 = 61_111;
|
||||
|
||||
/// The key to use when performing gas estimations.
|
||||
///
|
||||
@@ -114,120 +218,35 @@ impl Router {
|
||||
db
|
||||
};
|
||||
|
||||
// Create a custom handler so we can assume every CALL is the worst-case
|
||||
let handler = {
|
||||
let mut instructions = InstructionTables::<'_, _>::new_plain::<CancunSpec>();
|
||||
instructions.update_boxed(revm::interpreter::opcode::CALL, {
|
||||
move |call_op, interpreter, host: &mut Context<_, _>| {
|
||||
let (address_called, value, return_addr, return_len) = {
|
||||
let stack = &mut interpreter.stack;
|
||||
|
||||
let address = stack.peek(1).unwrap();
|
||||
let value = stack.peek(2).unwrap();
|
||||
let return_addr = stack.peek(5).unwrap();
|
||||
let return_len = stack.peek(6).unwrap();
|
||||
|
||||
(
|
||||
address,
|
||||
value,
|
||||
usize::try_from(return_addr).unwrap(),
|
||||
usize::try_from(return_len).unwrap(),
|
||||
)
|
||||
};
|
||||
let address_called =
|
||||
Address::from(U160::from_be_slice(&address_called.to_be_bytes::<32>()[12 ..]));
|
||||
|
||||
// Have the original call op incur its costs as programmed
|
||||
call_op(interpreter, host);
|
||||
|
||||
/*
|
||||
Unfortunately, the call opcode executed only sets itself up, it doesn't handle the
|
||||
entire inner call for us. We manually do so here by shimming the intended result. The
|
||||
other option, on this path chosen, would be to shim the call-frame execution ourselves
|
||||
and only then manipulate the result.
|
||||
|
||||
Ideally, we wouldn't override CALL, yet STOP/RETURN (the tail of the CALL) to avoid all
|
||||
of this. Those overrides weren't being successfully hit in initial experiments, and
|
||||
while this solution does appear overly complicated, it's sufficiently tested to justify
|
||||
itself.
|
||||
|
||||
revm does cost the entire gas limit during the call setup. After the call completes,
|
||||
it refunds whatever was unused. Since we manually complete the call here ourselves,
|
||||
but don't implement that refund logic as we want the worst-case scenario, we do
|
||||
successfully implement complete costing of the gas limit.
|
||||
*/
|
||||
|
||||
// Perform the call value transfer, which also marks the recipient as warm
|
||||
assert!(host
|
||||
.evm
|
||||
.inner
|
||||
.journaled_state
|
||||
.transfer(
|
||||
&interpreter.contract.target_address,
|
||||
&address_called,
|
||||
value,
|
||||
&mut host.evm.inner.db
|
||||
)
|
||||
.unwrap()
|
||||
.is_none());
|
||||
|
||||
// Clear the call-to-be
|
||||
debug_assert!(matches!(interpreter.next_action, InterpreterAction::Call { .. }));
|
||||
interpreter.next_action = InterpreterAction::None;
|
||||
interpreter.instruction_result = InstructionResult::Continue;
|
||||
|
||||
// Clear the existing return data
|
||||
interpreter.return_data_buffer.clear();
|
||||
|
||||
/*
|
||||
If calling an ERC20, trigger the return data's worst-case by returning `true`
|
||||
(as expected by compliant ERC20s). Else return none, as we expect none or won't bother
|
||||
copying/decoding the return data.
|
||||
|
||||
This doesn't affect calls to ecrecover as those use STATICCALL and this overrides CALL
|
||||
alone.
|
||||
*/
|
||||
if Some(address_called) == erc20 {
|
||||
interpreter.return_data_buffer = true.abi_encode().into();
|
||||
}
|
||||
// Also copy the return data into memory
|
||||
let return_len = return_len.min(interpreter.return_data_buffer.len());
|
||||
let needed_memory_size = return_addr + return_len;
|
||||
if interpreter.shared_memory.len() < needed_memory_size {
|
||||
assert!(interpreter.resize_memory(needed_memory_size));
|
||||
}
|
||||
interpreter
|
||||
.shared_memory
|
||||
.slice_mut(return_addr, return_len)
|
||||
.copy_from_slice(&interpreter.return_data_buffer[.. return_len]);
|
||||
|
||||
// Finally, push the result of the call onto the stack
|
||||
interpreter.stack.push(U256::from(1)).unwrap();
|
||||
}
|
||||
});
|
||||
let mut handler = Handler::mainnet::<CancunSpec>();
|
||||
handler.set_instruction_table(instructions);
|
||||
|
||||
handler
|
||||
};
|
||||
|
||||
EvmBuilder::default()
|
||||
.with_db(db)
|
||||
.with_handler(handler)
|
||||
.modify_cfg_env(|cfg| {
|
||||
cfg.chain_id = CHAIN_ID.try_into().unwrap();
|
||||
})
|
||||
.modify_tx_env(|tx| {
|
||||
tx.gas_limit = u64::MAX;
|
||||
tx.transact_to = self.address.into();
|
||||
})
|
||||
.build()
|
||||
Evm {
|
||||
data: EvmData {
|
||||
ctx: RevmContext::new(db, SPEC_ID)
|
||||
.modify_cfg_chained(|cfg| {
|
||||
cfg.chain_id = CHAIN_ID.try_into().unwrap();
|
||||
})
|
||||
.modify_tx_chained(|tx: &mut TxEnv| {
|
||||
tx.gas_limit = u64::MAX;
|
||||
tx.kind = self.address.into();
|
||||
}),
|
||||
inspector: WorstCaseCallInspector {
|
||||
erc20,
|
||||
call_depth: 0,
|
||||
unused_gas: 0,
|
||||
override_immediate_call_return_value: false,
|
||||
},
|
||||
},
|
||||
instruction: EthInstructions::default(),
|
||||
precompiles: precompiles(),
|
||||
}
|
||||
}
|
||||
|
||||
/// The worst-case gas cost for a legacy transaction which executes this batch.
|
||||
///
|
||||
/// This assumes the fee will be non-zero.
|
||||
pub fn execute_gas(&self, coin: Coin, fee_per_gas: U256, outs: &OutInstructions) -> u64 {
|
||||
pub fn execute_gas_and_fee(
|
||||
&self,
|
||||
coin: Coin,
|
||||
fee_per_gas: U256,
|
||||
outs: &OutInstructions,
|
||||
) -> (u64, U256) {
|
||||
// Unfortunately, we can't cache this in self, despite the following code being written such
|
||||
// that a common EVM instance could be used, as revm's types aren't Send/Sync and we expect the
|
||||
// Router to be send/sync
|
||||
@@ -236,17 +255,17 @@ impl Router {
|
||||
Coin::Erc20(erc20) => Some(erc20),
|
||||
});
|
||||
|
||||
let fee = match coin {
|
||||
let shimmed_fee = match coin {
|
||||
Coin::Ether => {
|
||||
// Use a fee of 1 so the fee payment is recognized as positive-value
|
||||
let fee = U256::from(1);
|
||||
// Use a fee of 1 so the fee payment is recognized as positive-value, if the fee is
|
||||
// non-zero
|
||||
let fee = if fee_per_gas == U256::ZERO { U256::ZERO } else { U256::ONE };
|
||||
|
||||
// Set a balance of the amount sent out to ensure we don't error on that premise
|
||||
{
|
||||
let db = gas_estimator.db_mut();
|
||||
gas_estimator.data.ctx.modify_db(|db| {
|
||||
let account = db.load_account(self.address).unwrap();
|
||||
account.info.balance = fee + outs.0.iter().map(|out| out.amount).sum::<U256>();
|
||||
}
|
||||
});
|
||||
|
||||
fee
|
||||
}
|
||||
@@ -259,7 +278,7 @@ impl Router {
|
||||
// Use a nonce of 1
|
||||
ProjectivePoint::GENERATOR,
|
||||
&public_key,
|
||||
&Self::execute_message(CHAIN_ID, 1, coin, fee, outs.clone()),
|
||||
&Self::execute_message(CHAIN_ID, self.address, 1, coin, shimmed_fee, outs.clone()),
|
||||
);
|
||||
let s = Scalar::ONE + (c * private_key);
|
||||
let sig = Signature::new(c, s).unwrap();
|
||||
@@ -271,8 +290,7 @@ impl Router {
|
||||
consistent use of nonce #1 shows storage read/writes aren't being persisted. They're solely
|
||||
returned upon execution in a `state` field we ignore.
|
||||
*/
|
||||
{
|
||||
let tx = gas_estimator.tx_mut();
|
||||
gas_estimator.data.ctx.modify_tx(|tx| {
|
||||
tx.caller = Address::from({
|
||||
/*
|
||||
We assume the transaction sender is not the destination of any `OutInstruction`, making
|
||||
@@ -291,55 +309,82 @@ impl Router {
|
||||
tx.data = abi::executeCall::new((
|
||||
abi::Signature::from(&sig),
|
||||
Address::from(coin),
|
||||
fee,
|
||||
shimmed_fee,
|
||||
outs.0.clone(),
|
||||
))
|
||||
.abi_encode()
|
||||
.into();
|
||||
}
|
||||
});
|
||||
|
||||
// Execute the transaction
|
||||
let mut gas = match gas_estimator.transact().unwrap().result {
|
||||
let mut gas = match MainnetHandler::<
|
||||
_,
|
||||
EVMError<Infallible, InvalidTransaction>,
|
||||
EthFrame<_, _, _>,
|
||||
>::default()
|
||||
.inspect_run(&mut gas_estimator)
|
||||
.unwrap()
|
||||
.result
|
||||
{
|
||||
ExecutionResult::Success { gas_used, gas_refunded, .. } => {
|
||||
assert_eq!(gas_refunded, 0);
|
||||
gas_used
|
||||
}
|
||||
res => panic!("estimated execute transaction failed: {res:?}"),
|
||||
};
|
||||
gas += gas_estimator.into_inspector().unused_gas;
|
||||
|
||||
// The transaction uses gas based on the amount of non-zero bytes in the calldata, which is
|
||||
// variable to the fee, which is variable to the gad used. This iterates until parity
|
||||
/*
|
||||
The transaction pays an initial gas fee which is dependent on the length of the calldata and
|
||||
the amount of non-zero bytes in the calldata. This is variable to the fee, which was prior
|
||||
shimmed to be `1`.
|
||||
|
||||
Here, we calculate the actual fee, and update the initial gas fee accordingly. We then update
|
||||
the fee again, until the initial gas fee stops increasing.
|
||||
*/
|
||||
let initial_gas = |fee, sig| {
|
||||
let gas = calculate_initial_tx_gas(
|
||||
SpecId::CANCUN,
|
||||
SPEC_ID,
|
||||
&abi::executeCall::new((sig, Address::from(coin), fee, outs.0.clone())).abi_encode(),
|
||||
false,
|
||||
&[],
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
);
|
||||
assert_eq!(gas.floor_gas, 0);
|
||||
gas.initial_gas
|
||||
};
|
||||
let mut current_initial_gas = initial_gas(fee, abi::Signature::from(&sig));
|
||||
let mut current_initial_gas = initial_gas(shimmed_fee, abi::Signature::from(&sig));
|
||||
// Remove the current initial gas from the transaction's gas
|
||||
gas -= current_initial_gas;
|
||||
loop {
|
||||
let fee = fee_per_gas * U256::from(gas);
|
||||
// Calculate the would-be fee
|
||||
let fee = fee_per_gas * U256::from(gas + current_initial_gas);
|
||||
// Calculate the would-be gas for this fee
|
||||
let new_initial_gas =
|
||||
initial_gas(fee, abi::Signature { c: [0xff; 32].into(), s: [0xff; 32].into() });
|
||||
// If the values are equal, or if it went down, return
|
||||
/*
|
||||
The gas will decrease if the new fee has more zero bytes in its encoding. Further
|
||||
iterations are unhelpful as they'll simply loop infinitely for some inputs. Accordingly, we
|
||||
return the current fee (which is for a very slightly higher gas rate) with the decreased
|
||||
gas to ensure this algorithm terminates.
|
||||
*/
|
||||
if current_initial_gas >= new_initial_gas {
|
||||
return gas;
|
||||
return (gas + new_initial_gas, fee);
|
||||
}
|
||||
|
||||
gas += new_initial_gas - current_initial_gas;
|
||||
// Update what the current initial gas is
|
||||
current_initial_gas = new_initial_gas;
|
||||
}
|
||||
}
|
||||
|
||||
/// The estimated fee for this `OutInstruction`.
|
||||
/// The estimated gas for this `OutInstruction`.
|
||||
///
|
||||
/// This does not model the quadratic costs incurred when in a batch, nor other misc costs such
|
||||
/// as the potential to cause one less zero byte in the fee's encoding. This is intended to
|
||||
/// produce a per-`OutInstruction` fee to deduct from each `OutInstruction`, before all
|
||||
/// `OutInstruction`s incur an amortized fee of what remains for the batch itself.
|
||||
/// produce a per-`OutInstruction` value which can be ratioed against others to decide the fee to
|
||||
/// deduct from each `OutInstruction`, before all `OutInstruction`s incur an amortized fee of
|
||||
/// what remains for the batch itself.
|
||||
pub fn execute_out_instruction_gas_estimate(
|
||||
&mut self,
|
||||
coin: Coin,
|
||||
@@ -348,11 +393,12 @@ impl Router {
|
||||
#[allow(clippy::map_entry)] // clippy doesn't realize the multiple mutable borrows
|
||||
if !self.empty_execute_gas.contains_key(&coin) {
|
||||
// This can't be de-duplicated across ERC20s due to the zero bytes in the address
|
||||
let gas = self.execute_gas(coin, U256::from(0), &OutInstructions(vec![]));
|
||||
let (gas, _fee) = self.execute_gas_and_fee(coin, U256::from(0), &OutInstructions(vec![]));
|
||||
self.empty_execute_gas.insert(coin, gas);
|
||||
}
|
||||
|
||||
let gas = self.execute_gas(coin, U256::from(0), &OutInstructions(vec![instruction]));
|
||||
let (gas, _fee) =
|
||||
self.execute_gas_and_fee(coin, U256::from(0), &OutInstructions(vec![instruction]));
|
||||
gas - self.empty_execute_gas[&coin]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,7 +19,6 @@ use alloy_consensus::TxLegacy;
|
||||
|
||||
use alloy_rpc_types_eth::{BlockId, Log, Filter, TransactionInput, TransactionRequest};
|
||||
use alloy_transport::{TransportErrorKind, RpcError};
|
||||
use alloy_simple_request_transport::SimpleRequest;
|
||||
use alloy_provider::{Provider, RootProvider};
|
||||
|
||||
use scale::Encode;
|
||||
@@ -48,6 +47,7 @@ mod _irouter_abi {
|
||||
#[expect(warnings)]
|
||||
#[expect(needless_pass_by_value)]
|
||||
#[expect(clippy::all)]
|
||||
#[expect(clippy::unused_self)]
|
||||
#[expect(clippy::ignored_unit_patterns)]
|
||||
#[expect(clippy::redundant_closure_for_method_calls)]
|
||||
mod _router_abi {
|
||||
@@ -236,7 +236,7 @@ pub struct Escape {
|
||||
/// A view of the Router for Serai.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Router {
|
||||
provider: Arc<RootProvider<SimpleRequest>>,
|
||||
provider: Arc<RootProvider>,
|
||||
address: Address,
|
||||
empty_execute_gas: HashMap<Coin, u64>,
|
||||
}
|
||||
@@ -272,7 +272,7 @@ impl Router {
|
||||
/// This performs an on-chain lookup for the first deployed Router constructed with this public
|
||||
/// key. This lookup is of a constant amount of calls and does not read any logs.
|
||||
pub async fn new(
|
||||
provider: Arc<RootProvider<SimpleRequest>>,
|
||||
provider: Arc<RootProvider>,
|
||||
initial_serai_key: &PublicKey,
|
||||
) -> Result<Option<Self>, RpcError<TransportErrorKind>> {
|
||||
let Some(deployer) = Deployer::new(provider.clone()).await? else {
|
||||
@@ -292,13 +292,22 @@ impl Router {
|
||||
self.address
|
||||
}
|
||||
|
||||
/// Get the signature data signed in place of the actual signature.
|
||||
fn signature_data(chain_id: U256, router_address: Address, nonce: u64) -> abi::Signature {
|
||||
let mut s = [0; 32];
|
||||
s[.. 20].copy_from_slice(router_address.as_slice());
|
||||
s[24 ..].copy_from_slice(&nonce.to_be_bytes());
|
||||
abi::Signature { c: chain_id.into(), s: s.into() }
|
||||
}
|
||||
|
||||
/// Get the message to be signed in order to confirm the next key for Serai.
|
||||
pub fn confirm_next_serai_key_message(chain_id: U256, nonce: u64) -> Vec<u8> {
|
||||
abi::confirmNextSeraiKeyCall::new((abi::Signature {
|
||||
c: chain_id.into(),
|
||||
s: U256::try_from(nonce).unwrap().into(),
|
||||
},))
|
||||
.abi_encode()
|
||||
pub fn confirm_next_serai_key_message(
|
||||
chain_id: U256,
|
||||
router_address: Address,
|
||||
nonce: u64,
|
||||
) -> Vec<u8> {
|
||||
abi::confirmNextSeraiKeyCall::new((Self::signature_data(chain_id, router_address, nonce),))
|
||||
.abi_encode()
|
||||
}
|
||||
|
||||
/// Construct a transaction to confirm the next key representing Serai.
|
||||
@@ -313,9 +322,14 @@ impl Router {
|
||||
}
|
||||
|
||||
/// Get the message to be signed in order to update the key for Serai.
|
||||
pub fn update_serai_key_message(chain_id: U256, nonce: u64, key: &PublicKey) -> Vec<u8> {
|
||||
pub fn update_serai_key_message(
|
||||
chain_id: U256,
|
||||
router_address: Address,
|
||||
nonce: u64,
|
||||
key: &PublicKey,
|
||||
) -> Vec<u8> {
|
||||
abi::updateSeraiKeyCall::new((
|
||||
abi::Signature { c: chain_id.into(), s: U256::try_from(nonce).unwrap().into() },
|
||||
Self::signature_data(chain_id, router_address, nonce),
|
||||
key.eth_repr().into(),
|
||||
))
|
||||
.abi_encode()
|
||||
@@ -371,13 +385,14 @@ impl Router {
|
||||
/// Get the message to be signed in order to execute a series of `OutInstruction`s.
|
||||
pub fn execute_message(
|
||||
chain_id: U256,
|
||||
router_address: Address,
|
||||
nonce: u64,
|
||||
coin: Coin,
|
||||
fee: U256,
|
||||
outs: OutInstructions,
|
||||
) -> Vec<u8> {
|
||||
abi::executeCall::new((
|
||||
abi::Signature { c: chain_id.into(), s: U256::try_from(nonce).unwrap().into() },
|
||||
Self::signature_data(chain_id, router_address, nonce),
|
||||
Address::from(coin),
|
||||
fee,
|
||||
outs.0,
|
||||
@@ -399,12 +414,14 @@ impl Router {
|
||||
}
|
||||
|
||||
/// Get the message to be signed in order to trigger the escape hatch.
|
||||
pub fn escape_hatch_message(chain_id: U256, nonce: u64, escape_to: Address) -> Vec<u8> {
|
||||
abi::escapeHatchCall::new((
|
||||
abi::Signature { c: chain_id.into(), s: U256::try_from(nonce).unwrap().into() },
|
||||
escape_to,
|
||||
))
|
||||
.abi_encode()
|
||||
pub fn escape_hatch_message(
|
||||
chain_id: U256,
|
||||
router_address: Address,
|
||||
nonce: u64,
|
||||
escape_to: Address,
|
||||
) -> Vec<u8> {
|
||||
abi::escapeHatchCall::new((Self::signature_data(chain_id, router_address, nonce), escape_to))
|
||||
.abi_encode()
|
||||
}
|
||||
|
||||
/// Construct a transaction to trigger the escape hatch.
|
||||
@@ -573,7 +590,7 @@ impl Router {
|
||||
if log.topics().first() != Some(&Transfer::SIGNATURE_HASH) {
|
||||
continue;
|
||||
}
|
||||
let Ok(transfer) = Transfer::decode_log(&log.inner.clone(), true) else { continue };
|
||||
let Ok(transfer) = Transfer::decode_log(&log.inner.clone()) else { continue };
|
||||
// Check if this aligns with the InInstruction
|
||||
if (transfer.from == in_instruction.from) &&
|
||||
(transfer.to == self.address) &&
|
||||
@@ -743,11 +760,11 @@ impl Router {
|
||||
) -> Result<Option<PublicKey>, RpcError<TransportErrorKind>> {
|
||||
let call =
|
||||
TransactionRequest::default().to(self.address).input(TransactionInput::new(call.into()));
|
||||
let bytes = self.provider.call(&call).block(block).await?;
|
||||
let bytes = self.provider.call(call).block(block).await?;
|
||||
// This is fine as both key calls share a return type
|
||||
let res = abi::nextSeraiKeyCall::abi_decode_returns(&bytes, true)
|
||||
let res = abi::nextSeraiKeyCall::abi_decode_returns(&bytes)
|
||||
.map_err(|e| TransportErrorKind::Custom(format!("failed to decode key: {e:?}").into()))?;
|
||||
let eth_repr = <[u8; 32]>::from(res._0);
|
||||
let eth_repr = <[u8; 32]>::from(res);
|
||||
Ok(if eth_repr == [0; 32] {
|
||||
None
|
||||
} else {
|
||||
@@ -778,10 +795,10 @@ impl Router {
|
||||
let call = TransactionRequest::default()
|
||||
.to(self.address)
|
||||
.input(TransactionInput::new(abi::nextNonceCall::new(()).abi_encode().into()));
|
||||
let bytes = self.provider.call(&call).block(block).await?;
|
||||
let res = abi::nextNonceCall::abi_decode_returns(&bytes, true)
|
||||
let bytes = self.provider.call(call).block(block).await?;
|
||||
let res = abi::nextNonceCall::abi_decode_returns(&bytes)
|
||||
.map_err(|e| TransportErrorKind::Custom(format!("failed to decode nonce: {e:?}").into()))?;
|
||||
Ok(u64::try_from(res._0).map_err(|_| {
|
||||
Ok(u64::try_from(res).map_err(|_| {
|
||||
TransportErrorKind::Custom("nonce returned exceeded 2**64".to_string().into())
|
||||
})?)
|
||||
}
|
||||
@@ -794,10 +811,10 @@ impl Router {
|
||||
let call = TransactionRequest::default()
|
||||
.to(self.address)
|
||||
.input(TransactionInput::new(abi::escapedToCall::new(()).abi_encode().into()));
|
||||
let bytes = self.provider.call(&call).block(block).await?;
|
||||
let res = abi::escapedToCall::abi_decode_returns(&bytes, true).map_err(|e| {
|
||||
let bytes = self.provider.call(call).block(block).await?;
|
||||
let res = abi::escapedToCall::abi_decode_returns(&bytes).map_err(|e| {
|
||||
TransportErrorKind::Custom(format!("failed to decode the address escaped to: {e:?}").into())
|
||||
})?;
|
||||
Ok(if res._0 == Address([0; 20].into()) { None } else { Some(res._0) })
|
||||
Ok(if res == Address::ZERO { None } else { Some(res) })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ use alloy_consensus::TxLegacy;
|
||||
use alloy_rpc_types_eth::{TransactionInput, TransactionRequest};
|
||||
use alloy_provider::Provider;
|
||||
|
||||
use revm::{primitives::SpecId, interpreter::gas::calculate_initial_tx_gas};
|
||||
use revm::{primitives::hardfork::SpecId, interpreter::gas::calculate_initial_tx_gas};
|
||||
|
||||
use crate::tests::Test;
|
||||
|
||||
@@ -65,13 +65,13 @@ async fn test_create_address() {
|
||||
let call =
|
||||
TransactionRequest::default().to(address).input(TransactionInput::new(input.clone().into()));
|
||||
assert_eq!(
|
||||
&test.provider.call(&call).await.unwrap().as_ref()[12 ..],
|
||||
&test.provider.call(call.clone()).await.unwrap().as_ref()[12 ..],
|
||||
address.create(nonce).as_slice(),
|
||||
);
|
||||
|
||||
// Check the function is constant-gas
|
||||
let gas_used = test.provider.estimate_gas(&call).await.unwrap();
|
||||
let initial_gas = calculate_initial_tx_gas(SpecId::CANCUN, &input, false, &[], 0).initial_gas;
|
||||
let gas_used = test.provider.estimate_gas(call).await.unwrap();
|
||||
let initial_gas = calculate_initial_tx_gas(SpecId::CANCUN, &input, false, 0, 0, 0).initial_gas;
|
||||
let this_call = gas_used - initial_gas;
|
||||
if gas.is_none() {
|
||||
gas = Some(this_call);
|
||||
|
||||
@@ -86,13 +86,13 @@ impl Erc20 {
|
||||
let call = TransactionRequest::default().to(self.0).input(TransactionInput::new(
|
||||
abi::TestERC20::balanceOfCall::new((account,)).abi_encode().into(),
|
||||
));
|
||||
U256::abi_decode(&test.provider.call(&call).await.unwrap(), true).unwrap()
|
||||
U256::abi_decode(&test.provider.call(call).await.unwrap()).unwrap()
|
||||
}
|
||||
|
||||
pub(crate) async fn router_approval(&self, test: &Test, account: Address) -> U256 {
|
||||
let call = TransactionRequest::default().to(self.0).input(TransactionInput::new(
|
||||
abi::TestERC20::allowanceCall::new((test.router.address(), account)).abi_encode().into(),
|
||||
));
|
||||
U256::abi_decode(&test.provider.call(&call).await.unwrap(), true).unwrap()
|
||||
U256::abi_decode(&test.provider.call(call).await.unwrap()).unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,12 @@ use crate::tests::*;
|
||||
|
||||
impl Test {
|
||||
pub(crate) fn escape_hatch_tx(&self, escape_to: Address) -> TxLegacy {
|
||||
let msg = Router::escape_hatch_message(self.chain_id, self.state.next_nonce, escape_to);
|
||||
let msg = Router::escape_hatch_message(
|
||||
self.chain_id,
|
||||
self.router.address(),
|
||||
self.state.next_nonce,
|
||||
escape_to,
|
||||
);
|
||||
let sig = sign(self.state.key.unwrap(), &msg);
|
||||
let mut tx = self.router.escape_hatch(escape_to, &sig);
|
||||
tx.gas_limit = Router::ESCAPE_HATCH_GAS + 5_000;
|
||||
|
||||
@@ -63,7 +63,7 @@ struct CalldataAgnosticGas;
|
||||
impl CalldataAgnosticGas {
|
||||
#[must_use]
|
||||
fn calculate(input: &[u8], mut constant_zero_bytes: usize, gas_used: u64) -> u64 {
|
||||
use revm::{primitives::SpecId, interpreter::gas::calculate_initial_tx_gas};
|
||||
use revm::{primitives::hardfork::SpecId, interpreter::gas::calculate_initial_tx_gas};
|
||||
|
||||
let mut without_variable_zero_bytes = Vec::with_capacity(input.len());
|
||||
for byte in input {
|
||||
@@ -76,9 +76,9 @@ impl CalldataAgnosticGas {
|
||||
}
|
||||
}
|
||||
gas_used +
|
||||
(calculate_initial_tx_gas(SpecId::CANCUN, &without_variable_zero_bytes, false, &[], 0)
|
||||
(calculate_initial_tx_gas(SpecId::CANCUN, &without_variable_zero_bytes, false, 0, 0, 0)
|
||||
.initial_gas -
|
||||
calculate_initial_tx_gas(SpecId::CANCUN, input, false, &[], 0).initial_gas)
|
||||
calculate_initial_tx_gas(SpecId::CANCUN, input, false, 0, 0, 0).initial_gas)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -92,7 +92,7 @@ struct RouterState {
|
||||
struct Test {
|
||||
#[allow(unused)]
|
||||
anvil: AnvilInstance,
|
||||
provider: Arc<RootProvider<SimpleRequest>>,
|
||||
provider: Arc<RootProvider>,
|
||||
chain_id: U256,
|
||||
router: Router,
|
||||
state: RouterState,
|
||||
@@ -173,12 +173,16 @@ impl Test {
|
||||
let call = TransactionRequest::default()
|
||||
.to(self.router.address())
|
||||
.input(TransactionInput::new(tx.input));
|
||||
let call_err = self.provider.call(&call).await.unwrap_err();
|
||||
call_err.as_error_resp().unwrap().as_decoded_error::<IRouterErrors>(true).unwrap()
|
||||
let call_err = self.provider.call(call).await.unwrap_err();
|
||||
call_err.as_error_resp().unwrap().as_decoded_interface_error::<IRouterErrors>().unwrap()
|
||||
}
|
||||
|
||||
fn confirm_next_serai_key_tx(&self) -> TxLegacy {
|
||||
let msg = Router::confirm_next_serai_key_message(self.chain_id, self.state.next_nonce);
|
||||
let msg = Router::confirm_next_serai_key_message(
|
||||
self.chain_id,
|
||||
self.router.address(),
|
||||
self.state.next_nonce,
|
||||
);
|
||||
let sig = sign(self.state.next_key.unwrap(), &msg);
|
||||
|
||||
self.router.confirm_next_serai_key(&sig)
|
||||
@@ -227,7 +231,12 @@ impl Test {
|
||||
fn update_serai_key_tx(&self) -> ((Scalar, PublicKey), TxLegacy) {
|
||||
let next_key = test_key();
|
||||
|
||||
let msg = Router::update_serai_key_message(self.chain_id, self.state.next_nonce, &next_key.1);
|
||||
let msg = Router::update_serai_key_message(
|
||||
self.chain_id,
|
||||
self.router.address(),
|
||||
self.state.next_nonce,
|
||||
&next_key.1,
|
||||
);
|
||||
let sig = sign(self.state.key.unwrap(), &msg);
|
||||
|
||||
(next_key, self.router.update_serai_key(&next_key.1, &sig))
|
||||
@@ -275,6 +284,7 @@ impl Test {
|
||||
) -> ([u8; 32], TxLegacy) {
|
||||
let msg = Router::execute_message(
|
||||
self.chain_id,
|
||||
self.router.address(),
|
||||
self.state.next_nonce,
|
||||
coin,
|
||||
fee,
|
||||
@@ -468,11 +478,10 @@ async fn test_update_serai_key() {
|
||||
|
||||
// But we shouldn't be able to update the key to None
|
||||
{
|
||||
let router_address_u256: U256 = test.router.address().into_word().into();
|
||||
let s: U256 = (router_address_u256 << 96) | U256::from(test.state.next_nonce);
|
||||
let msg = crate::abi::updateSeraiKeyCall::new((
|
||||
crate::abi::Signature {
|
||||
c: test.chain_id.into(),
|
||||
s: U256::try_from(test.state.next_nonce).unwrap().into(),
|
||||
},
|
||||
crate::abi::Signature { c: test.chain_id.into(), s: s.into() },
|
||||
[0; 32].into(),
|
||||
))
|
||||
.abi_encode();
|
||||
@@ -540,8 +549,8 @@ async fn test_empty_execute() {
|
||||
test.confirm_next_serai_key().await;
|
||||
|
||||
{
|
||||
let gas = test.router.execute_gas(Coin::Ether, U256::from(1), &[].as_slice().into());
|
||||
let fee = U256::from(gas);
|
||||
let (gas, fee) =
|
||||
test.router.execute_gas_and_fee(Coin::Ether, U256::from(1), &[].as_slice().into());
|
||||
|
||||
let () = test
|
||||
.provider
|
||||
@@ -574,15 +583,15 @@ async fn test_empty_execute() {
|
||||
TransactionRequest::default().to(token).input(TransactionInput::new(vec![].into()));
|
||||
// Check it returns the expected result
|
||||
assert_eq!(
|
||||
test.provider.call(&call).await.unwrap().as_ref(),
|
||||
test.provider.call(call.clone()).await.unwrap().as_ref(),
|
||||
U256::from(1).abi_encode().as_slice()
|
||||
);
|
||||
// Check it has the expected gas cost (16 is documented in `return_true_code`)
|
||||
assert_eq!(test.provider.estimate_gas(&call).await.unwrap(), 21_000 + 16);
|
||||
assert_eq!(test.provider.estimate_gas(call).await.unwrap(), 21_000 + 16);
|
||||
}
|
||||
|
||||
let gas = test.router.execute_gas(Coin::Erc20(token), U256::from(0), &[].as_slice().into());
|
||||
let fee = U256::from(0);
|
||||
let (gas, fee) =
|
||||
test.router.execute_gas_and_fee(Coin::Erc20(token), U256::from(0), &[].as_slice().into());
|
||||
let (_tx, gas_used) = test.execute(Coin::Erc20(token), fee, [].as_slice().into(), vec![]).await;
|
||||
const UNUSED_GAS: u64 = Router::GAS_FOR_ERC20_CALL - 16;
|
||||
assert_eq!(gas_used + UNUSED_GAS, gas);
|
||||
@@ -600,8 +609,7 @@ async fn test_eth_address_out_instruction() {
|
||||
let out_instructions =
|
||||
OutInstructions::from([(SeraiEthereumAddress::Address(rand_address), amount_out)].as_slice());
|
||||
|
||||
let gas = test.router.execute_gas(Coin::Ether, U256::from(1), &out_instructions);
|
||||
let fee = U256::from(gas);
|
||||
let (gas, fee) = test.router.execute_gas_and_fee(Coin::Ether, U256::from(1), &out_instructions);
|
||||
|
||||
let () = test
|
||||
.provider
|
||||
@@ -638,8 +646,7 @@ async fn test_erc20_address_out_instruction() {
|
||||
let out_instructions =
|
||||
OutInstructions::from([(SeraiEthereumAddress::Address(rand_address), amount_out)].as_slice());
|
||||
|
||||
let gas = test.router.execute_gas(coin, U256::from(1), &out_instructions);
|
||||
let fee = U256::from(gas);
|
||||
let (gas, fee) = test.router.execute_gas_and_fee(coin, U256::from(1), &out_instructions);
|
||||
|
||||
// Mint to the Router the necessary amount of the ERC20
|
||||
erc20.mint(&test, test.router.address(), amount_out + fee).await;
|
||||
@@ -674,8 +681,7 @@ async fn test_eth_code_out_instruction() {
|
||||
.as_slice(),
|
||||
);
|
||||
|
||||
let gas = test.router.execute_gas(Coin::Ether, U256::from(1), &out_instructions);
|
||||
let fee = U256::from(gas);
|
||||
let (gas, fee) = test.router.execute_gas_and_fee(Coin::Ether, U256::from(1), &out_instructions);
|
||||
let (tx, gas_used) = test.execute(Coin::Ether, fee, out_instructions, vec![true]).await;
|
||||
|
||||
// We use call-traces here to determine how much gas was allowed but unused due to the complexity
|
||||
@@ -700,6 +706,34 @@ async fn test_eth_code_out_instruction() {
|
||||
assert_eq!(test.provider.get_code_at(deployed).await.unwrap().to_vec(), true.abi_encode());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_eth_code_out_instruction_reverts() {
|
||||
let mut test = Test::new().await;
|
||||
test.confirm_next_serai_key().await;
|
||||
let () = test
|
||||
.provider
|
||||
.raw_request("anvil_setBalance".into(), (test.router.address(), 1_000_000))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// [REVERT], which will cause `executeArbitraryCode`'s call to CREATE to fail
|
||||
let code = vec![0xfd];
|
||||
let amount_out = U256::from(0);
|
||||
let out_instructions = OutInstructions::from(
|
||||
[(
|
||||
SeraiEthereumAddress::Contract(ContractDeployment::new(50_000, code.clone()).unwrap()),
|
||||
amount_out,
|
||||
)]
|
||||
.as_slice(),
|
||||
);
|
||||
|
||||
let (gas, fee) = test.router.execute_gas_and_fee(Coin::Ether, U256::from(1), &out_instructions);
|
||||
let (tx, gas_used) = test.execute(Coin::Ether, fee, out_instructions, vec![true]).await;
|
||||
|
||||
let unused_gas = test.gas_unused_by_calls(&tx).await;
|
||||
assert_eq!(gas_used + unused_gas, gas);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_erc20_code_out_instruction() {
|
||||
let mut test = Test::new().await;
|
||||
@@ -715,8 +749,7 @@ async fn test_erc20_code_out_instruction() {
|
||||
.as_slice(),
|
||||
);
|
||||
|
||||
let gas = test.router.execute_gas(coin, U256::from(1), &out_instructions);
|
||||
let fee = U256::from(gas);
|
||||
let (gas, fee) = test.router.execute_gas_and_fee(coin, U256::from(1), &out_instructions);
|
||||
|
||||
// Mint to the Router the necessary amount of the ERC20
|
||||
erc20.mint(&test, test.router.address(), amount_out + fee).await;
|
||||
@@ -748,11 +781,11 @@ async fn test_result_decoding() {
|
||||
.as_slice(),
|
||||
);
|
||||
|
||||
let gas = test.router.execute_gas(Coin::Ether, U256::from(0), &out_instructions);
|
||||
let (gas, fee) = test.router.execute_gas_and_fee(Coin::Ether, U256::from(0), &out_instructions);
|
||||
|
||||
// We should decode these in the correct order (not `false, true, true`)
|
||||
let (_tx, gas_used) =
|
||||
test.execute(Coin::Ether, U256::from(0), out_instructions, vec![true, true, false]).await;
|
||||
test.execute(Coin::Ether, fee, out_instructions, vec![true, true, false]).await;
|
||||
// We don't check strict equality as we don't know how much gas was used by the reverted call
|
||||
// (even with the trace), solely that it used less than or equal to the limit
|
||||
assert!(gas_used <= gas);
|
||||
@@ -788,9 +821,8 @@ async fn test_reentrancy() {
|
||||
.as_slice(),
|
||||
);
|
||||
|
||||
let gas = test.router.execute_gas(Coin::Ether, U256::from(0), &out_instructions);
|
||||
let (_tx, gas_used) =
|
||||
test.execute(Coin::Ether, U256::from(0), out_instructions, vec![true]).await;
|
||||
let (gas, fee) = test.router.execute_gas_and_fee(Coin::Ether, U256::from(0), &out_instructions);
|
||||
let (_tx, gas_used) = test.execute(Coin::Ether, fee, out_instructions, vec![true]).await;
|
||||
// Even though this doesn't have failed `OutInstruction`s, our logic is incomplete upon any
|
||||
// failed internal calls for some reason. That's fine, as the gas yielded is still the worst-case
|
||||
// (which this isn't a counter-example to) and is validated to be the worst-case, but is peculiar
|
||||
@@ -799,7 +831,7 @@ async fn test_reentrancy() {
|
||||
|
||||
#[tokio::test]
|
||||
async fn fuzz_test_out_instructions_gas() {
|
||||
for _ in 0 .. 10 {
|
||||
for _ in 0 .. 100 {
|
||||
let mut test = Test::new().await;
|
||||
test.confirm_next_serai_key().await;
|
||||
|
||||
@@ -817,7 +849,7 @@ async fn fuzz_test_out_instructions_gas() {
|
||||
code.extend(&ext);
|
||||
|
||||
out_instructions.push((
|
||||
SeraiEthereumAddress::Contract(ContractDeployment::new(100_000, ext).unwrap()),
|
||||
SeraiEthereumAddress::Contract(ContractDeployment::new(100_000, code).unwrap()),
|
||||
amount_out,
|
||||
));
|
||||
} else {
|
||||
@@ -854,8 +886,7 @@ async fn fuzz_test_out_instructions_gas() {
|
||||
};
|
||||
|
||||
let fee_per_gas = U256::from(1) + U256::from(OsRng.next_u64() % 10);
|
||||
let gas = test.router.execute_gas(coin, fee_per_gas, &out_instructions);
|
||||
let fee = U256::from(gas) * fee_per_gas;
|
||||
let (gas, fee) = test.router.execute_gas_and_fee(coin, fee_per_gas, &out_instructions);
|
||||
// All of these should have succeeded
|
||||
let (tx, gas_used) =
|
||||
test.execute(coin, fee, out_instructions.clone(), vec![true; out_instructions.0.len()]).await;
|
||||
@@ -867,3 +898,47 @@ async fn fuzz_test_out_instructions_gas() {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_gas_increases_then_decreases() {
|
||||
/*
|
||||
This specific batch of `OutInstruction`s causes the gas to be initially calculated, and then
|
||||
increase as the proper fee is written in (due to the increased amount of non-zero bytes). But
|
||||
then, as the fee is updated until the final fee no longer increases the gas used, the gas
|
||||
actually goes *back down*. To then derive the fee from this reduced gas causes the gas to go
|
||||
back up.
|
||||
|
||||
A prior version of this library would return the reduced amount of gas fee in this edge case,
|
||||
which only rarely appeared via the fuzz test (yet did once, yielding this). Then, it'd derive
|
||||
the fee from it, and expect the realized transaction to have parity (causing a test failure as
|
||||
it didn't). Now, `execute_gas` is `execute_gas_and_fee`, yielding both the gas which is
|
||||
expected *and the fee for it*. This fee is guaranteed to cost the reported amount of gas,
|
||||
resolving this issue.
|
||||
*/
|
||||
let out_instructions = vec![(
|
||||
SeraiEthereumAddress::Contract(ContractDeployment::new(100240, vec![]).unwrap()),
|
||||
U256::from(1u8),
|
||||
)];
|
||||
|
||||
let mut test = Test::new().await;
|
||||
test.confirm_next_serai_key().await;
|
||||
|
||||
let out_instructions = OutInstructions::from(out_instructions.as_slice());
|
||||
|
||||
let coin = {
|
||||
let () = test
|
||||
.provider
|
||||
.raw_request("anvil_setBalance".into(), (test.router.address(), 1_000_000_000))
|
||||
.await
|
||||
.unwrap();
|
||||
Coin::Ether
|
||||
};
|
||||
|
||||
let fee_per_gas = U256::from(1);
|
||||
let (gas, fee) = test.router.execute_gas_and_fee(coin, fee_per_gas, &out_instructions);
|
||||
assert!((U256::from(gas) * fee_per_gas) != fee);
|
||||
let (tx, gas_used) =
|
||||
test.execute(coin, fee, out_instructions.clone(), vec![true; out_instructions.0.len()]).await;
|
||||
let unused_gas = test.gas_unused_by_calls(&tx).await;
|
||||
assert_eq!(gas_used + unused_gas, gas);
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ use std::io;
|
||||
use ciphersuite::Secp256k1;
|
||||
use frost::dkg::ThresholdKeys;
|
||||
|
||||
use alloy_core::primitives::U256;
|
||||
use alloy_core::primitives::{U256, Address as EthereumAddress};
|
||||
|
||||
use serai_client::networks::ethereum::Address;
|
||||
|
||||
@@ -17,8 +17,20 @@ use crate::{output::OutputId, machine::ClonableTransctionMachine};
|
||||
|
||||
#[derive(Clone, PartialEq, Debug)]
|
||||
pub(crate) enum Action {
|
||||
SetKey { chain_id: U256, nonce: u64, key: PublicKey },
|
||||
Batch { chain_id: U256, nonce: u64, coin: Coin, fee: U256, outs: Vec<(Address, U256)> },
|
||||
SetKey {
|
||||
chain_id: U256,
|
||||
router_address: EthereumAddress,
|
||||
nonce: u64,
|
||||
key: PublicKey,
|
||||
},
|
||||
Batch {
|
||||
chain_id: U256,
|
||||
router_address: EthereumAddress,
|
||||
nonce: u64,
|
||||
coin: Coin,
|
||||
fee: U256,
|
||||
outs: Vec<(Address, U256)>,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Clone, PartialEq, Eq, Debug)]
|
||||
@@ -33,25 +45,28 @@ impl Action {
|
||||
|
||||
pub(crate) fn message(&self) -> Vec<u8> {
|
||||
match self {
|
||||
Action::SetKey { chain_id, nonce, key } => {
|
||||
Router::update_serai_key_message(*chain_id, *nonce, key)
|
||||
Action::SetKey { chain_id, router_address, nonce, key } => {
|
||||
Router::update_serai_key_message(*chain_id, *router_address, *nonce, key)
|
||||
}
|
||||
Action::Batch { chain_id, router_address, nonce, coin, fee, outs } => {
|
||||
Router::execute_message(
|
||||
*chain_id,
|
||||
*router_address,
|
||||
*nonce,
|
||||
*coin,
|
||||
*fee,
|
||||
OutInstructions::from(outs.as_ref()),
|
||||
)
|
||||
}
|
||||
Action::Batch { chain_id, nonce, coin, fee, outs } => Router::execute_message(
|
||||
*chain_id,
|
||||
*nonce,
|
||||
*coin,
|
||||
*fee,
|
||||
OutInstructions::from(outs.as_ref()),
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn eventuality(&self) -> Eventuality {
|
||||
Eventuality(match self {
|
||||
Self::SetKey { chain_id: _, nonce, key } => {
|
||||
Self::SetKey { chain_id: _, router_address: _, nonce, key } => {
|
||||
Executed::NextSeraiKeySet { nonce: *nonce, key: key.eth_repr() }
|
||||
}
|
||||
Self::Batch { chain_id: _, nonce, .. } => {
|
||||
Self::Batch { chain_id: _, router_address: _, nonce, .. } => {
|
||||
Executed::Batch { nonce: *nonce, message_hash: keccak256(self.message()), results: vec![] }
|
||||
}
|
||||
})
|
||||
@@ -89,6 +104,10 @@ impl SignableTransaction for Action {
|
||||
reader.read_exact(&mut chain_id)?;
|
||||
let chain_id = U256::from_be_bytes(chain_id);
|
||||
|
||||
let mut router_address = [0; 20];
|
||||
reader.read_exact(&mut router_address)?;
|
||||
let router_address = EthereumAddress::from(router_address);
|
||||
|
||||
let mut nonce = [0; 8];
|
||||
reader.read_exact(&mut nonce)?;
|
||||
let nonce = u64::from_le_bytes(nonce);
|
||||
@@ -100,7 +119,7 @@ impl SignableTransaction for Action {
|
||||
let key =
|
||||
PublicKey::from_eth_repr(key).ok_or_else(|| io::Error::other("invalid key in Action"))?;
|
||||
|
||||
Action::SetKey { chain_id, nonce, key }
|
||||
Action::SetKey { chain_id, router_address, nonce, key }
|
||||
}
|
||||
1 => {
|
||||
let coin = borsh::from_reader(reader)?;
|
||||
@@ -123,22 +142,24 @@ impl SignableTransaction for Action {
|
||||
|
||||
outs.push((address, amount));
|
||||
}
|
||||
Action::Batch { chain_id, nonce, coin, fee, outs }
|
||||
Action::Batch { chain_id, router_address, nonce, coin, fee, outs }
|
||||
}
|
||||
_ => unreachable!(),
|
||||
})
|
||||
}
|
||||
fn write(&self, writer: &mut impl io::Write) -> io::Result<()> {
|
||||
match self {
|
||||
Self::SetKey { chain_id, nonce, key } => {
|
||||
Self::SetKey { chain_id, router_address, nonce, key } => {
|
||||
writer.write_all(&[0])?;
|
||||
writer.write_all(&chain_id.to_be_bytes::<32>())?;
|
||||
writer.write_all(router_address.as_slice())?;
|
||||
writer.write_all(&nonce.to_le_bytes())?;
|
||||
writer.write_all(&key.eth_repr())
|
||||
}
|
||||
Self::Batch { chain_id, nonce, coin, fee, outs } => {
|
||||
Self::Batch { chain_id, router_address, nonce, coin, fee, outs } => {
|
||||
writer.write_all(&[1])?;
|
||||
writer.write_all(&chain_id.to_be_bytes::<32>())?;
|
||||
writer.write_all(router_address.as_slice())?;
|
||||
writer.write_all(&nonce.to_le_bytes())?;
|
||||
borsh::BorshSerialize::serialize(coin, writer)?;
|
||||
writer.write_all(&fee.as_le_bytes())?;
|
||||
|
||||
@@ -4,7 +4,6 @@ use std::sync::Arc;
|
||||
use alloy_rlp::Encodable;
|
||||
|
||||
use alloy_transport::{TransportErrorKind, RpcError};
|
||||
use alloy_simple_request_transport::SimpleRequest;
|
||||
use alloy_provider::RootProvider;
|
||||
|
||||
use tokio::{
|
||||
@@ -26,13 +25,13 @@ use crate::{
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct TransactionPublisher<D: Db> {
|
||||
db: D,
|
||||
rpc: Arc<RootProvider<SimpleRequest>>,
|
||||
rpc: Arc<RootProvider>,
|
||||
router: Arc<RwLock<Option<Router>>>,
|
||||
relayer_url: String,
|
||||
}
|
||||
|
||||
impl<D: Db> TransactionPublisher<D> {
|
||||
pub(crate) fn new(db: D, rpc: Arc<RootProvider<SimpleRequest>>, relayer_url: String) -> Self {
|
||||
pub(crate) fn new(db: D, rpc: Arc<RootProvider>, relayer_url: String) -> Self {
|
||||
Self { db, rpc, router: Arc::new(RwLock::new(None)), relayer_url }
|
||||
}
|
||||
|
||||
@@ -88,8 +87,10 @@ impl<D: Db> signers::TransactionPublisher<Transaction> for TransactionPublisher<
|
||||
let nonce = tx.0.nonce();
|
||||
// Convert from an Action (an internal representation of a signable event) to a TxLegacy
|
||||
let tx = match tx.0 {
|
||||
Action::SetKey { chain_id: _, nonce: _, key } => router.update_serai_key(&key, &tx.1),
|
||||
Action::Batch { chain_id: _, nonce: _, coin, fee, outs } => {
|
||||
Action::SetKey { chain_id: _, router_address: _, nonce: _, key } => {
|
||||
router.update_serai_key(&key, &tx.1)
|
||||
}
|
||||
Action::Batch { chain_id: _, router_address: _, nonce: _, coin, fee, outs } => {
|
||||
router.execute(coin, fee, OutInstructions::from(outs.as_ref()), &tx.1)
|
||||
}
|
||||
};
|
||||
|
||||
@@ -2,9 +2,8 @@ use core::future::Future;
|
||||
use std::{sync::Arc, collections::HashSet};
|
||||
|
||||
use alloy_core::primitives::B256;
|
||||
use alloy_rpc_types_eth::{Header, BlockTransactionsKind, BlockNumberOrTag};
|
||||
use alloy_rpc_types_eth::{Header, BlockNumberOrTag};
|
||||
use alloy_transport::{RpcError, TransportErrorKind};
|
||||
use alloy_simple_request_transport::SimpleRequest;
|
||||
use alloy_provider::{Provider, RootProvider};
|
||||
|
||||
use serai_client::primitives::{ExternalNetworkId, ExternalCoin, Amount};
|
||||
@@ -26,7 +25,7 @@ use crate::{
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct Rpc<D: Db> {
|
||||
pub(crate) db: D,
|
||||
pub(crate) provider: Arc<RootProvider<SimpleRequest>>,
|
||||
pub(crate) provider: Arc<RootProvider>,
|
||||
}
|
||||
|
||||
impl<D: Db> ScannerFeed for Rpc<D> {
|
||||
@@ -49,7 +48,7 @@ impl<D: Db> ScannerFeed for Rpc<D> {
|
||||
async move {
|
||||
let actual_number = self
|
||||
.provider
|
||||
.get_block(BlockNumberOrTag::Finalized.into(), BlockTransactionsKind::Hashes)
|
||||
.get_block(BlockNumberOrTag::Finalized.into())
|
||||
.await?
|
||||
.ok_or_else(|| {
|
||||
TransportErrorKind::Custom("there was no finalized block".to_string().into())
|
||||
@@ -77,7 +76,7 @@ impl<D: Db> ScannerFeed for Rpc<D> {
|
||||
async move {
|
||||
let header = self
|
||||
.provider
|
||||
.get_block(BlockNumberOrTag::Number(number).into(), BlockTransactionsKind::Hashes)
|
||||
.get_block(BlockNumberOrTag::Number(number).into())
|
||||
.await?
|
||||
.ok_or_else(|| {
|
||||
TransportErrorKind::Custom(
|
||||
@@ -105,7 +104,7 @@ impl<D: Db> ScannerFeed for Rpc<D> {
|
||||
} else {
|
||||
self
|
||||
.provider
|
||||
.get_block((start - 1).into(), BlockTransactionsKind::Hashes)
|
||||
.get_block((start - 1).into())
|
||||
.await?
|
||||
.ok_or_else(|| {
|
||||
TransportErrorKind::Custom(
|
||||
@@ -120,7 +119,7 @@ impl<D: Db> ScannerFeed for Rpc<D> {
|
||||
|
||||
let end_header = self
|
||||
.provider
|
||||
.get_block((start + 31).into(), BlockTransactionsKind::Hashes)
|
||||
.get_block((start + 31).into())
|
||||
.await?
|
||||
.ok_or_else(|| {
|
||||
TransportErrorKind::Custom(
|
||||
@@ -177,7 +176,7 @@ impl<D: Db> ScannerFeed for Rpc<D> {
|
||||
while to_check != epoch.prior_end_hash {
|
||||
let to_check_block = self
|
||||
.provider
|
||||
.get_block(B256::from(to_check).into(), BlockTransactionsKind::Hashes)
|
||||
.get_block(B256::from(to_check).into())
|
||||
.await?
|
||||
.ok_or_else(|| {
|
||||
TransportErrorKind::Custom(
|
||||
|
||||
@@ -50,6 +50,7 @@ impl<D: Db> smart_contract_scheduler::SmartContract<Rpc<D>> for SmartContract {
|
||||
) -> (Self::SignableTransaction, EventualityFor<Rpc<D>>) {
|
||||
let action = Action::SetKey {
|
||||
chain_id: self.chain_id,
|
||||
router_address: if true { todo!("TODO") } else { Default::default() },
|
||||
nonce,
|
||||
key: PublicKey::new(new_key).expect("rotating to an invald key"),
|
||||
};
|
||||
@@ -139,6 +140,7 @@ impl<D: Db> smart_contract_scheduler::SmartContract<Rpc<D>> for SmartContract {
|
||||
|
||||
res.push(Action::Batch {
|
||||
chain_id: self.chain_id,
|
||||
router_address: if true { todo!("TODO") } else { Default::default() },
|
||||
nonce,
|
||||
coin: coin_to_ethereum_coin(coin),
|
||||
fee: U256::try_from(total_gas).unwrap() * fee_per_gas,
|
||||
|
||||
@@ -19,11 +19,10 @@ workspace = true
|
||||
[dependencies]
|
||||
k256 = { version = "0.13", default-features = false, features = ["std"] }
|
||||
|
||||
alloy-core = { version = "0.8", default-features = false }
|
||||
alloy-consensus = { version = "0.9", default-features = false, features = ["std"] }
|
||||
alloy-core = { version = "1", default-features = false }
|
||||
alloy-consensus = { version = "0.14", default-features = false, features = ["std"] }
|
||||
|
||||
alloy-rpc-types-eth = { version = "0.9", default-features = false }
|
||||
alloy-simple-request-transport = { path = "../../../networks/ethereum/alloy-simple-request-transport", default-features = false }
|
||||
alloy-provider = { version = "0.9", default-features = false }
|
||||
alloy-rpc-types-eth = { version = "0.14", default-features = false }
|
||||
alloy-provider = { version = "0.14", default-features = false }
|
||||
|
||||
ethereum-primitives = { package = "serai-processor-ethereum-primitives", path = "../primitives", default-features = false }
|
||||
|
||||
@@ -5,13 +5,12 @@
|
||||
use k256::{elliptic_curve::sec1::ToEncodedPoint, ProjectivePoint};
|
||||
|
||||
use alloy_core::{
|
||||
primitives::{Address, U256, Bytes, PrimitiveSignature, TxKind},
|
||||
primitives::{Address, U256, Bytes, Signature, TxKind},
|
||||
hex::FromHex,
|
||||
};
|
||||
use alloy_consensus::{SignableTransaction, TxLegacy, Signed};
|
||||
|
||||
use alloy_rpc_types_eth::TransactionReceipt;
|
||||
use alloy_simple_request_transport::SimpleRequest;
|
||||
use alloy_provider::{Provider, RootProvider};
|
||||
|
||||
use ethereum_primitives::{keccak256, deterministically_sign};
|
||||
@@ -24,7 +23,7 @@ fn address(point: &ProjectivePoint) -> [u8; 20] {
|
||||
}
|
||||
|
||||
/// Fund an account.
|
||||
pub async fn fund_account(provider: &RootProvider<SimpleRequest>, address: Address, value: U256) {
|
||||
pub async fn fund_account(provider: &RootProvider, address: Address, value: U256) {
|
||||
let _: () = provider
|
||||
.raw_request("anvil_setBalance".into(), [address.to_string(), value.to_string()])
|
||||
.await
|
||||
@@ -32,10 +31,7 @@ pub async fn fund_account(provider: &RootProvider<SimpleRequest>, address: Addre
|
||||
}
|
||||
|
||||
/// Publish an already-signed transaction.
|
||||
pub async fn publish_tx(
|
||||
provider: &RootProvider<SimpleRequest>,
|
||||
tx: Signed<TxLegacy>,
|
||||
) -> TransactionReceipt {
|
||||
pub async fn publish_tx(provider: &RootProvider, tx: Signed<TxLegacy>) -> TransactionReceipt {
|
||||
// Fund the sender's address
|
||||
fund_account(
|
||||
provider,
|
||||
@@ -55,7 +51,7 @@ pub async fn publish_tx(
|
||||
///
|
||||
/// The contract deployment will be done by a random account.
|
||||
pub async fn deploy_contract(
|
||||
provider: &RootProvider<SimpleRequest>,
|
||||
provider: &RootProvider,
|
||||
file_path: &str,
|
||||
constructor_arguments: &[u8],
|
||||
) -> Address {
|
||||
@@ -88,7 +84,7 @@ pub async fn deploy_contract(
|
||||
///
|
||||
/// This assumes the wallet is funded.
|
||||
pub async fn send(
|
||||
provider: &RootProvider<SimpleRequest>,
|
||||
provider: &RootProvider,
|
||||
wallet: &k256::ecdsa::SigningKey,
|
||||
mut tx: TxLegacy,
|
||||
) -> TransactionReceipt {
|
||||
@@ -111,7 +107,7 @@ pub async fn send(
|
||||
);
|
||||
|
||||
let mut bytes = vec![];
|
||||
tx.into_signed(PrimitiveSignature::from(sig)).eip2718_encode(&mut bytes);
|
||||
tx.into_signed(Signature::from(sig)).eip2718_encode(&mut bytes);
|
||||
let pending_tx = provider.send_raw_transaction(&bytes).await.unwrap();
|
||||
pending_tx.get_receipt().await.unwrap()
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
[toolchain]
|
||||
channel = "1.81"
|
||||
channel = "1.86"
|
||||
targets = ["wasm32-unknown-unknown"]
|
||||
profile = "minimal"
|
||||
components = ["rust-src", "rustfmt", "clippy"]
|
||||
|
||||
@@ -381,7 +381,7 @@ pub mod pallet {
|
||||
|
||||
// Explicitly provide a pre-dispatch which calls validate_unsigned
|
||||
fn pre_dispatch(call: &Self::Call) -> Result<(), TransactionValidityError> {
|
||||
Self::validate_unsigned(TransactionSource::InBlock, call).map(|_| ()).map_err(Into::into)
|
||||
Self::validate_unsigned(TransactionSource::InBlock, call).map(|_| ())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -351,7 +351,7 @@ pub fn new_full(mut config: Configuration) -> Result<TaskManager, ServiceError>
|
||||
telemetry: telemetry.as_mut(),
|
||||
})?;
|
||||
|
||||
if let sc_service::config::Role::Authority { .. } = &role {
|
||||
if let sc_service::config::Role::Authority = &role {
|
||||
let slot_duration = babe_link.config().slot_duration();
|
||||
let babe_config = sc_consensus_babe::BabeParams {
|
||||
keystore: keystore.clone(),
|
||||
|
||||
@@ -1254,7 +1254,7 @@ pub mod pallet {
|
||||
|
||||
// Explicitly provide a pre-dispatch which calls validate_unsigned
|
||||
fn pre_dispatch(call: &Self::Call) -> Result<(), TransactionValidityError> {
|
||||
Self::validate_unsigned(TransactionSource::InBlock, call).map(|_| ()).map_err(Into::into)
|
||||
Self::validate_unsigned(TransactionSource::InBlock, call).map(|_| ())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user