Skip to content

Commit b72fff2

Browse files
authored
Cancel eth tests (#136)
1 parent 6893903 commit b72fff2

14 files changed

Lines changed: 363 additions & 78 deletions
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
1-
{ "num_publications": 20, "average_gas_used_publish": 44563 }
2-
1+
{num_publications:20,
2+
average_gas_used_publish: 44563}

offchain/sample_deposit_proof.rs

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ mod utils;
77
use utils::{deploy_eth_bridge, deploy_signal_service, get_proofs, get_provider, SignalProof};
88

99
use alloy::hex::decode;
10-
use alloy::primitives::{Address, Bytes, FixedBytes, U256};
10+
use alloy::primitives::{address, Address, Bytes, FixedBytes, U256};
1111
use std::fs;
1212

1313
fn expand_vector(vec: Vec<Bytes>, name: &str) -> String {
@@ -28,6 +28,7 @@ fn create_deposit_call(
2828
amount: U256,
2929
data: &str,
3030
context: &str,
31+
canceler: &str,
3132
id: &FixedBytes<32>,
3233
) -> String {
3334
let mut result = String::new();
@@ -50,6 +51,7 @@ fn create_deposit_call(
5051
result += format!("\t\tdeposit.amount = {};\n", amount).as_str();
5152
result += format!("\t\tdeposit.data = bytes(hex\"{}\");\n", data).as_str();
5253
result += format!("\t\tdeposit.context = bytes(hex\"{}\");\n", context).as_str();
54+
result += format!("\t\tdeposit.canceler = address({});\n", canceler).as_str();
5355
result += format!("\t\t_createDeposit(\n\t\t\taccountProof,\n\t\t\tstorageProof,\n\t\t\tdeposit,\n\t\t\tbytes32({}),\n\t\t\tbytes32({})\n\t\t);\n", proof.slot, id).as_str();
5456
return result;
5557
}
@@ -76,11 +78,13 @@ fn deposit_specification() -> Vec<DepositSpecification> {
7678
"5932a71200000000000000000000000000000000000000000000000000000000000004d2", // (valid) call to `someNonPayableFunction(1234)`
7779
];
7880

81+
// makeAddr("canceler");
82+
let canceler = address!("0x738b9be4596e37015bA15F17116c9B2eE971c238");
7983
let zero_canceler = Address::ZERO;
8084

8185
let mut specifications = vec![];
82-
for amount in amounts {
83-
for data in calldata.iter() {
86+
for &amount in &amounts {
87+
for &data in &calldata {
8488
specifications.push(DepositSpecification {
8589
recipient: recipient.parse().unwrap(),
8690
amount: U256::from(amount),
@@ -90,6 +94,26 @@ fn deposit_specification() -> Vec<DepositSpecification> {
9094
});
9195
}
9296
}
97+
98+
// Special canceler cases
99+
100+
// Case 1: Empty calldata with non-zero canceler
101+
specifications.push(DepositSpecification {
102+
recipient: recipient.parse().unwrap(),
103+
amount: U256::from(amounts[1]),
104+
data: calldata[0].to_string(),
105+
context: String::from(""),
106+
canceler,
107+
});
108+
109+
// Case 2: Valid calldata with non-zero canceler
110+
specifications.push(DepositSpecification {
111+
recipient: recipient.parse().unwrap(),
112+
amount: U256::from(amounts[1]),
113+
data: calldata[1].to_string(),
114+
context: String::from(""),
115+
canceler,
116+
});
93117
return specifications;
94118
}
95119

@@ -145,6 +169,7 @@ async fn main() -> Result<()> {
145169
d.amount,
146170
d.data.as_str(),
147171
d.context.as_str(),
172+
&d.canceler.to_string(),
148173
id,
149174
)
150175
.as_str();

src/protocol/ETHBridge.sol

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@ contract ETHBridge is IETHBridge, ReentrancyGuardTransient {
9494
bytes memory proof
9595
) internal returns (bytes32 id) {
9696
id = _generateId(ethDeposit);
97-
require(!processed(id), AlreadyClaimed());
97+
require(!processed(id), AlreadyProcessed());
9898

9999
signalService.verifySignal(height, trustedCommitmentPublisher, counterpart, id, proof);
100100

src/protocol/IETHBridge.sol

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -52,15 +52,15 @@ interface IETHBridge {
5252
/// @dev Failed to call the receiver with value.
5353
error FailedClaim();
5454

55-
/// @dev A deposit was already claimed.
56-
error AlreadyClaimed();
55+
/// @dev To address is set to zero
56+
error ZeroReceiver();
57+
58+
/// @dev A deposit was already claimed or cancelled
59+
error AlreadyProcessed();
5760

5861
/// @dev Only canceler can cancel a deposit.
5962
error OnlyCanceler();
6063

61-
/// @dev Zero receiver
62-
error ZeroReceiver();
63-
6464
/// @dev Whether the deposit identified by `id` has been claimed or cancelled.
6565
/// @param id The deposit id
6666
function processed(bytes32 id) external view returns (bool);

test/ETHBridge/CancelDepositTest.t.sol

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -165,7 +165,7 @@ contract CancelDepositTest is InitialState {
165165

166166
// Try to cancel again - should revert
167167
vm.prank(canceler);
168-
vm.expectRevert(IETHBridge.AlreadyClaimed.selector);
168+
vm.expectRevert(IETHBridge.AlreadyProcessed.selector);
169169
bridge.cancelDeposit(ethDeposit, charlie, "cancel_data", HEIGHT, "proof");
170170
}
171171

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
// SPDX-License-Identifier: MIT
2+
pragma solidity ^0.8.28;
3+
4+
import {CrossChainDepositExists} from "./CrossChainDepositExists.t.sol";
5+
import {IETHBridge} from "src/protocol/IETHBridge.sol";
6+
7+
/// This contract describes behaviours that should be valid when the deposit is cancelable.
8+
abstract contract DepositIsCancelable is CrossChainDepositExists {
9+
function test_cancelDeposit_shouldSucceed() public {
10+
IETHBridge.ETHDeposit memory deposit = sampleDepositProof.getEthDeposit(_depositIdx());
11+
bytes memory proof = abi.encode(sampleDepositProof.getDepositSignalProof(_depositIdx()));
12+
vm.prank(cancelerAddress);
13+
bridge.cancelDeposit(deposit, cancellationRecipient, "", HEIGHT, proof);
14+
}
15+
16+
function test_cancelDeposit_shouldSetClaimedFlag() public {
17+
IETHBridge.ETHDeposit memory deposit = sampleDepositProof.getEthDeposit(_depositIdx());
18+
bytes memory proof = abi.encode(sampleDepositProof.getDepositSignalProof(_depositIdx()));
19+
(, bytes32 id) = sampleDepositProof.getDepositInternals(_depositIdx());
20+
21+
assertFalse(bridge.processed(id), "deposit already marked as claimed");
22+
23+
vm.prank(cancelerAddress);
24+
bridge.cancelDeposit(deposit, cancellationRecipient, "", HEIGHT, proof);
25+
assertTrue(bridge.processed(id), "deposit not marked as claimed");
26+
}
27+
28+
function test_cancelDeposit_shouldEmitEvent() public {
29+
IETHBridge.ETHDeposit memory deposit = sampleDepositProof.getEthDeposit(_depositIdx());
30+
bytes memory proof = abi.encode(sampleDepositProof.getDepositSignalProof(_depositIdx()));
31+
(, bytes32 id) = sampleDepositProof.getDepositInternals(_depositIdx());
32+
33+
vm.expectEmit();
34+
emit IETHBridge.DepositCancelled(id, cancellationRecipient, "");
35+
36+
vm.prank(cancelerAddress);
37+
bridge.cancelDeposit(deposit, cancellationRecipient, "", HEIGHT, proof);
38+
}
39+
40+
function test_cancelDeposit_shouldSendETH() public {
41+
IETHBridge.ETHDeposit memory deposit = sampleDepositProof.getEthDeposit(_depositIdx());
42+
bytes memory proof = abi.encode(sampleDepositProof.getDepositSignalProof(_depositIdx()));
43+
44+
uint256 initialCancellationRecipientBalance = cancellationRecipient.balance;
45+
uint256 initialRecipientBalance = recipient.balance;
46+
uint256 initialBridgeBalance = address(bridge).balance;
47+
48+
vm.prank(cancelerAddress);
49+
bridge.cancelDeposit(deposit, cancellationRecipient, "", HEIGHT, proof);
50+
assertEq(recipient.balance, initialRecipientBalance, "recipient balance mismatch");
51+
assertEq(
52+
cancellationRecipient.balance,
53+
initialCancellationRecipientBalance + deposit.amount,
54+
"cancel recipient balance mismatch"
55+
);
56+
assertEq(address(bridge).balance, initialBridgeBalance - deposit.amount, "bridge balance mismatch");
57+
}
58+
59+
function test_claimDeposit_shouldRevertWhen_DepositIsCancelled() public {
60+
IETHBridge.ETHDeposit memory deposit = sampleDepositProof.getEthDeposit(_depositIdx());
61+
bytes memory proof = abi.encode(sampleDepositProof.getDepositSignalProof(_depositIdx()));
62+
63+
vm.prank(cancelerAddress);
64+
bridge.cancelDeposit(deposit, cancellationRecipient, "", HEIGHT, proof);
65+
vm.expectRevert(IETHBridge.AlreadyProcessed.selector);
66+
bridge.claimDeposit(deposit, HEIGHT, proof);
67+
}
68+
69+
function test_cancelDeposit_shouldRevertWhen_CancellerIsNotCaller() public {
70+
IETHBridge.ETHDeposit memory deposit = sampleDepositProof.getEthDeposit(_depositIdx());
71+
bytes memory proof = abi.encode(sampleDepositProof.getDepositSignalProof(_depositIdx()));
72+
73+
vm.expectRevert(IETHBridge.OnlyCanceler.selector);
74+
vm.prank(makeAddr("notCanceller"));
75+
bridge.cancelDeposit(deposit, cancellationRecipient, "", HEIGHT, proof);
76+
}
77+
}
78+
79+
abstract contract DepositIsNotCancelable is CrossChainDepositExists {
80+
function test_cancelDeposit_shouldRevertWhen_NoCancelerIsSet() public {
81+
IETHBridge.ETHDeposit memory deposit = sampleDepositProof.getEthDeposit(_depositIdx());
82+
bytes memory proof = abi.encode(sampleDepositProof.getDepositSignalProof(_depositIdx()));
83+
84+
vm.expectRevert(IETHBridge.OnlyCanceler.selector);
85+
vm.prank(cancelerAddress);
86+
bridge.cancelDeposit(deposit, cancellationRecipient, "", HEIGHT, proof);
87+
}
88+
}

test/ETHBridge/ContractCallValidityScenarios.t.sol

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
// SPDX-License-Identifier: MIT
22
pragma solidity ^0.8.28;
33

4+
import {DepositIsCancelable} from "./CancelableScenarios.t.sol";
45
import {BridgeSufficientlyCapitalized} from "./CapitalizationScenarios.t.sol";
56
import {DepositIsClaimable, DepositIsNotClaimable} from "./ClaimableScenarios.t.sol";
67
import {CrossChainDepositExists} from "./CrossChainDepositExists.t.sol";
7-
import {RecipientIsAContract} from "./RecipientScenarios.t.sol";
8+
import {REQUIRED_INPUT, RecipientIsAContract, TransferRecipient} from "./RecipientScenarios.t.sol";
89
import {TransferRecipient} from "./RecipientScenarios.t.sol";
910
import {IETHBridge} from "src/protocol/IETHBridge.sol";
1011

@@ -48,3 +49,39 @@ abstract contract DepositIsInvalidContractCall is
4849
super.setUp();
4950
}
5051
}
52+
53+
// This contract describes the secnario where a cancelable deposit is made but the canceler
54+
// specifies a contract as the recipient.
55+
abstract contract CancelableDepositIsValidContractCall is
56+
RecipientIsAContract,
57+
BridgeSufficientlyCapitalized,
58+
DepositIsCancelable
59+
{
60+
function setUp()
61+
public
62+
virtual
63+
override(CrossChainDepositExists, RecipientIsAContract, BridgeSufficientlyCapitalized)
64+
{
65+
super.setUp();
66+
}
67+
68+
function test_cancelDeposit_canInvokeCancelerContract() public {
69+
IETHBridge.ETHDeposit memory deposit = sampleDepositProof.getEthDeposit(_depositIdx());
70+
bytes memory proof = abi.encode(sampleDepositProof.getDepositSignalProof(_depositIdx()));
71+
72+
bytes memory cancelCalldata = abi.encodeWithSignature("somePayableFunction(uint256)", (REQUIRED_INPUT));
73+
vm.prank(cancelerAddress);
74+
vm.expectEmit();
75+
emit TransferRecipient.FunctionCalled();
76+
bridge.cancelDeposit(deposit, recipient, cancelCalldata, HEIGHT, proof);
77+
}
78+
79+
function test_cancelDeposit_shouldNotInvokeRecipientContract() public {
80+
IETHBridge.ETHDeposit memory deposit = sampleDepositProof.getEthDeposit(_depositIdx());
81+
bytes memory proof = abi.encode(sampleDepositProof.getDepositSignalProof(_depositIdx()));
82+
83+
vm.prank(cancelerAddress);
84+
vm.expectRevert(IETHBridge.FailedClaim.selector);
85+
bridge.cancelDeposit(deposit, recipient, "", HEIGHT, proof);
86+
}
87+
}

test/ETHBridge/CrossChainDepositExists.t.sol

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,9 @@ abstract contract CrossChainDepositExists is UniversalTest {
1515
// the sample deposit is to this address
1616
address recipient = makeAddr("recipient");
1717

18+
// the recipient that receives the cancelled deposit
19+
address cancellationRecipient = makeAddr("cancellationRecipient");
20+
1821
uint256 HEIGHT = 1;
1922

2023
function setUp() public virtual override {

test/ETHBridge/EnumeratedScenarios.t.sol

Lines changed: 54 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,23 @@
11
// SPDX-License-Identifier: MIT
22
pragma solidity ^0.8.28;
33

4+
import {DepositIsCancelable, DepositIsNotCancelable} from "./CancelableScenarios.t.sol";
45
import {BridgeHasNoEther, BridgeSufficientlyCapitalized} from "./CapitalizationScenarios.t.sol";
56
import {DepositIsClaimable, DepositIsNotClaimable} from "./ClaimableScenarios.t.sol";
6-
import {DepositIsInvalidContractCall, DepositIsValidContractCall} from "./ContractCallValidityScenarios.t.sol";
7+
import {
8+
CancelableDepositIsValidContractCall,
9+
DepositIsInvalidContractCall,
10+
DepositIsValidContractCall
11+
} from "./ContractCallValidityScenarios.t.sol";
712
import {CrossChainDepositExists} from "./CrossChainDepositExists.t.sol";
813
import {RecipientIsAContract, RecipientIsAnEOA} from "./RecipientScenarios.t.sol";
914
import {
1015
NonzeroETH_InvalidCallToPayableFn,
1116
NonzeroETH_NoCalldata,
17+
NonzeroETH_NoCalldata_IsCancellable,
1218
NonzeroETH_ValidCallToNonpayableFn,
1319
NonzeroETH_ValidCallToPayableFn,
20+
NonzeroETH_ValidCallToPayableFn_IsCancelable,
1421
ZeroETH_InvalidCallToPayableFn,
1522
ZeroETH_NoCalldata,
1623
ZeroETH_ValidCallToNonpayableFn,
@@ -49,6 +56,42 @@ contract SimpleDepositToEOA_BridgeUndercollateralized is
4956
DepositIsNotClaimable
5057
{}
5158

59+
// A cancelable deposit is made to an EOA with no calldata
60+
contract CancelDeposit_WithNoCalldata_ToEOACancelRecipient is
61+
NonzeroETH_NoCalldata_IsCancellable,
62+
RecipientIsAnEOA,
63+
BridgeSufficientlyCapitalized,
64+
DepositIsCancelable
65+
{
66+
function setUp() public override(CrossChainDepositExists, BridgeSufficientlyCapitalized) {
67+
super.setUp();
68+
}
69+
}
70+
71+
// A cancelable deposit is made to an contract with calldata, cancel recipient is an EOA
72+
contract CancelDeposit_WithCalldata_ToEOACancelRecipient is
73+
NonzeroETH_ValidCallToPayableFn_IsCancelable,
74+
RecipientIsAnEOA,
75+
BridgeSufficientlyCapitalized,
76+
DepositIsCancelable
77+
{
78+
function setUp() public override(CrossChainDepositExists, BridgeSufficientlyCapitalized) {
79+
super.setUp();
80+
}
81+
}
82+
83+
// Same transfer as above, but no canceler is set. Cancellation attempts should fail.
84+
contract SimpleDepositToEOA_NoCancelerIsSet is
85+
NonzeroETH_NoCalldata,
86+
RecipientIsAnEOA,
87+
BridgeSufficientlyCapitalized,
88+
DepositIsNotCancelable
89+
{
90+
function setUp() public override(CrossChainDepositExists, BridgeSufficientlyCapitalized) {
91+
super.setUp();
92+
}
93+
}
94+
5295
// The bridge does a direct call (without the standard function invocation syntax). This means calldata passed to an EOA
5396
// is ignored
5497
// (and the call still succeeds)
@@ -70,6 +113,16 @@ contract DepositToPayableFunction is NonzeroETH_ValidCallToPayableFn, DepositIsV
70113
}
71114
}
72115

116+
// Should not be able to claim a deposit if the recipient of a cancelable deposit is a contract
117+
contract CancelDeposit_WithCalldata_ToContractCancelRecipient is
118+
NonzeroETH_ValidCallToPayableFn_IsCancelable,
119+
CancelableDepositIsValidContractCall
120+
{
121+
function setUp() public override(CrossChainDepositExists, CancelableDepositIsValidContractCall) {
122+
super.setUp();
123+
}
124+
}
125+
73126
// We should not be able to send ETH to a nonpayable function on a contract.
74127
contract DepositToNonPayableFunction is NonzeroETH_ValidCallToNonpayableFn, DepositIsInvalidContractCall {
75128
function setUp() public override(CrossChainDepositExists, DepositIsInvalidContractCall) {

test/ETHBridge/InitialState.t.sol

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,10 @@ abstract contract InitialState is Test {
1515
// zero address means any relayer is allowed
1616
bytes anyRelayer = new bytes(0);
1717

18-
// zero address means deposit is uncancellable
19-
address nonCancellableAddress = address(0);
18+
// address that can cancel deposits (if specified in the ETHDeposit)
19+
address cancelerAddress = makeAddr("canceler");
20+
21+
address zeroCanceler = address(0);
2022

2123
address trustedCommitmentPublisher = makeAddr("trustedCommitmentPublisher");
2224

0 commit comments

Comments
 (0)