Skip to content

Commit

Permalink
Merge pull request #78 from primevprotocol/standard-bridge
Browse files Browse the repository at this point in the history
feat: standard bridge contracts
  • Loading branch information
shaspitz authored Jan 29, 2024
2 parents 26b3ce8 + be55c8b commit 34414db
Show file tree
Hide file tree
Showing 7 changed files with 464 additions and 0 deletions.
7 changes: 7 additions & 0 deletions contracts/interfaces/IWhitelist.sol
Original file line number Diff line number Diff line change
@@ -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;
}
78 changes: 78 additions & 0 deletions contracts/standard-bridge/Gateway.sol
Original file line number Diff line number Diff line change
@@ -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);
}
23 changes: 23 additions & 0 deletions contracts/standard-bridge/L1Gateway.sol
Original file line number Diff line number Diff line change
@@ -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 {}
}

30 changes: 30 additions & 0 deletions contracts/standard-bridge/SettlementGateway.sol
Original file line number Diff line number Diff line change
@@ -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);
}
}
72 changes: 72 additions & 0 deletions scripts/DeployStandardBridge.s.sol
Original file line number Diff line number Diff line change
@@ -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();
}
}
115 changes: 115 additions & 0 deletions test/standard-bridge/L1GatewayTest.sol
Original file line number Diff line number Diff line change
@@ -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);
}
}
Loading

0 comments on commit 34414db

Please sign in to comment.