Skip to content

Commit 32fbadf

Browse files
Merge pull request #450 from rsksmart/refactor/FLY-2022
test(pegout): add MultiPegOutPayer integration test contract and Mult…
2 parents c7c87e5 + c13568d commit 32fbadf

2 files changed

Lines changed: 223 additions & 0 deletions

File tree

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
// SPDX-License-Identifier: MIT
2+
pragma solidity 0.8.25;
3+
4+
import {IPegOut} from "../interfaces/IPegOut.sol";
5+
import {Quotes} from "../libraries/Quotes.sol";
6+
7+
// solhint-disable comprehensive-interface
8+
contract MultiPegOutPayer {
9+
struct PegOutPayment {
10+
Quotes.PegOutQuote quote;
11+
bytes signature;
12+
}
13+
14+
IPegOut public immutable LBC;
15+
address public immutable OWNER;
16+
17+
event Deposit(address indexed sender, uint256 indexed amount);
18+
event Withdraw(address indexed owner, uint256 indexed amount);
19+
20+
error InsufficientBalance(uint256 balance, uint256 required);
21+
error NotOwner(address account);
22+
error SendError(bytes cause);
23+
24+
constructor(address payable lbc_) {
25+
LBC = IPegOut(lbc_);
26+
OWNER = msg.sender;
27+
}
28+
29+
receive() external payable {
30+
emit Deposit(msg.sender, msg.value);
31+
}
32+
33+
/// @notice Pays for all N quotes in a single transaction, emitting N PegOutDeposit events.
34+
function executeMultiplePegOuts(PegOutPayment[] calldata payments) external {
35+
uint256 paymentsLength = payments.length;
36+
for (uint256 i = 0; i < paymentsLength; ++i) {
37+
Quotes.PegOutQuote calldata q = payments[i].quote;
38+
uint256 total = q.value + q.gasFee + q.callFee;
39+
if (address(this).balance < total) {
40+
revert InsufficientBalance(address(this).balance, total);
41+
}
42+
LBC.depositPegOut{value: total}(q, payments[i].signature);
43+
}
44+
}
45+
46+
function withdraw(uint256 amount) external {
47+
if (msg.sender != OWNER) revert NotOwner(msg.sender);
48+
if (address(this).balance < amount) revert InsufficientBalance(address(this).balance, amount);
49+
emit Withdraw(OWNER, amount);
50+
(bool sent, bytes memory cause) = payable(OWNER).call{value: amount}("");
51+
if (!sent) revert SendError(cause);
52+
}
53+
}

