@inco/lightning 0.8.0-devnet → 0.8.0-devnet-2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/IncoLightning.sol +4 -0
- package/src/Lib.alphanet.sol +62 -6
- package/src/Lib.demonet.sol +62 -6
- package/src/Lib.devnet.sol +62 -6
- package/src/Lib.sol +62 -6
- package/src/Lib.template.sol +65 -9
- package/src/Lib.testnet.sol +62 -6
- package/src/interfaces/IIncoLightning.sol +2 -0
- package/src/libs/incoLightning_alphanet_v0_297966649.sol +62 -6
- package/src/libs/incoLightning_alphanet_v1_725458969.sol +62 -6
- package/src/libs/incoLightning_alphanet_v2_976644394.sol +62 -6
- package/src/libs/incoLightning_demonet_v0_863421733.sol +62 -6
- package/src/libs/incoLightning_demonet_v2_467437523.sol +62 -6
- package/src/libs/incoLightning_devnet_v0_340846814.sol +62 -6
- package/src/libs/incoLightning_devnet_v1_904635675.sol +62 -6
- package/src/libs/incoLightning_devnet_v2_295237520.sol +62 -6
- package/src/libs/incoLightning_devnet_v3_976859633.sol +62 -6
- package/src/libs/incoLightning_testnet_v0_183408998.sol +62 -6
- package/src/libs/incoLightning_testnet_v2_889158349.sol +62 -6
- package/src/lightning-parts/Fee.sol +9 -13
- package/src/lightning-parts/TEELifecycle.sol +24 -0
- package/src/periphery/IncoUtils.sol +42 -0
- package/src/test/AddTwo.sol +3 -3
- package/src/test/TEELifecycle/TEELifecycleMockTest.t.sol +55 -0
- package/src/test/TestFakeInfra.t.sol +263 -0
- package/src/test/TestFeeWithdrawal.t.sol +60 -0
- package/src/test/TestIncoUtils.t.sol +242 -0
- package/src/IIncoLightning.sol +0 -22
|
@@ -14,6 +14,7 @@ contract TakesEInput is IncoTest {
|
|
|
14
14
|
|
|
15
15
|
euint256 public a;
|
|
16
16
|
ebool public b;
|
|
17
|
+
eaddress public c;
|
|
17
18
|
uint256 public decryptedA;
|
|
18
19
|
|
|
19
20
|
function setA(bytes memory uint256EInput) external {
|
|
@@ -25,6 +26,10 @@ contract TakesEInput is IncoTest {
|
|
|
25
26
|
b = boolEInput.newEbool(msg.sender);
|
|
26
27
|
}
|
|
27
28
|
|
|
29
|
+
function setC(bytes memory addressEInput) external {
|
|
30
|
+
c = addressEInput.newEaddress(msg.sender);
|
|
31
|
+
}
|
|
32
|
+
|
|
28
33
|
}
|
|
29
34
|
|
|
30
35
|
// its meta: this is testing correct behavior of our testing infrastructure
|
|
@@ -238,6 +243,264 @@ contract TestFakeInfra is IncoTest {
|
|
|
238
243
|
assertEq(getUint256Value(a), 1);
|
|
239
244
|
}
|
|
240
245
|
|
|
246
|
+
function testERandBoundedWithEuint256() public {
|
|
247
|
+
euint256 bound = e.asEuint256(100);
|
|
248
|
+
processAllOperations();
|
|
249
|
+
euint256 a = e.randBounded(bound);
|
|
250
|
+
processAllOperations();
|
|
251
|
+
assertEq(getUint256Value(a), 1);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// ============ FEE CACHING TESTS ============
|
|
255
|
+
|
|
256
|
+
// The fee slot used by the library
|
|
257
|
+
bytes32 constant FEE_SLOT = keccak256("inco.fee");
|
|
258
|
+
|
|
259
|
+
function testFeeCachingOnRand() public {
|
|
260
|
+
// Verify the slot is empty before any operation
|
|
261
|
+
bytes32 cachedFeeBefore = vm.load(address(this), FEE_SLOT);
|
|
262
|
+
assertEq(uint256(cachedFeeBefore), 0, "Fee should not be cached initially");
|
|
263
|
+
|
|
264
|
+
// Call rand which should cache the fee
|
|
265
|
+
euint256 a = e.rand();
|
|
266
|
+
processAllOperations();
|
|
267
|
+
assertEq(getUint256Value(a), 1);
|
|
268
|
+
|
|
269
|
+
// Verify the fee is now cached
|
|
270
|
+
bytes32 cachedFeeAfter = vm.load(address(this), FEE_SLOT);
|
|
271
|
+
assertEq(uint256(cachedFeeAfter), inco.getFee(), "Cached fee should match inco.getFee()");
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function testFeeCachingPersistsAcrossOperations() public {
|
|
275
|
+
// Verify the slot is empty before any operation
|
|
276
|
+
bytes32 cachedFeeBefore = vm.load(address(this), FEE_SLOT);
|
|
277
|
+
assertEq(uint256(cachedFeeBefore), 0, "Fee should not be cached initially");
|
|
278
|
+
|
|
279
|
+
// First operation caches the fee
|
|
280
|
+
e.rand();
|
|
281
|
+
processAllOperations();
|
|
282
|
+
|
|
283
|
+
// Get the cached fee
|
|
284
|
+
bytes32 cachedFeeFirst = vm.load(address(this), FEE_SLOT);
|
|
285
|
+
assertEq(uint256(cachedFeeFirst), inco.getFee(), "Fee should be cached after first operation");
|
|
286
|
+
|
|
287
|
+
// Second operation should use cached fee
|
|
288
|
+
e.rand();
|
|
289
|
+
processAllOperations();
|
|
290
|
+
|
|
291
|
+
// Verify fee is still the same (wasn't re-fetched)
|
|
292
|
+
bytes32 cachedFeeSecond = vm.load(address(this), FEE_SLOT);
|
|
293
|
+
assertEq(uint256(cachedFeeFirst), uint256(cachedFeeSecond), "Cached fee should persist across operations");
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
function testFeeCachingOnRandBounded() public {
|
|
297
|
+
// Verify the slot is empty before any operation
|
|
298
|
+
bytes32 cachedFeeBefore = vm.load(address(this), FEE_SLOT);
|
|
299
|
+
assertEq(uint256(cachedFeeBefore), 0, "Fee should not be cached initially");
|
|
300
|
+
|
|
301
|
+
// Call randBounded which should cache the fee
|
|
302
|
+
euint256 a = e.randBounded(100);
|
|
303
|
+
processAllOperations();
|
|
304
|
+
assertEq(getUint256Value(a), 1);
|
|
305
|
+
|
|
306
|
+
// Verify the fee is now cached
|
|
307
|
+
bytes32 cachedFeeAfter = vm.load(address(this), FEE_SLOT);
|
|
308
|
+
assertEq(uint256(cachedFeeAfter), inco.getFee(), "Cached fee should match inco.getFee()");
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
function testFeeCachingOnNewEuint256() public {
|
|
312
|
+
// Create a contract that will use e.newEuint256
|
|
313
|
+
TakesEInput inputContract = new TakesEInput();
|
|
314
|
+
vm.deal(address(inputContract), 1 ether);
|
|
315
|
+
|
|
316
|
+
// Verify the slot is empty in the input contract before operation
|
|
317
|
+
bytes32 cachedFeeBefore = vm.load(address(inputContract), FEE_SLOT);
|
|
318
|
+
assertEq(uint256(cachedFeeBefore), 0, "Fee should not be cached initially");
|
|
319
|
+
|
|
320
|
+
// Call setA which uses e.newEuint256 internally
|
|
321
|
+
address self = address(this);
|
|
322
|
+
bytes memory ciphertext = fakePrepareEuint256Ciphertext(42, self, address(inputContract));
|
|
323
|
+
inputContract.setA(ciphertext);
|
|
324
|
+
processAllOperations();
|
|
325
|
+
|
|
326
|
+
// Verify the fee is now cached in the input contract
|
|
327
|
+
bytes32 cachedFeeAfter = vm.load(address(inputContract), FEE_SLOT);
|
|
328
|
+
assertEq(uint256(cachedFeeAfter), inco.getFee(), "Cached fee should match inco.getFee()");
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
function testFeeCachingOnNewEbool() public {
|
|
332
|
+
// Create a contract that will use e.newEbool
|
|
333
|
+
TakesEInput inputContract = new TakesEInput();
|
|
334
|
+
vm.deal(address(inputContract), 1 ether);
|
|
335
|
+
|
|
336
|
+
// Verify the slot is empty in the input contract before operation
|
|
337
|
+
bytes32 cachedFeeBefore = vm.load(address(inputContract), FEE_SLOT);
|
|
338
|
+
assertEq(uint256(cachedFeeBefore), 0, "Fee should not be cached initially");
|
|
339
|
+
|
|
340
|
+
// Call setB which uses e.newEbool internally
|
|
341
|
+
address self = address(this);
|
|
342
|
+
bytes memory ciphertext = fakePrepareEboolCiphertext(true, self, address(inputContract));
|
|
343
|
+
inputContract.setB(ciphertext);
|
|
344
|
+
processAllOperations();
|
|
345
|
+
|
|
346
|
+
// Verify the fee is now cached in the input contract
|
|
347
|
+
bytes32 cachedFeeAfter = vm.load(address(inputContract), FEE_SLOT);
|
|
348
|
+
assertEq(uint256(cachedFeeAfter), inco.getFee(), "Cached fee should match inco.getFee()");
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
function testFeeCachingOnEAddress() public {
|
|
352
|
+
// Create a contract that will use e.newEaddress
|
|
353
|
+
TakesEInput inputContract = new TakesEInput();
|
|
354
|
+
vm.deal(address(inputContract), 1 ether);
|
|
355
|
+
|
|
356
|
+
// Verify the slot is empty in the input contract before operation
|
|
357
|
+
bytes32 cachedFeeBefore = vm.load(address(inputContract), FEE_SLOT);
|
|
358
|
+
assertEq(uint256(cachedFeeBefore), 0, "Fee should not be cached initially");
|
|
359
|
+
|
|
360
|
+
// Call setC which uses e.newEaddress internally
|
|
361
|
+
address self = address(this);
|
|
362
|
+
bytes memory ciphertext = fakePrepareEaddressCiphertext(alice, self, address(inputContract));
|
|
363
|
+
inputContract.setC(ciphertext);
|
|
364
|
+
processAllOperations();
|
|
365
|
+
|
|
366
|
+
// Verify the fee is now cached in the input contract
|
|
367
|
+
bytes32 cachedFeeAfter = vm.load(address(inputContract), FEE_SLOT);
|
|
368
|
+
assertEq(uint256(cachedFeeAfter), inco.getFee(), "Cached fee should match inco.getFee()");
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
function testFeeRetryWithStaleCachedFee() public {
|
|
372
|
+
// First, cache a valid fee by calling rand
|
|
373
|
+
e.rand();
|
|
374
|
+
processAllOperations();
|
|
375
|
+
|
|
376
|
+
// Now manually set the cached fee to a wrong value
|
|
377
|
+
// This simulates a fee increase after caching
|
|
378
|
+
vm.store(address(this), FEE_SLOT, bytes32(uint256(1)));
|
|
379
|
+
|
|
380
|
+
// Verify the stale fee is set
|
|
381
|
+
bytes32 staleFee = vm.load(address(this), FEE_SLOT);
|
|
382
|
+
assertEq(uint256(staleFee), 1, "Stale fee should be 1 wei");
|
|
383
|
+
|
|
384
|
+
// Call rand again - the first call with stale fee should fail,
|
|
385
|
+
// but retry with fresh fee should succeed (no revert = success)
|
|
386
|
+
euint256 a = e.rand();
|
|
387
|
+
processAllOperations();
|
|
388
|
+
|
|
389
|
+
// Verify we got a valid handle (non-zero)
|
|
390
|
+
assertTrue(euint256.unwrap(a) != bytes32(0), "Should return a valid handle");
|
|
391
|
+
|
|
392
|
+
// Verify the fee cache was updated to the correct value
|
|
393
|
+
bytes32 updatedFee = vm.load(address(this), FEE_SLOT);
|
|
394
|
+
assertEq(uint256(updatedFee), inco.getFee(), "Fee should be updated after retry");
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
function testFeeRetryOnRandBounded() public {
|
|
398
|
+
// Cache a valid fee first
|
|
399
|
+
e.rand();
|
|
400
|
+
processAllOperations();
|
|
401
|
+
|
|
402
|
+
// Set stale fee
|
|
403
|
+
vm.store(address(this), FEE_SLOT, bytes32(uint256(1)));
|
|
404
|
+
|
|
405
|
+
// randBounded should also retry successfully (no revert = success)
|
|
406
|
+
euint256 a = e.randBounded(100);
|
|
407
|
+
processAllOperations();
|
|
408
|
+
|
|
409
|
+
// Verify we got a valid handle (non-zero)
|
|
410
|
+
assertTrue(euint256.unwrap(a) != bytes32(0), "Should return a valid handle");
|
|
411
|
+
|
|
412
|
+
// Verify fee was updated
|
|
413
|
+
bytes32 updatedFee = vm.load(address(this), FEE_SLOT);
|
|
414
|
+
assertEq(uint256(updatedFee), inco.getFee(), "Fee should be updated after retry");
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
function testFeeRetryOnNewEuint256() public {
|
|
418
|
+
TakesEInput inputContract = new TakesEInput();
|
|
419
|
+
vm.deal(address(inputContract), 1 ether);
|
|
420
|
+
|
|
421
|
+
// First call to cache the fee
|
|
422
|
+
address self = address(this);
|
|
423
|
+
bytes memory ciphertext1 = fakePrepareEuint256Ciphertext(42, self, address(inputContract));
|
|
424
|
+
inputContract.setA(ciphertext1);
|
|
425
|
+
processAllOperations();
|
|
426
|
+
|
|
427
|
+
// Verify first value was stored correctly
|
|
428
|
+
assertEq(getUint256Value(inputContract.a()), 42, "First value should be 42");
|
|
429
|
+
|
|
430
|
+
// Set stale fee in the input contract
|
|
431
|
+
vm.store(address(inputContract), FEE_SLOT, bytes32(uint256(1)));
|
|
432
|
+
|
|
433
|
+
// Second call should retry and succeed
|
|
434
|
+
bytes memory ciphertext2 = fakePrepareEuint256Ciphertext(99, self, address(inputContract));
|
|
435
|
+
inputContract.setA(ciphertext2);
|
|
436
|
+
processAllOperations();
|
|
437
|
+
|
|
438
|
+
// Verify the retried value was stored correctly
|
|
439
|
+
assertEq(getUint256Value(inputContract.a()), 99, "Retried value should be 99");
|
|
440
|
+
|
|
441
|
+
// Verify fee was updated
|
|
442
|
+
bytes32 updatedFee = vm.load(address(inputContract), FEE_SLOT);
|
|
443
|
+
assertEq(uint256(updatedFee), inco.getFee(), "Fee should be updated after retry");
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
function testFeeRetryOnNewEbool() public {
|
|
447
|
+
TakesEInput inputContract = new TakesEInput();
|
|
448
|
+
vm.deal(address(inputContract), 1 ether);
|
|
449
|
+
|
|
450
|
+
// First call to cache the fee
|
|
451
|
+
address self = address(this);
|
|
452
|
+
bytes memory ciphertext1 = fakePrepareEboolCiphertext(true, self, address(inputContract));
|
|
453
|
+
inputContract.setB(ciphertext1);
|
|
454
|
+
processAllOperations();
|
|
455
|
+
|
|
456
|
+
// Verify first value was stored correctly
|
|
457
|
+
assertEq(getBoolValue(inputContract.b()), true, "First value should be true");
|
|
458
|
+
|
|
459
|
+
// Set stale fee
|
|
460
|
+
vm.store(address(inputContract), FEE_SLOT, bytes32(uint256(1)));
|
|
461
|
+
|
|
462
|
+
// Second call should retry and succeed
|
|
463
|
+
bytes memory ciphertext2 = fakePrepareEboolCiphertext(false, self, address(inputContract));
|
|
464
|
+
inputContract.setB(ciphertext2);
|
|
465
|
+
processAllOperations();
|
|
466
|
+
|
|
467
|
+
// Verify the retried value was stored correctly
|
|
468
|
+
assertEq(getBoolValue(inputContract.b()), false, "Retried value should be false");
|
|
469
|
+
|
|
470
|
+
// Verify fee was updated
|
|
471
|
+
bytes32 updatedFee = vm.load(address(inputContract), FEE_SLOT);
|
|
472
|
+
assertEq(uint256(updatedFee), inco.getFee(), "Fee should be updated after retry");
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
function testFeeRetryOnNewEaddress() public {
|
|
476
|
+
TakesEInput inputContract = new TakesEInput();
|
|
477
|
+
vm.deal(address(inputContract), 1 ether);
|
|
478
|
+
|
|
479
|
+
// First call to cache the fee
|
|
480
|
+
address self = address(this);
|
|
481
|
+
bytes memory ciphertext1 = fakePrepareEaddressCiphertext(alice, self, address(inputContract));
|
|
482
|
+
inputContract.setC(ciphertext1);
|
|
483
|
+
processAllOperations();
|
|
484
|
+
|
|
485
|
+
// Verify first value was stored correctly
|
|
486
|
+
assertEq(getAddressValue(inputContract.c()), alice, "First value should be alice");
|
|
487
|
+
|
|
488
|
+
// Set stale fee
|
|
489
|
+
vm.store(address(inputContract), FEE_SLOT, bytes32(uint256(1)));
|
|
490
|
+
|
|
491
|
+
// Second call should retry and succeed
|
|
492
|
+
bytes memory ciphertext2 = fakePrepareEaddressCiphertext(bob, self, address(inputContract));
|
|
493
|
+
inputContract.setC(ciphertext2);
|
|
494
|
+
processAllOperations();
|
|
495
|
+
|
|
496
|
+
// Verify the retried value was stored correctly
|
|
497
|
+
assertEq(getAddressValue(inputContract.c()), bob, "Retried value should be bob");
|
|
498
|
+
|
|
499
|
+
// Verify fee was updated
|
|
500
|
+
bytes32 updatedFee = vm.load(address(inputContract), FEE_SLOT);
|
|
501
|
+
assertEq(uint256(updatedFee), inco.getFee(), "Fee should be updated after retry");
|
|
502
|
+
}
|
|
503
|
+
|
|
241
504
|
function testEIfThenElse() public {
|
|
242
505
|
ebool controlA = e.asEbool(true);
|
|
243
506
|
ebool controlB = e.asEbool(false);
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
// SPDX-License-Identifier: No License
|
|
2
|
+
pragma solidity ^0.8;
|
|
3
|
+
|
|
4
|
+
import {inco} from "../Lib.sol";
|
|
5
|
+
import {IncoTest} from "./IncoTest.sol";
|
|
6
|
+
import {OwnableUpgradeable} from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
|
|
7
|
+
import {Fee} from "../lightning-parts/Fee.sol";
|
|
8
|
+
|
|
9
|
+
contract RejectingContract {
|
|
10
|
+
// Contract that rejects all Ether transfers
|
|
11
|
+
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
contract TestFeeWithdrawal is IncoTest {
|
|
15
|
+
|
|
16
|
+
function setUp() public override {
|
|
17
|
+
super.setUp();
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function testWithdrawFees_Succeeds() public {
|
|
21
|
+
vm.deal(address(inco), 1 ether);
|
|
22
|
+
vm.prank(owner);
|
|
23
|
+
inco.withdrawFees();
|
|
24
|
+
assertEq(address(inco).balance, 0);
|
|
25
|
+
assertEq(address(owner).balance, 1 ether);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function testWithdrawFees_Reverts_NoFeesToWithdraw() public {
|
|
29
|
+
vm.expectRevert(Fee.NoFeesToWithdraw.selector);
|
|
30
|
+
vm.prank(owner);
|
|
31
|
+
inco.withdrawFees();
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function testWithdrawFees_Reverts_NotOwner() public {
|
|
35
|
+
vm.expectRevert(abi.encodeWithSelector(OwnableUpgradeable.OwnableUnauthorizedAccount.selector, alice));
|
|
36
|
+
vm.prank(alice);
|
|
37
|
+
inco.withdrawFees();
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function testWithdrawFees_Reverts_FeeWithdrawalFailed() public {
|
|
41
|
+
// Deploy a contract that rejects Ether
|
|
42
|
+
RejectingContract rejectingOwner = new RejectingContract();
|
|
43
|
+
|
|
44
|
+
// Change owner to the rejecting contract
|
|
45
|
+
vm.prank(owner);
|
|
46
|
+
inco.transferOwnership(address(rejectingOwner));
|
|
47
|
+
|
|
48
|
+
// Add fees to withdraw
|
|
49
|
+
vm.deal(address(inco), 1 ether);
|
|
50
|
+
|
|
51
|
+
// Expect the FeeWithdrawalFailed error
|
|
52
|
+
vm.expectRevert(Fee.FeeWithdrawalFailed.selector);
|
|
53
|
+
|
|
54
|
+
// Try to withdraw (will fail because rejectingOwner can't receive Ether)
|
|
55
|
+
vm.prank(address(rejectingOwner));
|
|
56
|
+
inco.withdrawFees();
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
}
|
|
60
|
+
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
// SPDX-License-Identifier: No License
|
|
2
|
+
pragma solidity ^0.8;
|
|
3
|
+
|
|
4
|
+
import {Test} from "forge-std/Test.sol";
|
|
5
|
+
import {IncoUtils} from "../periphery/IncoUtils.sol";
|
|
6
|
+
|
|
7
|
+
/// @dev Helper contract that inherits IncoUtils and rejects ETH refunds
|
|
8
|
+
contract RejectingContract is IncoUtils {
|
|
9
|
+
|
|
10
|
+
// No receive() function - will reject ETH transfers
|
|
11
|
+
|
|
12
|
+
function callWithRefund() external payable refundUnspent {
|
|
13
|
+
// Does nothing, should trigger refund which will fail because this contract can't receive ETH
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/// @dev Helper contract that sends ETH to the target during execution
|
|
19
|
+
contract EthSenderDuringExecution {
|
|
20
|
+
|
|
21
|
+
function sendTo(address target, uint256 amount) external {
|
|
22
|
+
payable(target).transfer(amount);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
receive() external payable {}
|
|
26
|
+
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/// @dev Attacker contract that tries to re-enter on receiving refund
|
|
30
|
+
contract ReentrantAttacker {
|
|
31
|
+
|
|
32
|
+
TestIncoUtils target;
|
|
33
|
+
uint256 public attackCount;
|
|
34
|
+
|
|
35
|
+
constructor(TestIncoUtils _target) {
|
|
36
|
+
target = _target;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function attack() external payable {
|
|
40
|
+
target.mockFunctionNoSpend{value: msg.value}();
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
receive() external payable {
|
|
44
|
+
attackCount++;
|
|
45
|
+
if (attackCount < 3) {
|
|
46
|
+
// Try to re-enter
|
|
47
|
+
target.mockFunctionNoSpend{value: msg.value}();
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
contract TestIncoUtils is Test, IncoUtils {
|
|
54
|
+
|
|
55
|
+
EthSenderDuringExecution sender;
|
|
56
|
+
|
|
57
|
+
function setUp() public {
|
|
58
|
+
sender = new EthSenderDuringExecution();
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Mock functions to test the modifier
|
|
62
|
+
function mockFunctionNoSpend() external payable refundUnspent {
|
|
63
|
+
// Does nothing, no ETH spent
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function mockFunctionPartialSpend(uint256 amount) external payable refundUnspent {
|
|
67
|
+
// Send ETH to simulate spending
|
|
68
|
+
payable(address(0)).transfer(amount);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function mockFunctionFullSpend() external payable refundUnspent {
|
|
72
|
+
// Send all msg.value to simulate full spending
|
|
73
|
+
payable(address(0)).transfer(msg.value);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/// @dev Mock function that receives ETH during execution (simulates external deposit)
|
|
77
|
+
function mockFunctionReceivesEthDuringExecution(address payable ethSender, uint256 incomingAmount)
|
|
78
|
+
external
|
|
79
|
+
payable
|
|
80
|
+
refundUnspent
|
|
81
|
+
{
|
|
82
|
+
// Trigger external contract to send us ETH during execution
|
|
83
|
+
EthSenderDuringExecution(ethSender).sendTo(address(this), incomingAmount);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
receive() external payable {}
|
|
87
|
+
|
|
88
|
+
function testRefundUnspentWhenNoSpend() public {
|
|
89
|
+
uint256 msgValue = 1 ether;
|
|
90
|
+
|
|
91
|
+
// Set up balance for the call (vm.deal overwrites, not adds)
|
|
92
|
+
vm.deal(address(this), msgValue);
|
|
93
|
+
uint256 balanceBefore = address(this).balance;
|
|
94
|
+
|
|
95
|
+
// Call function that doesn't spend any ETH
|
|
96
|
+
this.mockFunctionNoSpend{value: msgValue}();
|
|
97
|
+
|
|
98
|
+
// Check that full amount was refunded (balance unchanged)
|
|
99
|
+
assertEq(address(this).balance, balanceBefore);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function testRefundUnspentWhenPartialSpend() public {
|
|
103
|
+
uint256 msgValue = 1 ether;
|
|
104
|
+
uint256 spendAmount = 0.3 ether;
|
|
105
|
+
|
|
106
|
+
// Set up balance for the call
|
|
107
|
+
vm.deal(address(this), msgValue);
|
|
108
|
+
uint256 balanceBefore = address(this).balance;
|
|
109
|
+
|
|
110
|
+
// Call function that spends partial amount
|
|
111
|
+
this.mockFunctionPartialSpend{value: msgValue}(spendAmount);
|
|
112
|
+
|
|
113
|
+
// Check that correct amount was refunded (only spendAmount was consumed)
|
|
114
|
+
assertEq(address(this).balance, balanceBefore - spendAmount);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function testRefundUnspentWhenFullSpend() public {
|
|
118
|
+
uint256 msgValue = 1 ether;
|
|
119
|
+
|
|
120
|
+
// Set up balance for the call
|
|
121
|
+
vm.deal(address(this), msgValue);
|
|
122
|
+
uint256 balanceBefore = address(this).balance;
|
|
123
|
+
|
|
124
|
+
// Call function that spends full amount
|
|
125
|
+
this.mockFunctionFullSpend{value: msgValue}();
|
|
126
|
+
|
|
127
|
+
// Check that no refund occurred (all was spent)
|
|
128
|
+
assertEq(address(this).balance, balanceBefore - msgValue);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function testRefundUnspentWhenOverSpend() public {
|
|
132
|
+
uint256 msgValue = 0.5 ether;
|
|
133
|
+
uint256 spendAmount = 1 ether;
|
|
134
|
+
|
|
135
|
+
// Add extra balance to contract beyond what we'll send
|
|
136
|
+
vm.deal(address(this), spendAmount + msgValue);
|
|
137
|
+
uint256 balanceBefore = address(this).balance;
|
|
138
|
+
|
|
139
|
+
// Call function that spends more than msg.value (uses contract's existing balance)
|
|
140
|
+
this.mockFunctionPartialSpend{value: msgValue}(spendAmount);
|
|
141
|
+
|
|
142
|
+
// Check that no refund occurred (spent more than msg.value, so nothing to refund)
|
|
143
|
+
assertEq(address(this).balance, balanceBefore - spendAmount);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function testRefundFailedReverts() public {
|
|
147
|
+
uint256 msgValue = 1 ether;
|
|
148
|
+
|
|
149
|
+
// Create a contract that cannot receive ETH refunds (no receive function)
|
|
150
|
+
RejectingContract rejecter = new RejectingContract();
|
|
151
|
+
vm.deal(address(rejecter), msgValue);
|
|
152
|
+
|
|
153
|
+
// Call from rejecter itself so msg.sender (rejecter) cannot receive the refund
|
|
154
|
+
vm.prank(address(rejecter));
|
|
155
|
+
vm.expectRevert(IncoUtils.RefundFailed.selector);
|
|
156
|
+
rejecter.callWithRefund{value: msgValue}();
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function testRefundUnspentWithZeroMsgValue() public {
|
|
160
|
+
// Set up some initial balance
|
|
161
|
+
vm.deal(address(this), 1 ether);
|
|
162
|
+
uint256 balanceBefore = address(this).balance;
|
|
163
|
+
|
|
164
|
+
// Call with zero msg.value - should not revert and no refund needed
|
|
165
|
+
this.mockFunctionNoSpend{value: 0}();
|
|
166
|
+
|
|
167
|
+
// Balance should be unchanged
|
|
168
|
+
assertEq(address(this).balance, balanceBefore);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function testRefundUnspentWithWeiPrecision() public {
|
|
172
|
+
uint256 msgValue = 1000 wei;
|
|
173
|
+
uint256 spendAmount = 333 wei;
|
|
174
|
+
|
|
175
|
+
vm.deal(address(this), msgValue);
|
|
176
|
+
uint256 balanceBefore = address(this).balance;
|
|
177
|
+
|
|
178
|
+
this.mockFunctionPartialSpend{value: msgValue}(spendAmount);
|
|
179
|
+
|
|
180
|
+
// Check exact wei-level precision
|
|
181
|
+
assertEq(address(this).balance, balanceBefore - spendAmount);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function testRefundUnspentWhenContractReceivesEthDuringExecution() public {
|
|
185
|
+
uint256 msgValue = 1 ether;
|
|
186
|
+
uint256 incomingEth = 0.5 ether;
|
|
187
|
+
|
|
188
|
+
// Fund the sender contract
|
|
189
|
+
vm.deal(address(sender), incomingEth);
|
|
190
|
+
vm.deal(address(this), msgValue);
|
|
191
|
+
uint256 balanceBefore = address(this).balance;
|
|
192
|
+
|
|
193
|
+
// During execution, sender will send us 0.5 ETH
|
|
194
|
+
// This means balanceAfter > balanceBefore, so spent = 0
|
|
195
|
+
// Refund should be capped at msg.value (1 ETH)
|
|
196
|
+
this.mockFunctionReceivesEthDuringExecution{value: msgValue}(payable(address(sender)), incomingEth);
|
|
197
|
+
|
|
198
|
+
// We sent 1 ETH, received 0.5 ETH during execution, and got 1 ETH refunded
|
|
199
|
+
// Final: balanceBefore - msgValue + incomingEth + msgValue = balanceBefore + incomingEth
|
|
200
|
+
assertEq(address(this).balance, balanceBefore + incomingEth);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function testRefundUnspentWhenSpendingExactlyMsgValue() public {
|
|
204
|
+
// Edge case: spend exactly msg.value (boundary condition)
|
|
205
|
+
// This is similar to testRefundUnspentWhenFullSpend but uses mockFunctionPartialSpend
|
|
206
|
+
uint256 msgValue = 1 ether;
|
|
207
|
+
|
|
208
|
+
vm.deal(address(this), msgValue);
|
|
209
|
+
uint256 balanceBefore = address(this).balance;
|
|
210
|
+
|
|
211
|
+
// Spend exactly msg.value using partial spend function
|
|
212
|
+
this.mockFunctionPartialSpend{value: msgValue}(msgValue);
|
|
213
|
+
|
|
214
|
+
// No refund should occur - spent exactly what was sent
|
|
215
|
+
assertEq(address(this).balance, balanceBefore - msgValue);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function testRefundUnspentReentrancyProtection() public {
|
|
219
|
+
ReentrantAttacker attacker = new ReentrantAttacker(this);
|
|
220
|
+
vm.deal(address(attacker), 2 ether);
|
|
221
|
+
|
|
222
|
+
// When the attacker tries to re-enter, ReentrantCall() is thrown inside the receive()
|
|
223
|
+
// This causes the receive() to revert, which fails the refund .call(), surfacing RefundFailed()
|
|
224
|
+
vm.prank(address(attacker));
|
|
225
|
+
vm.expectRevert(IncoUtils.RefundFailed.selector);
|
|
226
|
+
attacker.attack{value: 1 ether}();
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function testFuzzRefundUnspent(uint256 msgValue, uint256 spendAmount) public {
|
|
230
|
+
// Bound to reasonable values
|
|
231
|
+
msgValue = bound(msgValue, 0, 10 ether);
|
|
232
|
+
spendAmount = bound(spendAmount, 0, msgValue);
|
|
233
|
+
|
|
234
|
+
vm.deal(address(this), msgValue);
|
|
235
|
+
uint256 balanceBefore = address(this).balance;
|
|
236
|
+
|
|
237
|
+
this.mockFunctionPartialSpend{value: msgValue}(spendAmount);
|
|
238
|
+
|
|
239
|
+
assertEq(address(this).balance, balanceBefore - spendAmount);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
}
|
package/src/IIncoLightning.sol
DELETED
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
// SPDX-License-Identifier: No License
|
|
2
|
-
pragma solidity ^0.8;
|
|
3
|
-
|
|
4
|
-
import {IEncryptedInput} from "./lightning-parts/interfaces/IEncryptedInput.sol";
|
|
5
|
-
import {IEncryptedOperations} from "./lightning-parts/interfaces/IEncryptedOperations.sol";
|
|
6
|
-
import {ITrivialEncryption} from "./lightning-parts/interfaces/ITrivialEncryption.sol";
|
|
7
|
-
import {IBaseAccessControlList} from "./lightning-parts/AccessControl/interfaces/IBaseAccessControlList.sol";
|
|
8
|
-
import {IHandleGeneration} from "./lightning-parts/primitives/interfaces/IHandleGeneration.sol";
|
|
9
|
-
import {IVersion} from "./version/interfaces/IVersion.sol";
|
|
10
|
-
|
|
11
|
-
interface IIncoLightning is
|
|
12
|
-
IEncryptedInput,
|
|
13
|
-
IEncryptedOperations,
|
|
14
|
-
ITrivialEncryption,
|
|
15
|
-
IBaseAccessControlList,
|
|
16
|
-
IHandleGeneration,
|
|
17
|
-
IVersion
|
|
18
|
-
{
|
|
19
|
-
|
|
20
|
-
function initialize(address owner) external;
|
|
21
|
-
|
|
22
|
-
}
|