Skip to content

Commit 5c30ddd

Browse files
committed
feat: enhance PegInInvariant tests with non-LP refund liability and additional helper functions
1 parent 4a00380 commit 5c30ddd

File tree

2 files changed

+148
-3
lines changed

2 files changed

+148
-3
lines changed

test/invariant/PegInInvariant.t.sol

Lines changed: 44 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,12 @@ import {Flyover} from "../../src/libraries/Flyover.sol";
1010
/// @notice Tests critical invariants for the PegInContract using a dedicated handler
1111
contract PegInInvariantTest is PegInTestBase {
1212
PegInHandler public handler;
13+
address public nonLpRefundCreditor;
14+
uint256 public baselineBalance;
15+
16+
bytes constant RAW_TX_MOCK = hex"112233";
17+
bytes constant PMT_MOCK = hex"010203";
18+
uint256 constant HEIGHT_MOCK = 10;
1319

1420
function setUp() public {
1521
deployPegInContract();
@@ -30,6 +36,17 @@ contract PegInInvariantTest is PegInTestBase {
3036
);
3137
handler.addLP(extraPegInLp);
3238

39+
(address creditor, ) = _seedNonLpRefundLiabilityFixture(
40+
fullLp,
41+
fullLpKey,
42+
1 ether,
43+
HEIGHT_MOCK,
44+
RAW_TX_MOCK,
45+
PMT_MOCK
46+
);
47+
nonLpRefundCreditor = creditor;
48+
baselineBalance = address(pegInContract).balance;
49+
3350
targetContract(address(handler));
3451

3552
bytes4[] memory selectors = new bytes4[](2);
@@ -42,7 +59,7 @@ contract PegInInvariantTest is PegInTestBase {
4259

4360
// ============ Invariant Tests ============
4461

45-
/// @notice Contract balance must cover sum of all LP internal balances
62+
/// @notice LP-only lower bound: contract balance must cover tracked LP internal balances
4663
function invariant_ContractSolvent() public view {
4764
uint256 totalLPBalances = handler.calculateTotalLPBalances();
4865
assertGe(
@@ -52,6 +69,20 @@ contract PegInInvariantTest is PegInTestBase {
5269
);
5370
}
5471

72+
/// @notice Contract balance must cover tracked LP liabilities plus seeded non-LP refund creditor
73+
function invariant_ContractSolventIncludingNonLpRefundCreditor()
74+
public
75+
view
76+
{
77+
uint256 totalLPBalances = handler.calculateTotalLPBalances();
78+
uint256 nonLpLiability = pegInContract.getBalance(nonLpRefundCreditor);
79+
assertGe(
80+
address(pegInContract).balance,
81+
totalLPBalances + nonLpLiability,
82+
"INVARIANT VIOLATED: PegIn contract insolvent with non-LP refund liability"
83+
);
84+
}
85+
5586
/// @notice No individual LP balance should exceed total deposited through the handler
5687
function invariant_NoUnderflow() public view {
5788
uint256 deposited = handler.ghost_totalDeposited();
@@ -66,13 +97,18 @@ contract PegInInvariantTest is PegInTestBase {
6697
}
6798
}
6899

69-
/// @notice Contract balance must equal deposited minus withdrawn (tight equality)
100+
/// @notice Contract balance must equal deposited minus withdrawn (for deposit/withdraw handler actions)
70101
function invariant_GhostAccounting() public view {
71102
uint256 deposited = handler.ghost_totalDeposited();
72103
uint256 withdrawn = handler.ghost_totalWithdrawn();
104+
assertGe(
105+
address(pegInContract).balance,
106+
baselineBalance,
107+
"INVARIANT VIOLATED: Contract balance unexpectedly below baseline"
108+
);
73109

74110
assertEq(
75-
address(pegInContract).balance,
111+
address(pegInContract).balance - baselineBalance,
76112
deposited - withdrawn,
77113
"INVARIANT VIOLATED: Contract balance != deposited - withdrawn"
78114
);
@@ -91,6 +127,11 @@ contract PegInInvariantTest is PegInTestBase {
91127
"Handler withdraw calls:",
92128
handler.getHandlerCalls("withdraw")
93129
);
130+
console.log(
131+
"Seeded non-LP liability:",
132+
pegInContract.getBalance(nonLpRefundCreditor)
133+
);
134+
console.log("Baseline balance:", baselineBalance);
94135
console.log("Contract balance:", address(pegInContract).balance);
95136
console.log("-------------------------------\n");
96137
}

test/pegin/PegInTestBase.sol

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {CollateralManagementContract} from "../../src/CollateralManagement.sol";
77
import {FlyoverDiscovery} from "../../src/FlyoverDiscovery.sol";
88
import {PauseRegistry} from "../../src/PauseRegistry.sol";
99
import {BridgeMock} from "../../src/test-contracts/BridgeMock.sol";
10+
import {WalletMock} from "../../src/test-contracts/WalletMock.sol";
1011
import {ERC1967Proxy} from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol";
1112
import {Quotes} from "../../src/libraries/Quotes.sol";
1213
import {Flyover} from "../../src/libraries/Flyover.sol";
@@ -184,4 +185,107 @@ abstract contract PegInTestBase is Test {
184185
Flyover.ProviderType.Both
185186
);
186187
}
188+
189+
/// @notice Shared helper for constructing a standard PegIn quote fixture
190+
function _buildPegInQuoteFixture(
191+
uint256 value,
192+
address lp,
193+
address payable refundAddress,
194+
address destinationContract,
195+
uint256 nonce
196+
) internal view returns (Quotes.PegInQuote memory) {
197+
bytes memory testBtcAddress = new bytes(21);
198+
199+
return
200+
Quotes.PegInQuote({
201+
chainId: block.chainid,
202+
callFee: 100000000000000,
203+
penaltyFee: 10000000000000,
204+
value: value,
205+
gasFee: 100,
206+
fedBtcAddress: bytes20(testBtcAddress),
207+
lbcAddress: address(pegInContract),
208+
liquidityProviderRskAddress: lp,
209+
contractAddress: destinationContract,
210+
rskRefundAddress: refundAddress,
211+
nonce: int64(uint64(nonce)),
212+
gasLimit: 21000,
213+
agreementTimestamp: uint32(block.timestamp),
214+
timeForDeposit: 3600,
215+
callTime: 7200,
216+
depositConfirmations: 10,
217+
callOnRegister: false,
218+
btcRefundAddress: testBtcAddress,
219+
liquidityProviderBtcAddress: testBtcAddress,
220+
data: new bytes(0)
221+
});
222+
}
223+
224+
/// @notice Shared helper for signing PegIn quotes
225+
function _signPegInQuoteWithKey(
226+
uint256 privateKey,
227+
Quotes.PegInQuote memory quote
228+
) internal view returns (bytes memory) {
229+
bytes32 eip712Hash = pegInContract.hashPegInQuoteEIP712(quote);
230+
(uint8 v, bytes32 r, bytes32 s) = vm.sign(privateKey, eip712Hash);
231+
return abi.encodePacked(r, s, v);
232+
}
233+
234+
/// @notice Shared helper for creating BTC block headers with LE timestamp
235+
function _btcHeaderFromTimestamp(
236+
uint32 timestamp
237+
) internal pure returns (bytes memory) {
238+
bytes memory header = new bytes(80);
239+
header[68] = bytes1(uint8(timestamp));
240+
header[69] = bytes1(uint8(timestamp >> 8));
241+
header[70] = bytes1(uint8(timestamp >> 16));
242+
header[71] = bytes1(uint8(timestamp >> 24));
243+
return header;
244+
}
245+
246+
/// @notice Seeds one non-LP internal balance by forcing refund transfer failure on registerPegIn
247+
function _seedNonLpRefundLiabilityFixture(
248+
address lp,
249+
uint256 lpPrivateKey,
250+
uint256 value,
251+
uint256 height,
252+
bytes memory rawTx,
253+
bytes memory pmt
254+
) internal returns (address creditor, uint256 creditedAmount) {
255+
WalletMock refundWallet = new WalletMock();
256+
refundWallet.setRejectFunds(true);
257+
creditor = address(refundWallet);
258+
259+
Quotes.PegInQuote memory quote = _buildPegInQuoteFixture(
260+
value,
261+
lp,
262+
payable(creditor),
263+
address(0xBEEF),
264+
uint256(uint64(block.timestamp))
265+
);
266+
bytes32 quoteHash = pegInContract.hashPegInQuote(quote);
267+
bytes memory signature = _signPegInQuoteWithKey(lpPrivateKey, quote);
268+
uint256 peginAmount = quote.value + quote.callFee + quote.gasFee;
269+
270+
vm.deal(address(this), peginAmount);
271+
bridgeMock.setPegin{value: peginAmount}(quoteHash);
272+
bridgeMock.setHeader(
273+
height,
274+
_btcHeaderFromTimestamp(uint32(block.timestamp) + 300)
275+
);
276+
bridgeMock.setHeader(
277+
height + uint256(quote.depositConfirmations) - 1,
278+
_btcHeaderFromTimestamp(uint32(block.timestamp) + 600)
279+
);
280+
281+
vm.prank(lp);
282+
pegInContract.registerPegIn(quote, signature, rawTx, pmt, height);
283+
284+
creditedAmount = pegInContract.getBalance(creditor);
285+
assertGt(
286+
creditedAmount,
287+
0,
288+
"Expected non-LP refund creditor balance to be seeded"
289+
);
290+
}
187291
}

0 commit comments

Comments
 (0)