test/pegout/MultiPegOut.t.sol

Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
// SPDX-License-Identifier: MIT
2+
pragma solidity 0.8.25;
3+
4+
import {PegOutTestBase} from "./PegOutTestBase.sol";
5+
import {IPegOut} from "../../src/interfaces/IPegOut.sol";
6+
import {Quotes} from "../../src/libraries/Quotes.sol";
7+
import {MultiPegOutPayer} from "../../src/test-contracts/MultiPegOutPayer.sol";
8+
9+
contract MultiPegOutTest is PegOutTestBase {
10+
MultiPegOutPayer public multiPayer;
11+
address public user;
12+
13+
function setUp() public {
14+
deployPegOutContract();
15+
setupProviders();
16+
initBtcMocks();
17+
user = makeAddr("user");
18+
multiPayer = new MultiPegOutPayer(payable(address(pegOutContract)));
19+
vm.deal(address(multiPayer), 100 ether);
20+
}
21+
22+
function test_MultiPegOut_EmitsNDepositEvents() public {
23+
Quotes.PegOutQuote memory quote1 = createTestPegOutQuote(
24+
0.5 ether,
25+
fullLp,
26+
1
27+
);
28+
Quotes.PegOutQuote memory quote2 = createTestPegOutQuote(
29+
0.5 ether,
30+
fullLp,
31+
2
32+
);
33+
34+
bytes32 quoteHash1 = pegOutContract.hashPegOutQuote(quote1);
35+
bytes32 quoteHash2 = pegOutContract.hashPegOutQuote(quote2);
36+
37+
bytes memory sig1 = signQuote(fullLp, quote1);
38+
bytes memory sig2 = signQuote(fullLp, quote2);
39+
40+
MultiPegOutPayer.PegOutPayment[]
41+
memory payments = new MultiPegOutPayer.PegOutPayment[](2);
42+
payments[0] = MultiPegOutPayer.PegOutPayment({
43+
quote: quote1,
44+
signature: sig1
45+
});
46+
payments[1] = MultiPegOutPayer.PegOutPayment({
47+
quote: quote2,
48+
signature: sig2
49+
});
50+
51+
vm.expectEmit(true, true, false, false);
52+
emit IPegOut.PegOutDeposit(
53+
quoteHash1,
54+
address(multiPayer),
55+
0,
56+
getTotalValue(quote1)
57+
);
58+
vm.expectEmit(true, true, false, false);
59+
emit IPegOut.PegOutDeposit(
60+
quoteHash2,
61+
address(multiPayer),
62+
0,
63+
getTotalValue(quote2)
64+
);
65+
66+
multiPayer.executeMultiplePegOuts(payments);
67+
68+
assertFalse(pegOutContract.isQuoteCompleted(quoteHash1));
69+
assertFalse(pegOutContract.isQuoteCompleted(quoteHash2));
70+
}
71+
72+
function test_MultiPegOut_RevertsIfBalanceInsufficient() public {
73+
Quotes.PegOutQuote memory quote1 = createTestPegOutQuote(
74+
0.5 ether,
75+
fullLp,
76+
1
77+
);
78+
Quotes.PegOutQuote memory quote2 = createTestPegOutQuote(
79+
0.5 ether,
80+
fullLp,
81+
2
82+
);
83+
84+
bytes memory sig1 = signQuote(fullLp, quote1);
85+
bytes memory sig2 = signQuote(fullLp, quote2);
86+
87+
MultiPegOutPayer.PegOutPayment[]
88+
memory payments = new MultiPegOutPayer.PegOutPayment[](2);
89+
payments[0] = MultiPegOutPayer.PegOutPayment({
90+
quote: quote1,
91+
signature: sig1
92+
});
93+
payments[1] = MultiPegOutPayer.PegOutPayment({
94+
quote: quote2,
95+
signature: sig2
96+
});
97+
98+
// Give multiPayer only enough for the first payment; after it is sent the balance is 0.
99+
vm.deal(address(multiPayer), getTotalValue(quote1));
100+
101+
vm.expectRevert(
102+
abi.encodeWithSelector(
103+
MultiPegOutPayer.InsufficientBalance.selector,
104+
uint256(0),
105+
getTotalValue(quote2)
106+
)
107+
);
108+
multiPayer.executeMultiplePegOuts(payments);
109+
}
110+
111+
// ============ Helper Functions ============
112+
113+
function createTestPegOutQuote(
114+
uint256 value,
115+
address lp,
116+
int64 nonce
117+
) internal view returns (Quotes.PegOutQuote memory) {
118+
bytes memory testBtcAddress = abi.encodePacked(
119+
hex"6f",
120+
hex"89abcdefabbaabbaabbaabbaabbaabbaabbaabba"
121+
);
122+
uint32 currentTime = uint32(block.timestamp);
123+
124+
return
125+
Quotes.PegOutQuote({
126+
chainId: block.chainid,
127+
callFee: 100000000000000,
128+
penaltyFee: 10000000000000,
129+
value: value,
130+
gasFee: 100,
131+
lbcAddress: address(pegOutContract),
132+
lpRskAddress: lp,
133+
rskRefundAddress: user,
134+
nonce: nonce,
135+
agreementTimestamp: currentTime,
136+
depositDateLimit: currentTime + 7200,
137+
transferTime: 3600,
138+
depositConfirmations: 10,
139+
transferConfirmations: 2,
140+
expireBlock: uint32(block.number + 1000),
141+
expireDate: currentTime + 20000,
142+
depositAddress: testBtcAddress,
143+
btcRefundAddress: testBtcAddress,
144+
lpBtcAddress: testBtcAddress
145+
});
146+
}
147+
148+
function getTotalValue(
149+
Quotes.PegOutQuote memory quote
150+
) internal pure returns (uint256) {
151+
return quote.value + quote.callFee + quote.gasFee;
152+
}
153+
154+
function signQuote(
155+
address signer,
156+
Quotes.PegOutQuote memory quote
157+
) internal returns (bytes memory) {
158+
uint256 privateKey;
159+
if (signer == fullLp) {
160+
privateKey = fullLpKey;
161+
} else if (signer == pegInLp) {
162+
privateKey = pegInLpKey;
163+
} else {
164+
privateKey = pegOutLpKey;
165+
}
166+
bytes32 eip712Hash = pegOutContract.hashPegOutQuoteEIP712(quote);
167+
(uint8 v, bytes32 r, bytes32 s) = vm.sign(privateKey, eip712Hash);
168+
return abi.encodePacked(r, s, v);
169+
}
170+
}

0 commit comments

Comments
 (0)