From 384fb11e7d458067c7ef2868f4b39d80dd055235 Mon Sep 17 00:00:00 2001 From: Henry <11198460+godzillaba@users.noreply.github.com> Date: Wed, 4 Dec 2024 10:38:14 -0500 Subject: [PATCH 1/2] parent and child insurance stubs --- src/insurance/Child.sol | 45 +++++++++++++++++++++++++++++++++++ src/insurance/Parent.sol | 51 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 96 insertions(+) create mode 100644 src/insurance/Child.sol create mode 100644 src/insurance/Parent.sol diff --git a/src/insurance/Child.sol b/src/insurance/Child.sol new file mode 100644 index 000000000..f4c404442 --- /dev/null +++ b/src/insurance/Child.sol @@ -0,0 +1,45 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.0; + +import {AddressAliasHelper} from "../libraries/AddressAliasHelper.sol"; + +// assumes parent and child chain uses ETH for fees +// EVERY FUNCTION MUST NEVER REVERT WHEN CALLED BY PARENT IN SEQUENCE, OTHERWISE QUEUE IS STUCK +contract Child { + address public immutable parentChainAddr; + address public immutable sequencerAddr; + uint256 public sequenceNumber; + uint256 public amtSatisfied; + + constructor(address _parentChainAddr, address _sequencerAddr) { + parentChainAddr = _parentChainAddr; + sequencerAddr = _sequencerAddr; + } + + modifier onlyInSequenceFromParent(uint256 seqNum) { + require(seqNum == sequenceNumber, "invalid sequence number"); + require(msg.sender == AddressAliasHelper.applyL1ToL2Alias(parentChainAddr), "only parent chain contract can call"); + sequenceNumber++; + _; + } + + function deposit(uint256 seqNum) public payable onlyInSequenceFromParent(seqNum) { + // no need to do anything here, just receiving ETH + } + + function withdraw(uint256 seqNum, uint256 amount) public onlyInSequenceFromParent(seqNum) { + amount = amount < address(this).balance ? amount : address(this).balance; + (bool success,) = payable(sequencerAddr).call{value: amount}(""); + require(success, "withdraw failed"); + } + + // If (blocknum, blockhash) is in the history of Chain X, then add amount to S. Otherwise pay out amount to beneficiaryAddr. + // will revert if arb block num < blockNum + function commit(uint256 seqNum, address beneficiary, uint256 amount, uint256 blockNum, bytes32 blockHash) external payable onlyInSequenceFromParent(seqNum) { + // revert if arb block num < blockNum + // pay msg.value to sequencer + // check if blockHash is part of history + // if yes: increment amtSatisfied (S) + // if no: pay out amount to beneficiary. do not revert on failure, because a contract could DoS + } +} diff --git a/src/insurance/Parent.sol b/src/insurance/Parent.sol new file mode 100644 index 000000000..70e07ffde --- /dev/null +++ b/src/insurance/Parent.sol @@ -0,0 +1,51 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.0; + +// assumes parent and child chain uses ETH for fees +contract Parent { + struct SequencerCommitment { + uint256 blockNum; + bytes32 blockHash; + uint256 pricePerEthWad; + } + + address public immutable sequencer; + + // D + uint256 public depositedAmount; + // I - insurance sold + uint256 public insuranceSold; + + // sequence number sigma + uint256 public sequenceNumber; + + constructor(address _sequencer) { + sequencer = _sequencer; + } + + modifier onlySequencer() { + require(msg.sender == sequencer, "only sequencer can call"); + _; + } + + function deposit() public payable onlySequencer { + // increment depositedAmount by value + // create a retryable to hit the child contract deposit function + // increment seqNum + } + + function withdraw(uint256 amount) public onlySequencer { + // decrement depositedAmount by amount + // create a retryable to hit the child contract withdraw function + // increment seqNum + } + + function buy(uint256 amount, uint256 minSatisfied, address beneficiary, bytes memory signedCommitment) public payable { + // verify the signed commitment + // require depositAmount - insuranceSold + minSatisfied >= amount + // require msg.value == price*amount + // increment insuranceSold by amount + // create a retryable to hit the child contract settle function, sending msg.value + // increment seqNum + } +} \ No newline at end of file From 0a73ccde3f2b6659b59694bd01b49241fae4e710 Mon Sep 17 00:00:00 2001 From: Henry <11198460+godzillaba@users.noreply.github.com> Date: Wed, 4 Dec 2024 11:40:33 -0500 Subject: [PATCH 2/2] implement buy --- src/insurance/Child.sol | 14 ++++- src/insurance/Parent.sol | 120 ++++++++++++++++++++++++++++++++++++--- 2 files changed, 123 insertions(+), 11 deletions(-) diff --git a/src/insurance/Child.sol b/src/insurance/Child.sol index f4c404442..2ef3b0064 100644 --- a/src/insurance/Child.sol +++ b/src/insurance/Child.sol @@ -16,9 +16,13 @@ contract Child { sequencerAddr = _sequencerAddr; } + // gates every function modifier onlyInSequenceFromParent(uint256 seqNum) { require(seqNum == sequenceNumber, "invalid sequence number"); - require(msg.sender == AddressAliasHelper.applyL1ToL2Alias(parentChainAddr), "only parent chain contract can call"); + require( + msg.sender == AddressAliasHelper.applyL1ToL2Alias(parentChainAddr), + "only parent chain contract can call" + ); sequenceNumber++; _; } @@ -35,7 +39,13 @@ contract Child { // If (blocknum, blockhash) is in the history of Chain X, then add amount to S. Otherwise pay out amount to beneficiaryAddr. // will revert if arb block num < blockNum - function commit(uint256 seqNum, address beneficiary, uint256 amount, uint256 blockNum, bytes32 blockHash) external payable onlyInSequenceFromParent(seqNum) { + function commit( + uint256 seqNum, + address beneficiary, + uint256 amount, + uint256 blockNum, + bytes32 blockHash + ) external payable onlyInSequenceFromParent(seqNum) { // revert if arb block num < blockNum // pay msg.value to sequencer // check if blockHash is part of history diff --git a/src/insurance/Parent.sol b/src/insurance/Parent.sol index 70e07ffde..941127b85 100644 --- a/src/insurance/Parent.sol +++ b/src/insurance/Parent.sol @@ -1,16 +1,39 @@ // SPDX-License-Identifier: BUSL-1.1 -pragma solidity ^0.8.0; +pragma solidity ^0.8.13; + +import "@openzeppelin/contracts/utils/cryptography/draft-EIP712.sol"; +import {IInbox} from "../bridge/IInbox.sol"; +import {Child} from "./Child.sol"; // assumes parent and child chain uses ETH for fees -contract Parent { +contract Parent is EIP712 { struct SequencerCommitment { + // should be unique + uint256 nonce; + // commitments are unique to a beneficiary. specified here + address beneficiary; + // amount of insurance to sell + uint256 insuranceAmount; + // cost of insurance, paid by the buyer + uint256 insuranceCost; + // block number the sequencer is committing to uint256 blockNum; + // block hash the sequencer is committing to bytes32 blockHash; - uint256 pricePerEthWad; } + struct RetryableParams { + uint256 maxSubmissionCost; + uint256 gasLimit; + uint256 gasPrice; + } + + IInbox public immutable inbox; + address public immutable sequencer; + address public immutable childChainContract; + // D uint256 public depositedAmount; // I - insurance sold @@ -19,8 +42,12 @@ contract Parent { // sequence number sigma uint256 public sequenceNumber; - constructor(address _sequencer) { + mapping(bytes32 => bool) public usedCommitments; + + constructor(address _sequencer, IInbox _inbox, address _childChainContract) EIP712("Parent", "1") { sequencer = _sequencer; + inbox = _inbox; + childChainContract = _childChainContract; } modifier onlySequencer() { @@ -40,12 +67,87 @@ contract Parent { // increment seqNum } - function buy(uint256 amount, uint256 minSatisfied, address beneficiary, bytes memory signedCommitment) public payable { - // verify the signed commitment - // require depositAmount - insuranceSold + minSatisfied >= amount - // require msg.value == price*amount + // todo: amount and beneficiary should be part of the commitment + // it's possible to frontrun the buyer and burn the commitment they got from the sequencer. + // This has little cost to the victim, but is annoying since they'll have to go get another commitment and might pay some unnecessary gas. + function buy( + uint256 minSatisfied, + SequencerCommitment calldata commitment, + bytes calldata signature, + RetryableParams calldata retryableParams + ) public payable { + // require depositAmount - insuranceSold + minSatisfied >= insuranceAmount + // put this check first because it's the mosy likely to fail without user error + require(depositedAmount - insuranceSold + minSatisfied >= commitment.insuranceAmount, "potential undercollateralization"); + + // verify the signature + require(isValidSignature(commitment, signature), "invalid signature"); + + // require msg.value == insuranceCost + retryableCost + uint256 retryableCost = retryableParams.maxSubmissionCost + retryableParams.gasLimit * retryableParams.gasPrice; + require(msg.value == retryableCost + commitment.insuranceCost, "invalid value"); + + // require !hasUsedCommitment(commitment) + require(!hasUsedCommitment(commitment), "commitment already used"); + // increment insuranceSold by amount + insuranceSold += commitment.insuranceAmount; + + // mark commitment as used + usedCommitments[hashCommitment(commitment)] = true; + + // build calldata for child contract + bytes memory data = abi.encodeCall( + Child.commit, + ( + sequenceNumber, + commitment.beneficiary, + commitment.insuranceAmount, + commitment.blockNum, + commitment.blockHash + ) + ); + // create a retryable to hit the child contract settle function, sending msg.value + inbox.createRetryableTicket{value: msg.value}({ + to: childChainContract, + l2CallValue: commitment.insuranceCost, + maxSubmissionCost: retryableParams.maxSubmissionCost, + excessFeeRefundAddress: msg.sender, + callValueRefundAddress: msg.sender, + gasLimit: retryableParams.gasLimit, + maxFeePerGas: retryableParams.gasPrice, + data: data + }); + // increment seqNum + sequenceNumber++; + } + + function isValidSignature(SequencerCommitment calldata commitment, bytes calldata signature) + public + view + returns (bool) + { + bytes32 digest = _hashTypedDataV4( + keccak256( + abi.encode( + keccak256( + "SequencerCommitment(uint256 nonce,uint256 blockNum,bytes32 blockHash,uint256 pricePerEthWad)" + ), + commitment + ) + ) + ); + address signer = ECDSA.recover(digest, signature); + return signer == sequencer; + } + + function hasUsedCommitment(SequencerCommitment calldata commitment) public view returns (bool) { + return usedCommitments[hashCommitment(commitment)]; + } + + function hashCommitment(SequencerCommitment calldata commitment) public pure returns (bytes32) { + return keccak256(abi.encode(commitment)); } -} \ No newline at end of file +}