-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #78 from primevprotocol/standard-bridge
feat: standard bridge contracts
- Loading branch information
Showing
7 changed files
with
464 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 {} | ||
} | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
} |
Oops, something went wrong.