Skip to content

Commit 6542ac1

Browse files
authored
Merge pull request #414 from rsksmart/feature/FLY-2196
Feature/FLY-2196 - Add EIP-712 typed signatures
2 parents 23817d5 + 9dece52 commit 6542ac1

33 files changed

+480
-233
lines changed

src/PegInContract.sol

Lines changed: 36 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
// SPDX-License-Identifier: MIT
22
pragma solidity 0.8.25;
33

4+
import {EIP712Upgradeable} from "@openzeppelin/contracts-upgradeable/utils/cryptography/EIP712Upgradeable.sol";
45
import {ReentrancyGuardUpgradeable} from "@openzeppelin/contracts-upgradeable/utils/ReentrancyGuardUpgradeable.sol";
56
import {BtcUtils} from "@rsksmart/btc-transaction-solidity-helper/contracts/BtcUtils.sol";
67
import {OpCodes} from "@rsksmart/btc-transaction-solidity-helper/contracts/OpCodes.sol";
@@ -19,6 +20,7 @@ import {SignatureValidator} from "./libraries/SignatureValidator.sol";
1920
contract PegInContract is
2021
EmergencyPause,
2122
ReentrancyGuardUpgradeable,
23+
EIP712Upgradeable,
2224
IPegIn
2325
{
2426
/// @notice This struct is used to store the information of a call on behalf of the user
@@ -31,6 +33,8 @@ contract PegInContract is
3133

3234
/// @notice The version of the contract
3335
string constant public VERSION = "1.0.0";
36+
/// @notice The name of the contract (used for EIP712)
37+
string constant public NAME = "PegInContract";
3438
Flyover.ProviderType constant private _PEG_TYPE = Flyover.ProviderType.PegIn;
3539
uint256 constant private _REFUND_ADDRESS_LENGTH = 21;
3640

@@ -94,6 +98,7 @@ contract PegInContract is
9498
) external initializer {
9599
if (collateralManagement.code.length == 0) revert Flyover.NoContract(collateralManagement);
96100
__ReentrancyGuard_init();
101+
__EIP712_init(NAME, VERSION);
97102
// Initialize EmergencyPause (includes AccessControl, Pausable, and grants PAUSER_ROLE)
98103
__EmergencyPause_init(0, defaultAdmin);
99104
_bridge = IBridge(bridge);
@@ -289,6 +294,11 @@ contract PegInContract is
289294
return _hashPegInQuote(quote);
290295
}
291296

