Skip to content

Commit 294045c

Browse files
LeoPatOZnikeshnazarethggonzalez94
authored
Add Cancel Eth Deposit on Bridge (#120)
Co-authored-by: Nikesh Nazareth <business@nikeshnazareth.com> Co-authored-by: Gustavo Gonzalez <gustavo.gonzalez@openzeppelin.com>
1 parent 4fdd13d commit 294045c

7 files changed

Lines changed: 162 additions & 102 deletions

File tree

offchain/sample_deposit_proof.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ pub struct DepositSpecification {
5959
pub amount: U256,
6060
pub data: String,
6161
pub context: String,
62+
pub canceler: Address,
6263
}
6364

6465
fn deposit_specification() -> Vec<DepositSpecification> {
@@ -75,6 +76,8 @@ fn deposit_specification() -> Vec<DepositSpecification> {
7576
"5932a71200000000000000000000000000000000000000000000000000000000000004d2", // (valid) call to `someNonPayableFunction(1234)`
7677
];
7778

79+
let zero_canceler = Address::ZERO;
80+
7881
let mut specifications = vec![];
7982
for amount in amounts {
8083
for data in calldata.iter() {
@@ -83,6 +86,7 @@ fn deposit_specification() -> Vec<DepositSpecification> {
8386
amount: U256::from(amount),
8487
data: data.to_string(),
8588
context: String::from(""),
89+
canceler: zero_canceler,
8690
});
8791
}
8892
}
@@ -105,6 +109,7 @@ async fn main() -> Result<()> {
105109
spec.recipient,
106110
decode(spec.data.clone())?.into(),
107111
decode(spec.context.clone())?.into(),
112+
spec.canceler,
108113
)
109114
.value(spec.amount)
110115
.send()

src/protocol/ETHBridge.sol

Lines changed: 38 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,11 @@ import {IETHBridge} from "./IETHBridge.sol";
55
import {ISignalService} from "./ISignalService.sol";
66
import {ReentrancyGuardTransient} from "@openzeppelin/contracts/utils/ReentrancyGuardTransient.sol";
77

8-
/// @dev ETH bridging contract to send native ETH between L1 <-> L2 using storage proofs.
9-
/// @dev In contracts to the `SignalService`, this contract does not expect the bridge to be deployed on the same
8+
/// @dev In contrast to the `SignalService`, this contract does not expect the bridge to be deployed on the same
109
/// address on both chains. This is because it is designed so that each rollup has its own independent bridge contract,
1110
/// and they may furthermore decide to deploy a new version of the bridge in the future.
12-
///
13-
/// IMPORTANT: No recovery mechanism is implemented in case an account creates a deposit that can't be claimed.
1411
contract ETHBridge is IETHBridge, ReentrancyGuardTransient {
15-
mapping(bytes32 id => bool claimed) private _claimed;
12+
mapping(bytes32 id => bool processed) private _processed;
1613

1714
/// Incremental nonce to generate unique deposit IDs.
1815
uint256 private _globalDepositNonce;
@@ -39,8 +36,8 @@ contract ETHBridge is IETHBridge, ReentrancyGuardTransient {
3936
}
4037

4138
/// @inheritdoc IETHBridge
42-
function claimed(bytes32 id) public view returns (bool) {
43-
return _claimed[id];
39+
function processed(bytes32 id) public view returns (bool) {
40+
return _processed[id];
4441
}
4542

4643
/// @inheritdoc IETHBridge
@@ -49,8 +46,13 @@ contract ETHBridge is IETHBridge, ReentrancyGuardTransient {
4946
}
5047

5148
/// @inheritdoc IETHBridge
52-
function deposit(address to, bytes memory data, bytes memory context) public payable returns (bytes32 id) {
53-
ETHDeposit memory ethDeposit = ETHDeposit(_globalDepositNonce, msg.sender, to, msg.value, data, context);
49+
function deposit(address to, bytes memory data, bytes memory context, address canceler)
50+
public
51+
payable
52+
returns (bytes32 id)
53+
{
54+
ETHDeposit memory ethDeposit =
55+
ETHDeposit(_globalDepositNonce, msg.sender, to, msg.value, data, context, canceler);
5456
id = _generateId(ethDeposit);
5557
unchecked {
5658
++_globalDepositNonce;
@@ -62,15 +64,36 @@ contract ETHBridge is IETHBridge, ReentrancyGuardTransient {
6264

6365
/// @inheritdoc IETHBridge
6466
function claimDeposit(ETHDeposit memory ethDeposit, uint256 height, bytes memory proof) external nonReentrant {
65-
bytes32 id = _generateId(ethDeposit);
66-
require(!claimed(id), AlreadyClaimed());
67+
bytes32 id = _claimDeposit(ethDeposit, ethDeposit.to, ethDeposit.data, height, proof);
68+
emit DepositClaimed(id, ethDeposit);
69+
}
6770

68-
signalService.verifySignal(height, trustedCommitmentPublisher, counterpart, id, proof);
71+
/// @inheritdoc IETHBridge
72+
function cancelDeposit(ETHDeposit memory ethDeposit, address claimee, uint256 height, bytes memory proof)
73+
external
74+
nonReentrant
75+
{
76+
require(msg.sender == ethDeposit.canceler, OnlyCanceler());
6977

70-
_claimed[id] = true;
71-
_sendETH(ethDeposit.to, ethDeposit.amount, ethDeposit.data);
78+
bytes32 id = _claimDeposit(ethDeposit, claimee, bytes(""), height, proof);
7279

73-
emit DepositClaimed(id, ethDeposit);
80+
emit DepositCancelled(id, claimee);
81+
}
82+
83+
function _claimDeposit(
84+
ETHDeposit memory ethDeposit,
85+
address to,
86+
bytes memory data,
87+
uint256 height,
88+
bytes memory proof
89+
) internal returns (bytes32 id) {
90+
id = _generateId(ethDeposit);
91+
require(!processed(id), AlreadyClaimed());
92+
93+
signalService.verifySignal(height, trustedCommitmentPublisher, counterpart, id, proof);
94+
95+
_processed[id] = true;
96+
_sendETH(to, ethDeposit.amount, data);
7497
}
7598

7699
/// @dev Function to transfer ETH to the receiver but ignoring the returndata.

src/protocol/IETHBridge.sol

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,9 @@ interface IETHBridge {
1919
bytes data;
2020
// Application-specific context data (e.g., for relayer selection, tips, etc.)
2121
bytes context;
22+
// Address that is allowed to cancel the deposit on the destination chain (zero address means deposit is
23+
// uncancellable)
24+
address canceler;
2225
}
2326

2427
/// @dev Emitted when a deposit is made.
@@ -31,15 +34,23 @@ interface IETHBridge {
3134
/// @param deposit The claimed ETH deposit
3235
event DepositClaimed(bytes32 indexed id, ETHDeposit deposit);
3336

37+
/// @dev Emitted when a deposit is cancelled.
38+
/// @param id The deposit id
39+
/// @param claimee The address that received the cancelled deposit
40+
event DepositCancelled(bytes32 indexed id, address claimee);
41+
3442
/// @dev Failed to call the receiver with value.
3543
error FailedClaim();
3644

3745
/// @dev A deposit was already claimed.
3846
error AlreadyClaimed();
3947

40-
/// @dev Whether the deposit identified by `id` has been claimed.
48+
/// @dev Only canceler can cancel a deposit.
49+
error OnlyCanceler();
50+
51+
/// @dev Whether the deposit identified by `id` has been claimed or cancelled.
4152
/// @param id The deposit id
42-
function claimed(bytes32 id) external view returns (bool);
53+
function processed(bytes32 id) external view returns (bool);
4354

4455
/// @dev ETH Deposit identifier.
4556
/// @param ethDeposit The ETH deposit struct
@@ -49,12 +60,25 @@ interface IETHBridge {
4960
/// @param to The receiver of the deposit
5061
/// @param data Any calldata to be sent to the receiver in case of a contract
5162
/// @param context Application-specific context data
52-
function deposit(address to, bytes memory data, bytes memory context) external payable returns (bytes32 id);
63+
/// @param canceler Address on the destination chain that is allowed to cancel the deposit (zero address means
64+
/// deposit is uncancellable)
65+
function deposit(address to, bytes memory data, bytes memory context, address canceler)
66+
external
67+
payable
68+
returns (bytes32 id);
5369

5470
/// @dev Claims an ETH deposit created by the sender (`from`) with `nonce`. The `value` ETH claimed is
5571
/// sent to the receiver (`to`) after verifying a storage proof.
5672
/// @param ethDeposit The ETH deposit struct
5773
/// @param height The `height` of the checkpoint on the source chain (i.e. the block number or publicationId)
5874
/// @param proof Encoded proof of the storage slot where the deposit is stored
5975
function claimDeposit(ETHDeposit memory ethDeposit, uint256 height, bytes memory proof) external;
76+
77+
/// @dev Initiates a cancel on the deposit, must be called by the canceler on the destination chain.
78+
/// @param ethDeposit The ETH deposit struct
79+
/// @param claimee The address that will receive the cancelled deposit
80+
/// @param height The `height` of the checkpoint on the source chain (i.e. the block number or publicationId)
81+
/// @param proof Encoded proof of the storage slot where the deposit is stored
82+
function cancelDeposit(ETHDeposit memory ethDeposit, address claimee, uint256 height, bytes memory proof)
83+
external;
6084
}

test/ETHBridge/ClaimableScenarios.t.sol

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,9 @@ abstract contract DepositIsClaimable is CrossChainDepositExists {
1717
bytes memory proof = abi.encode(sampleDepositProof.getDepositSignalProof(_depositIdx()));
1818
(, bytes32 id) = sampleDepositProof.getDepositInternals(_depositIdx());
1919

20-
assertFalse(bridge.claimed(id), "deposit already marked as claimed");
20+
assertFalse(bridge.processed(id), "deposit already marked as claimed");
2121
bridge.claimDeposit(deposit, HEIGHT, proof);
22-
assertTrue(bridge.claimed(id), "deposit not marked as claimed");
22+
assertTrue(bridge.processed(id), "deposit not marked as claimed");
2323
}
2424

2525
function test_claimDeposit_shouldEmitEvent() public {

test/ETHBridge/InitialState.t.sol

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,9 @@ 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);
20+
1821
address trustedCommitmentPublisher = _randomAddress("trustedCommitmentPublisher");
1922

2023
function setUp() public virtual {

0 commit comments

Comments
 (0)