diff --git a/contracts/interfaces/IWhitelist.sol b/contracts/interfaces/IWhitelist.sol new file mode 100644 index 0000000..1b8db76 --- /dev/null +++ b/contracts/interfaces/IWhitelist.sol @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: BSL 1.1 +pragma solidity ^0.8.15; + +interface IWhitelist { + function mint(address _mintTo, uint256 _amount) external; + function burn(address _burnFrom, uint256 _amount) external; +} diff --git a/contracts/standard-bridge/Gateway.sol b/contracts/standard-bridge/Gateway.sol new file mode 100644 index 0000000..165768f --- /dev/null +++ b/contracts/standard-bridge/Gateway.sol @@ -0,0 +1,78 @@ +// SPDX-License-Identifier: BSL 1.1 +pragma solidity ^0.8.15; + +import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; + +/** + * @dev Gateway contract for standard bridge. + */ +abstract contract Gateway is Ownable { + + // @dev index for tracking transfers. + // Also total number of transfers initiated from this gateway. + uint256 public transferIdx; + + // @dev Address of relayer account. + address public immutable relayer; + + // @dev Flat fee (wei) paid to relayer on destination chain upon transfer finalization. + // This must be greater than what relayer will pay per tx. + uint256 public immutable finalizationFee; + + // The counterparty's finalization fee (wei), included for UX purposes + uint256 public immutable counterpartyFee; + + constructor(address _owner, address _relayer, + uint256 _finalizationFee, uint256 _counterpartyFee) Ownable() { + relayer = _relayer; + finalizationFee = _finalizationFee; + counterpartyFee = _counterpartyFee; + _transferOwnership(_owner); + } + + function initiateTransfer(address _recipient, uint256 _amount + ) external payable returns (uint256 returnIdx) { + require(_amount >= counterpartyFee, "Amount must cover counterpartys finalization fee"); + ++transferIdx; + _decrementMsgSender(_amount); + emit TransferInitiated(msg.sender, _recipient, _amount, transferIdx); + return transferIdx; + } + // @dev where _decrementMsgSender is implemented by inheriting contract. + function _decrementMsgSender(uint256 _amount) internal virtual; + + modifier onlyRelayer() { + require(msg.sender == relayer, "Only relayer can call this function"); + _; + } + + function finalizeTransfer(address _recipient, uint256 _amount, uint256 _counterpartyIdx + ) external onlyRelayer { + require(_amount >= finalizationFee, "Amount must cover finalization fee"); + uint256 amountAfterFee = _amount - finalizationFee; + _fund(amountAfterFee, _recipient); + _fund(finalizationFee, relayer); + emit TransferFinalized(_recipient, _amount, _counterpartyIdx); + } + // @dev where _fund is implemented by inheriting contract. + function _fund(uint256 _amount, address _toFund) internal virtual; + + /** + * @dev Emitted when a cross chain transfer is initiated. + * @param sender Address initiating the transfer. Indexed for efficient filtering. + * @param recipient Address receiving the tokens. Indexed for efficient filtering. + * @param amount Ether being transferred in wei. + * @param transferIdx Current index of this gateway. + */ + event TransferInitiated( + address indexed sender, address indexed recipient, uint256 amount, uint256 transferIdx); + + /** + * @dev Emitted when a transfer is finalized. + * @param recipient Address receiving the tokens. Indexed for efficient filtering. + * @param amount Ether being transferred in wei. + * @param counterpartyIdx Index of counterpary gateway when transfer was initiated. + */ + event TransferFinalized( + address indexed recipient, uint256 amount, uint256 counterpartyIdx); +} diff --git a/contracts/standard-bridge/L1Gateway.sol b/contracts/standard-bridge/L1Gateway.sol new file mode 100644 index 0000000..1c0faf0 --- /dev/null +++ b/contracts/standard-bridge/L1Gateway.sol @@ -0,0 +1,23 @@ +// SPDX-License-Identifier: BSL 1.1 +pragma solidity ^0.8.15; + +import {Gateway} from "./Gateway.sol"; + +contract L1Gateway is Gateway { + + constructor(address _owner, address _relayer, uint256 _finalizationFee, uint256 _counterpartyFee + ) Gateway(_owner, _relayer, _finalizationFee, _counterpartyFee) {} + + function _decrementMsgSender(uint256 _amount) internal override { + require(msg.value == _amount, "Incorrect Ether value sent"); + // Wrapping function initiateTransfer is payable. Ether is escrowed in contract balance + } + + function _fund(uint256 _amount, address _toFund) internal override { + require(address(this).balance >= _amount, "Insufficient contract balance"); + payable(_toFund).transfer(_amount); + } + + receive() external payable {} +} + diff --git a/contracts/standard-bridge/SettlementGateway.sol b/contracts/standard-bridge/SettlementGateway.sol new file mode 100644 index 0000000..f2b23f8 --- /dev/null +++ b/contracts/standard-bridge/SettlementGateway.sol @@ -0,0 +1,30 @@ +// SPDX-License-Identifier: BSL 1.1 +pragma solidity ^0.8.15; + +import {Gateway} from "./Gateway.sol"; +import {IWhitelist} from "../interfaces/IWhitelist.sol"; + +contract SettlementGateway is Gateway{ + + // Assuming deployer is 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266, + // whitelist's create2 addr should be 0x5D1415C0973034d162F5FEcF19B50dA057057e29. + // This variable is not hardcoded for testing purposes. + address public immutable whitelistAddr; + + constructor(address _whitelistAddr, address _owner, address _relayer, uint256 _finalizationFee, uint256 _counterpartyFee + ) Gateway(_owner, _relayer, _finalizationFee, _counterpartyFee) { + whitelistAddr = _whitelistAddr; + } + + // Burns native ether on settlement chain, + // there should be equiv ether on L1 which will be UNLOCKED during finalization. + function _decrementMsgSender(uint256 _amount) internal override { + IWhitelist(whitelistAddr).burn(msg.sender, _amount); + } + + // Mints native ether on settlement chain, + // there should be equiv ether on L1 which remains LOCKED. + function _fund(uint256 _amount, address _toFund) internal override { + IWhitelist(whitelistAddr).mint(_toFund, _amount); + } +} diff --git a/scripts/DeployStandardBridge.s.sol b/scripts/DeployStandardBridge.s.sol new file mode 100644 index 0000000..9ccbc07 --- /dev/null +++ b/scripts/DeployStandardBridge.s.sol @@ -0,0 +1,72 @@ +// SPDX-License-Identifier: BSL 1.1 +pragma solidity ^0.8.15; +import "forge-std/Script.sol"; +import {Create2Deployer} from "scripts/DeployScripts.s.sol"; +import {SettlementGateway} from "contracts/standard-bridge/SettlementGateway.sol"; +import {L1Gateway} from "contracts/standard-bridge/L1Gateway.sol"; + +contract DeploySettlementGateway is Script, Create2Deployer { + function run() external { + + // Note this addr is dependant on values given to contract constructor + address expectedAddr = 0x0D70A44c81a27f33a36C334bFEA8bBBD8A7d58AA; + if (isContractDeployed(expectedAddr)) { + console.log("Standard bridge gateway on settlement chain already deployed to:", + expectedAddr); + return; + } + + vm.startBroadcast(); + + checkCreate2Deployed(); + checkDeployer(); + + // Forge deploy with salt uses create2 proxy from https://github.com/primevprotocol/deterministic-deployment-proxy + bytes32 salt = 0x8989000000000000000000000000000000000000000000000000000000000000; + + address whitelistAddr = 0x5D1415C0973034d162F5FEcF19B50dA057057e29; + address relayerAddr = vm.envAddress("RELAYER_ADDR"); + + SettlementGateway gateway = new SettlementGateway{salt: salt}( + whitelistAddr, + msg.sender, // Owner + relayerAddr, + 1, 1); // Fees set to 1 wei for now + console.log("Standard bridge gateway for settlement chain deployed to:", + address(gateway)); + + vm.stopBroadcast(); + } +} + +contract DeployL1Gateway is Script, Create2Deployer { + function run() external { + + // Note this addr is dependant on values given to contract constructor + address expectedAddr = 0x38b7e046bd971B4123974Bc78DcB0D7C680d85d2; + if (isContractDeployed(expectedAddr)) { + console.log("Standard bridge gateway on l1 already deployed to:", + expectedAddr); + return; + } + + vm.startBroadcast(); + + checkCreate2Deployed(); + checkDeployer(); + + // Forge deploy with salt uses create2 proxy from https://github.com/primevprotocol/deterministic-deployment-proxy + bytes32 salt = 0x8989000000000000000000000000000000000000000000000000000000000000; + + address relayerAddr = vm.envAddress("RELAYER_ADDR"); + + L1Gateway gateway = new L1Gateway{salt: salt}( + msg.sender, // Owner + relayerAddr, + 1, 1); // Fees set to 1 wei for now + console.log("Standard bridge gateway for l1 deployed to:", + address(gateway)); + + vm.stopBroadcast(); + } +} diff --git a/test/standard-bridge/L1GatewayTest.sol b/test/standard-bridge/L1GatewayTest.sol new file mode 100644 index 0000000..c131498 --- /dev/null +++ b/test/standard-bridge/L1GatewayTest.sol @@ -0,0 +1,115 @@ +// SPDX-License-Identifier: BSL 1.1 +pragma solidity ^0.8.15; + +import "forge-std/Test.sol"; +import "../../contracts/standard-bridge/L1Gateway.sol"; + +contract L1GatewayTest is Test { + L1Gateway l1Gateway; + address owner; + address relayer; + address bridgeUser; + uint256 finalizationFee; + uint256 counterpartyFee; + + function setUp() public { + owner = address(this); // Original contract deployer as owner + relayer = address(0x78); + bridgeUser = address(0x101); + finalizationFee = 0.1 ether; + counterpartyFee = 0.05 ether; + l1Gateway = new L1Gateway(owner, relayer, finalizationFee, counterpartyFee); + } + + function test_ConstructorSetsVariablesCorrectly() public { + assertEq(l1Gateway.owner(), owner); + assertEq(l1Gateway.relayer(), relayer); + assertEq(l1Gateway.finalizationFee(), finalizationFee); + assertEq(l1Gateway.counterpartyFee(), counterpartyFee); + } + + // Expected event signature emitted in initiateTransfer() + event TransferInitiated( + address indexed sender, address indexed recipient, uint256 amount, uint256 transferIdx); + + function test_InitiateTransfer() public { + vm.deal(bridgeUser, 100 ether); + uint256 amount = 7 ether; + + // Initial assertions + assertEq(address(bridgeUser).balance, 100 ether); + assertEq(l1Gateway.transferIdx(), 0); + + // Set up expectation for event + vm.expectEmit(true, true, true, true); + emit TransferInitiated(bridgeUser, bridgeUser, amount, 1); + + // Call function as bridgeUser + vm.prank(bridgeUser); + uint256 returnedIdx = l1Gateway.initiateTransfer{value: amount}(bridgeUser, amount); + + // Assertions after call + assertEq(address(bridgeUser).balance, 93 ether); + assertEq(l1Gateway.transferIdx(), 1); + assertEq(returnedIdx, 1); + } + + function TestAmountTooSmallForCounterpartyFee() public { + vm.deal(bridgeUser, 100 ether); + vm.deal(address(l1Gateway), 1 ether); + assertEq(address(bridgeUser).balance, 100 ether); + vm.expectRevert("Amount must cover counterpartys finalization fee"); + vm.prank(bridgeUser); + l1Gateway.initiateTransfer{value: 0.04 ether}(bridgeUser, 0.04 ether); + } + + event TransferFinalized( + address indexed recipient, uint256 amount, uint256 counterpartyIdx); + + function test_FinalizeTransfer() public { + // These values are trusted from relayer + uint256 amount = 4 ether; + uint256 counterpartyIdx = 1; + + // Fund gateway and relayer + vm.deal(address(l1Gateway), 5 ether); + vm.deal(relayer, 5 ether); + + // Initial assertions + assertEq(address(l1Gateway).balance, 5 ether); + assertEq(relayer.balance, 5 ether); + assertEq(bridgeUser.balance, 0 ether); + assertEq(l1Gateway.transferIdx(), 0); + + // Set up expectation for event + vm.expectEmit(true, true, true, true); + emit TransferFinalized(bridgeUser, amount, counterpartyIdx); + + // Call function as relayer + vm.prank(relayer); + l1Gateway.finalizeTransfer(bridgeUser, amount, counterpartyIdx); + + // Finalization fee is 0.1 ether + assertEq(address(l1Gateway).balance, 1 ether); + assertEq(relayer.balance, 5.1 ether); + assertEq(bridgeUser.balance, 3.9 ether); + assertEq(l1Gateway.transferIdx(), 0); + } + + function test_OnlyRelayerCanCallFinalizeTransfer() public { + uint256 amount = 0.1 ether; + vm.deal(address(l1Gateway), 1 ether); + vm.expectRevert("Only relayer can call this function"); + vm.prank(bridgeUser); + l1Gateway.finalizeTransfer(address(0x101), amount, 1); + } + + // This scenario shouldn't be possible since initiateTransfer() should have prevented it. + function test_AmountTooSmallForFinalizationFee() public { + uint256 amount = 0.09 ether; + vm.deal(address(l1Gateway), 1 ether); + vm.expectRevert("Amount must cover finalization fee"); + vm.prank(relayer); + l1Gateway.finalizeTransfer(address(0x101), amount, 1); + } +} diff --git a/test/standard-bridge/SettlementGatewayTest.sol b/test/standard-bridge/SettlementGatewayTest.sol new file mode 100644 index 0000000..3b17e63 --- /dev/null +++ b/test/standard-bridge/SettlementGatewayTest.sol @@ -0,0 +1,139 @@ +// SPDX-License-Identifier: BSL 1.1 +pragma solidity ^0.8.15; + +import "forge-std/Test.sol"; +import "../../contracts/standard-bridge/SettlementGateway.sol"; +import "../../contracts/interfaces/IWhitelist.sol"; + +contract MockWhitelist is IWhitelist { + uint256 public mintIdx; + address public lastMintTo; + uint256 public lastMintAmount; + + uint256 public burnIdx; + address public lastBurnFrom; + uint256 public lastBurnAmount; + + function mint(address _to, uint256 _amount) external override { + ++mintIdx; + } + + function burn(address _from, uint256 _amount) external override { + ++burnIdx; + lastBurnFrom = _from; + lastBurnAmount = _amount; + } +} + +contract SettlementGatewayTest is Test { + + SettlementGateway settlementGateway; + MockWhitelist mockWhitelist; + + address owner; + address relayer; + address bridgeUser; + uint256 finalizationFee; + uint256 counterpartyFee; + + function setUp() public { + owner = address(this); // Original contract deployer as owner + relayer = address(0x78); + bridgeUser = address(0x101); + finalizationFee = 0.05 ether; + counterpartyFee = 0.1 ether; + mockWhitelist = new MockWhitelist(); + settlementGateway = new SettlementGateway(address(mockWhitelist), owner, relayer, finalizationFee, counterpartyFee); + } + + function test_ConstructorSetsVariablesCorrectly() public { + // Test if the constructor correctly initializes variables + assertEq(settlementGateway.owner(), owner); + assertEq(settlementGateway.relayer(), relayer); + assertEq(settlementGateway.finalizationFee(), finalizationFee); + assertEq(settlementGateway.counterpartyFee(), counterpartyFee); + assertEq(settlementGateway.whitelistAddr(), address(mockWhitelist)); + } + + // Expected event signature emitted in initiateTransfer() + event TransferInitiated( + address indexed sender, address indexed recipient, uint256 amount, uint256 transferIdx); + + function test_InitiateTransfer() public { + vm.deal(bridgeUser, 100 ether); + uint256 amount = 7 ether; + + // Initial assertions + assertEq(address(bridgeUser).balance, 100 ether); + assertEq(settlementGateway.transferIdx(), 0); + + // Set up expectation for event + vm.expectEmit(true, true, true, true); + emit TransferInitiated(bridgeUser, bridgeUser, amount, 1); + + // Call function as bridgeUser + vm.prank(bridgeUser); + uint256 returnedIdx = settlementGateway.initiateTransfer{value: amount}(bridgeUser, amount); + + // Assertions after call + assertEq(mockWhitelist.burnIdx(), 1); + assertEq(mockWhitelist.lastBurnFrom(), bridgeUser); + assertEq(mockWhitelist.lastBurnAmount(), amount); + + assertEq(settlementGateway.transferIdx(), 1); + assertEq(returnedIdx, 1); + } + + function TestAmountTooSmallForCounterpartyFee() public { + vm.deal(bridgeUser, 100 ether); + vm.deal(address(settlementGateway), 1 ether); + assertEq(address(bridgeUser).balance, 100 ether); + vm.expectRevert("Amount must cover counterpartys finalization fee"); + vm.prank(bridgeUser); + settlementGateway.initiateTransfer{value: 0.04 ether}(bridgeUser, 0.04 ether); + } + + event TransferFinalized( + address indexed recipient, uint256 amount, uint256 counterpartyIdx); + + function test_FinalizeTransfer() public { + // These values are trusted from relayer + uint256 amount = 2 ether; + uint256 counterpartyIdx = 8; + + // Fund gateway and relayer + vm.deal(address(settlementGateway), 3 ether); + vm.deal(relayer, 3 ether); + + // Initial assertions + assertEq(address(settlementGateway).balance, 3 ether); + assertEq(relayer.balance, 3 ether); + assertEq(bridgeUser.balance, 0 ether); + assertEq(settlementGateway.transferIdx(), 0); + + // Set up expectation for event + vm.expectEmit(true, true, true, true); + emit TransferFinalized(bridgeUser, amount, counterpartyIdx); + + // Call function as relayer + vm.prank(relayer); + settlementGateway.finalizeTransfer(bridgeUser, amount, counterpartyIdx); + + assertEq(mockWhitelist.mintIdx(), 2); + assertEq(settlementGateway.transferIdx(), 0); + } + + function test_OnlyRelayerCanCallFinalizeTransfer() public { + vm.expectRevert("Only relayer can call this function"); + vm.prank(bridgeUser); + settlementGateway.finalizeTransfer(bridgeUser, 1 ether, 1); + } + + function test_AmountTooSmallForFinalizationFee() public { + vm.deal(address(settlementGateway), 1 ether); + vm.deal(relayer, 1 ether); + vm.expectRevert("Amount must cover finalization fee"); + vm.prank(relayer); + settlementGateway.finalizeTransfer(bridgeUser, 0.04 ether, 1); + } +}