297+
/// @inheritdoc IPegIn
298+
function hashPegInQuoteEIP712(Quotes.PegInQuote calldata quote) external view override returns (bytes32) {
299+
return _hashPegInQuoteEIP712(quote);
300+
}
301+
292302
/// @inheritdoc IPegIn
293303
function getQuoteStatus(bytes32 quoteHash) external view override returns (PegInStates) {
294304
if (_reentrancyGuardEntered()) revert ReentrancyGuardReentrantCall();
@@ -469,8 +479,9 @@ contract PegInContract is
469479
if (_processedQuotes[quoteHash] == PegInStates.PROCESSED_QUOTE) {
470480
revert QuoteAlreadyProcessed(quoteHash);
471481
}
472-
if (!SignatureValidator.verify(quote.liquidityProviderRskAddress, quoteHash, signature)) {
473-
revert SignatureValidator.IncorrectSignature(quote.liquidityProviderRskAddress, quoteHash, signature);
482+
bytes32 eip712hash = _hashPegInQuoteEIP712(quote);
483+
if (!SignatureValidator.verify(quote.liquidityProviderRskAddress, eip712hash, signature)) {
484+
revert SignatureValidator.IncorrectSignature(quote.liquidityProviderRskAddress, eip712hash, signature);
474485
}
475486
// the actual type in the RSKj node source code is a java int which is equivalent to int32
476487
if (height > uint256(int(type(int32).max)) - 1) {
@@ -489,6 +500,29 @@ contract PegInContract is
489500
/// @param quote The peg in quote
490501
/// @return quoteHash The hash of the quote
491502
function _hashPegInQuote(Quotes.PegInQuote calldata quote) private view returns (bytes32) {
503+
_validatePegInQuote(quote);
504+
return keccak256(Quotes.encodeQuote(quote));
505+
}
506+
507+
/// @notice This function is used to hash a peg in quote using EIP712 specification
508+
/// @dev The function also validates the following:
509+
/// - The quote belongs to this contract
510+
/// - The quote destination is not the bridge contract
511+
/// - The quote BTC refund address is valid
512+
/// - The quote liquidity provider BTC address is valid
513+
/// - The quote total amount is greater than the bridge minimum peg in amount
514+
/// - The sum of the timestamp values is not greater than the maximum uint32 value
515+
/// @param quote The peg in quote
516+
/// @return quoteHash The hash struct to be combined with the domain separator
517+
function _hashPegInQuoteEIP712(Quotes.PegInQuote calldata quote) private view returns (bytes32) {
518+
_validatePegInQuote(quote);
519+
return _hashTypedDataV4(Quotes.hashPegInQuoteEIP712(quote));
520+
}
521+
522+
function _validatePegInQuote(Quotes.PegInQuote calldata quote) private view {
523+
if (quote.chainId != block.chainid) {
524+
revert Flyover.InvalidChainId(block.chainid, quote.chainId);
525+
}
492526
if (address(this) != quote.lbcAddress) {
493527
revert Flyover.IncorrectContract(address(this), quote.lbcAddress);
494528
}
@@ -508,7 +542,6 @@ contract PegInContract is
508542
if (type(uint32).max < uint64(quote.agreementTimestamp) + uint64(quote.timeForDeposit)) {
509543
revert Flyover.Overflow(type(uint32).max);
510544
}
511-
return keccak256(Quotes.encodeQuote(quote));
512545
}
513546

514547
/// @notice This function is used to determine if the liquidity provider should be penalized

src/PegOutContract.sol

Lines changed: 35 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
// SPDX-License-Identifier: MIT
22
pragma solidity 0.8.25;
33

4+
import {EIP712Upgradeable} from "@openzeppelin/contracts-upgradeable/utils/cryptography/EIP712Upgradeable.sol";
45
import {ReentrancyGuardUpgradeable} from "@openzeppelin/contracts-upgradeable/utils/ReentrancyGuardUpgradeable.sol";
56
import {BtcUtils} from "@rsksmart/btc-transaction-solidity-helper/contracts/BtcUtils.sol";
67
import {EmergencyPause} from "./EmergencyPause/EmergencyPause.sol";
@@ -17,6 +18,7 @@ import {SignatureValidator} from "./libraries/SignatureValidator.sol";
1718
contract PegOutContract is
1819
EmergencyPause,
1920
ReentrancyGuardUpgradeable,
21+
EIP712Upgradeable,
2022
IPegOut
2123
{
2224
/// @notice This struct is used to store the information of a peg out
@@ -30,6 +32,8 @@ contract PegOutContract is
3032

3133
/// @notice The version of the contract
3234
string constant public VERSION = "1.0.0";
35+
/// @notice The name of the contract (used for EIP712)
36+
string constant public NAME = "PegOutContract";
3337
Flyover.ProviderType constant private _PEG_TYPE = Flyover.ProviderType.PegOut;
3438
// Index of the BTC output that must pay quote.depositAddress during peg-out refund validation.
3539
uint256 constant private _PAY_TO_ADDRESS_OUTPUT = 0;
@@ -90,10 +94,11 @@ contract PegOutContract is
9094
revert QuoteExpiredByBlocks(quote.expireBlock);
9195
}
9296

93-
bytes32 quoteHash = _hashPegOutQuote(quote);
94-
if (!SignatureValidator.verify(quote.lpRskAddress, quoteHash, signature)) {
95-
revert SignatureValidator.IncorrectSignature(quote.lpRskAddress, quoteHash, signature);
97+
bytes32 eip712Hash = _hashPegOutQuoteEIP712(quote);
98+
if (!SignatureValidator.verify(quote.lpRskAddress, eip712Hash, signature)) {
99+
revert SignatureValidator.IncorrectSignature(quote.lpRskAddress, eip712Hash, signature);
96100
}
101+
bytes32 quoteHash = _hashPegOutQuote(quote);
97102

98103
Quotes.PegOutQuote storage registeredQuote = _pegOutQuotes[quoteHash];
99104

@@ -139,6 +144,7 @@ contract PegOutContract is
139144
) external initializer {
140145
if (collateralManagement.code.length == 0) revert Flyover.NoContract(collateralManagement);
141146
__ReentrancyGuard_init();
147+
__EIP712_init(NAME, VERSION);
142148
// Initialize EmergencyPause (includes AccessControl, Pausable, and grants PAUSER_ROLE)
143149
__EmergencyPause_init(0, defaultAdmin);
144150
_bridge = IBridge(bridge);
@@ -255,6 +261,13 @@ contract PegOutContract is
255261
return _hashPegOutQuote(quote);
256262
}
257263

264+
/// @inheritdoc IPegOut
265+
function hashPegOutQuoteEIP712(
266+
Quotes.PegOutQuote calldata quote
267+
) external view override returns (bytes32) {
268+
return _hashPegOutQuoteEIP712(quote);
269+
}
270+
258271
/// @inheritdoc IPegOut
259272
function isQuoteCompleted(bytes32 quoteHash) external view override returns (bool) {
260273
return _isQuoteCompleted(quoteHash);
@@ -297,10 +310,28 @@ contract PegOutContract is
297310
function _hashPegOutQuote(
298311
Quotes.PegOutQuote calldata quote
299312
) private view returns (bytes32) {
313+
_validatePegOutQuote(quote);
314+
return keccak256(Quotes.encodePegOutQuote(quote));
315+
}
316+
317+
/// @notice This function is used to hash a peg out quote using EIP712 specification
318+
/// @dev The function also validates the quote belongs to this contract
319+
/// @param quote the peg out quote to hash
320+
/// @return quoteHash the hash of the peg out quote
321+
function _hashPegOutQuoteEIP712(Quotes.PegOutQuote calldata quote) private view returns (bytes32) {
322+
_validatePegOutQuote(quote);
323+
return _hashTypedDataV4(Quotes.hashPegOutQuoteEIP712(quote));
324+
}
325+
326+
/// @notice This function is used to validate a peg out quote before hashing it
327+
/// @param quote The peg out quote to validate
328+
function _validatePegOutQuote(Quotes.PegOutQuote calldata quote) private view {
329+
if (quote.chainId != block.chainid) {
330+
revert Flyover.InvalidChainId(block.chainid, quote.chainId);
331+
}
300332
if (address(this) != quote.lbcAddress) {
301333
revert Flyover.IncorrectContract(address(this), quote.lbcAddress);
302334
}
303-
return keccak256(Quotes.encodePegOutQuote(quote));
304335
}
305336

306337
/// @notice This function is used to check if a quote has been completed (refunded by any party)

src/interfaces/IPegIn.sol

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
// SPDX-License-Identifier: MIT
22
pragma solidity 0.8.25;
33

4+
import {IERC5267} from "@openzeppelin/contracts/interfaces/IERC5267.sol";
45
import {Quotes} from "../libraries/Quotes.sol";
56
import {IPausable} from "./IPausable.sol";
67

78
/// @title PegIn interface
89
/// @notice This interface is used to expose the required functions to provide the Flyover peg in service
9-
interface IPegIn is IPausable {
10+
interface IPegIn is IPausable, IERC5267 {
1011

1112
/// @notice The states of a peg in quote
1213
/// @dev The quote set to CALL_DONE when the callForUser function is called
@@ -160,6 +161,11 @@ interface IPegIn is IPausable {
160161
/// @return quoteHash The hash of the quote
161162
function hashPegInQuote(Quotes.PegInQuote calldata quote) external view returns (bytes32);
162163

164+
/// @notice This view is used to get the hash of a peg in quote using EIP712 specification
165+
/// @param quote The quote of the peg in
166+
/// @return hashStruct The hash struct to be combined with the domain separator
167+
function hashPegInQuoteEIP712(Quotes.PegInQuote calldata quote) external view returns (bytes32);
168+
163169
/// @notice This function is used to get the minimum peg in amount allowed by the protocol
164170
/// @return minPegIn The minimum peg in amount
165171
function getMinPegIn() external view returns (uint256);

src/interfaces/IPegOut.sol

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
// SPDX-License-Identifier: MIT
22
pragma solidity 0.8.25;
33

4+
import {IERC5267} from "@openzeppelin/contracts/interfaces/IERC5267.sol";
45
import {Quotes} from "../libraries/Quotes.sol";
56
import {IPausable} from "./IPausable.sol";
67

78
/// @title PegOut interface
89
/// @notice This interface is used to expose the required functions to provide the Flyover peg out service
9-
interface IPegOut is IPausable {
10+
interface IPegOut is IPausable, IERC5267 {
1011

1112
/// @notice Emitted when a peg out is refunded to the liquidity
1213
/// provider after successfully providing the service
@@ -153,6 +154,11 @@ interface IPegOut is IPausable {
153154
/// @param quote the quote to hash
154155
function hashPegOutQuote(Quotes.PegOutQuote calldata quote) external view returns (bytes32);
155156

157+
/// @notice This view is used to get the hash of a peg out quote using EIP712 specification
158+
/// @param quote The quote of the peg out
159+
/// @return hashStruct The hash struct to be combined with the domain separator
160+
function hashPegOutQuoteEIP712(Quotes.PegOutQuote calldata quote) external view returns (bytes32);
161+
156162
/// @notice This view is used to check if a quote has been completed. Completed means it was paid and refunded
157163
/// doesn't matter if the refund was to the liquidity provider (success) or to the user (failure)
158164
/// @param quoteHash the hash of the quote to check

src/legacy/LiquidityBridgeContract.sol

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ pragma experimental ABIEncoderV2;
44

55
import "../interfaces/IBridge.sol";
66
import "./Quotes.sol";
7-
import "../libraries/SignatureValidator.sol";
7+
import "./SignatureValidator.sol";
88
import "@rsksmart/btc-transaction-solidity-helper/contracts/BtcUtils.sol";
99
import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
1010
import "@openzeppelin/contracts-upgradeable/utils/ReentrancyGuardUpgradeable.sol";

src/legacy/LiquidityBridgeContractV2.sol

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ pragma experimental ABIEncoderV2;
44

55
import "../interfaces/IBridge.sol";
66
import "./QuotesV2.sol";
7-
import "../libraries/SignatureValidator.sol";
7+
import "./SignatureValidator.sol";
88
import "@rsksmart/btc-transaction-solidity-helper/contracts/BtcUtils.sol";
99
import "@rsksmart/btc-transaction-solidity-helper/contracts/OpCodes.sol";
1010
import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";

src/legacy/SignatureValidator.sol

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
// SPDX-License-Identifier: MIT
2+
pragma solidity 0.8.25;
3+
4+
import { ECDSA } from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";
5+
6+
library SignatureValidator {
7+
8+
using ECDSA for bytes32;
9+
10+
error IncorrectSignature(address expectedAddress, bytes32 usedHash, bytes signature);
11+
error ZeroAddress();
12+
/**
13+
@dev Verfies signature against address
14+
@param addr The signing address
15+
@param quoteHash The hash of the signed data
16+
@param signature The signature containing v, r and s
17+
@return True if the signature is valid, false otherwise.
18+
*/
19+
function verify(address addr, bytes32 quoteHash, bytes memory signature) public pure returns (bool) {
20+
21+
if (addr == address(0)) {
22+
revert ZeroAddress();
23+
}
24+
25+
if (signature.length != 65) {
26+
revert IncorrectSignature(addr, quoteHash, signature);
27+
}
28+
29+
30+
bytes32 r;
31+
bytes32 s;
32+
uint8 v;
33+
34+
assembly {
35+
r := mload(add(signature, 0x20))
36+
s := mload(add(signature, 0x40))
37+
v := byte(0, mload(add(signature, 0x60)))
38+
}
39+
// TODO use EIP712 compatible format instead
40+
bytes memory prefix = "\x19Ethereum Signed Message:\n32";
41+
bytes32 prefixedHash = keccak256(abi.encodePacked(prefix, quoteHash));
42+
return prefixedHash.recover(v, r, s) == addr;
43+
}
44+
}

src/libraries/Flyover.sol

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,4 +31,8 @@ library Flyover {
3131
error InsufficientAmount(uint256 amount, uint256 target);
3232
error Overflow(uint256 passedAmount);
3333
error InvalidAddress(address addr);
34+
/// @notice Quote was created for a different chain
35+
/// @param expected The current chain id (block.chainid)
36+
/// @param actual The chain id in the quote
37+
error InvalidChainId(uint256 expected, uint256 actual);
3438
}

0 commit comments

Comments
 (0)