-
Notifications
You must be signed in to change notification settings - Fork 32
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
139 changed files
with
13,120 additions
and
5,571 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
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
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
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 |
---|---|---|
|
@@ -3,6 +3,17 @@ | |
All notable changes to this project will be documented in this file. | ||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. | ||
|
||
# [0.4.0](https://github.com/synapsecns/sanguine/compare/[email protected]@0.4.0) (2024-09-20) | ||
|
||
|
||
### Features | ||
|
||
* **contracts-rfq:** relay/prove/claim with different address [SLT-130] ([#3138](https://github.com/synapsecns/sanguine/issues/3138)) ([23f6c4c](https://github.com/synapsecns/sanguine/commit/23f6c4c652743c5ca7a184ad730ce19af3600a9c)) | ||
|
||
|
||
|
||
|
||
|
||
# [0.3.0](https://github.com/synapsecns/sanguine/compare/[email protected]@0.3.0) (2024-09-10) | ||
|
||
|
||
|
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
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,287 @@ | ||
// SPDX-License-Identifier: MIT | ||
pragma solidity 0.8.24; | ||
|
||
import {SafeERC20, IERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; | ||
|
||
import "./libs/Errors.sol"; | ||
import {UniversalTokenLib} from "./libs/UniversalToken.sol"; | ||
|
||
import {Admin} from "./Admin.sol"; | ||
import {IFastBridge} from "./interfaces/IFastBridge.sol"; | ||
import {IFastBridgeV2} from "./interfaces/IFastBridgeV2.sol"; | ||
|
||
contract FastBridgeV2 is Admin, IFastBridgeV2 { | ||
using SafeERC20 for IERC20; | ||
using UniversalTokenLib for address; | ||
|
||
/// @notice Dispute period for relayed transactions | ||
uint256 public constant DISPUTE_PERIOD = 30 minutes; | ||
|
||
/// @notice Delay for a transaction after which it could be permisionlessly refunded | ||
uint256 public constant REFUND_DELAY = 7 days; | ||
|
||
/// @notice Minimum deadline period to relay a requested bridge transaction | ||
uint256 public constant MIN_DEADLINE_PERIOD = 30 minutes; | ||
|
||
enum BridgeStatus { | ||
NULL, // doesn't exist yet | ||
REQUESTED, | ||
RELAYER_PROVED, | ||
RELAYER_CLAIMED, | ||
REFUNDED | ||
} | ||
|
||
/// @notice Status of the bridge tx on origin chain | ||
mapping(bytes32 => BridgeStatus) public bridgeStatuses; | ||
/// @notice Proof of relayed bridge tx on origin chain | ||
mapping(bytes32 => BridgeProof) public bridgeProofs; | ||
/// @notice Whether bridge has been relayed on destination chain | ||
mapping(bytes32 => bool) public bridgeRelays; | ||
|
||
/// @dev to prevent replays | ||
uint256 public nonce; | ||
// @dev the block the contract was deployed at | ||
uint256 public immutable deployBlock; | ||
|
||
constructor(address _owner) Admin(_owner) { | ||
deployBlock = block.number; | ||
} | ||
|
||
/// @notice Pulls a requested token from the user to the requested recipient. | ||
/// @dev Be careful of re-entrancy issues when msg.value > 0 and recipient != address(this) | ||
function _pullToken(address recipient, address token, uint256 amount) internal returns (uint256 amountPulled) { | ||
if (token != UniversalTokenLib.ETH_ADDRESS) { | ||
token.assertIsContract(); | ||
// Record token balance before transfer | ||
amountPulled = IERC20(token).balanceOf(recipient); | ||
// Token needs to be pulled only if msg.value is zero | ||
// This way user can specify WETH as the origin asset | ||
IERC20(token).safeTransferFrom(msg.sender, recipient, amount); | ||
// Use the difference between the recorded balance and the current balance as the amountPulled | ||
amountPulled = IERC20(token).balanceOf(recipient) - amountPulled; | ||
} else { | ||
// Otherwise, we need to check that ETH amount matches msg.value | ||
if (amount != msg.value) revert MsgValueIncorrect(); | ||
// Transfer value to recipient if not this address | ||
if (recipient != address(this)) token.universalTransfer(recipient, amount); | ||
// We will forward msg.value in the external call later, if recipient is not this contract | ||
amountPulled = msg.value; | ||
} | ||
} | ||
|
||
/// @inheritdoc IFastBridge | ||
function getBridgeTransaction(bytes memory request) public pure returns (BridgeTransaction memory) { | ||
return abi.decode(request, (BridgeTransaction)); | ||
} | ||
|
||
/// @inheritdoc IFastBridge | ||
function bridge(BridgeParams memory params) external payable { | ||
// check bridge params | ||
if (params.dstChainId == block.chainid) revert ChainIncorrect(); | ||
if (params.originAmount == 0 || params.destAmount == 0) revert AmountIncorrect(); | ||
if (params.originToken == address(0) || params.destToken == address(0)) revert ZeroAddress(); | ||
if (params.deadline < block.timestamp + MIN_DEADLINE_PERIOD) revert DeadlineTooShort(); | ||
|
||
// transfer tokens to bridge contract | ||
// @dev use returned originAmount in request in case of transfer fees | ||
uint256 originAmount = _pullToken(address(this), params.originToken, params.originAmount); | ||
|
||
// track amount of origin token owed to protocol | ||
uint256 originFeeAmount; | ||
if (protocolFeeRate > 0) originFeeAmount = (originAmount * protocolFeeRate) / FEE_BPS; | ||
originAmount -= originFeeAmount; // remove from amount used in request as not relevant for relayers | ||
|
||
// set status to requested | ||
bytes memory request = abi.encode( | ||
BridgeTransaction({ | ||
originChainId: uint32(block.chainid), | ||
destChainId: params.dstChainId, | ||
originSender: params.sender, | ||
destRecipient: params.to, | ||
originToken: params.originToken, | ||
destToken: params.destToken, | ||
originAmount: originAmount, | ||
destAmount: params.destAmount, | ||
originFeeAmount: originFeeAmount, | ||
sendChainGas: params.sendChainGas, | ||
deadline: params.deadline, | ||
nonce: nonce++ // increment nonce on every bridge | ||
}) | ||
); | ||
bytes32 transactionId = keccak256(request); | ||
bridgeStatuses[transactionId] = BridgeStatus.REQUESTED; | ||
|
||
emit BridgeRequested( | ||
transactionId, | ||
params.sender, | ||
request, | ||
params.dstChainId, | ||
params.originToken, | ||
params.destToken, | ||
originAmount, | ||
params.destAmount, | ||
params.sendChainGas | ||
); | ||
} | ||
|
||
/// @inheritdoc IFastBridge | ||
function relay(bytes memory request) external payable { | ||
relay(request, msg.sender); | ||
} | ||
|
||
/// @inheritdoc IFastBridgeV2 | ||
function relay(bytes memory request, address relayer) public payable { | ||
bytes32 transactionId = keccak256(request); | ||
BridgeTransaction memory transaction = getBridgeTransaction(request); | ||
if (transaction.destChainId != uint32(block.chainid)) revert ChainIncorrect(); | ||
|
||
// check haven't exceeded deadline for relay to happen | ||
if (block.timestamp > transaction.deadline) revert DeadlineExceeded(); | ||
|
||
// mark bridge transaction as relayed | ||
if (bridgeRelays[transactionId]) revert TransactionRelayed(); | ||
bridgeRelays[transactionId] = true; | ||
|
||
// transfer tokens to recipient on destination chain and gas rebate if requested | ||
address to = transaction.destRecipient; | ||
address token = transaction.destToken; | ||
uint256 amount = transaction.destAmount; | ||
|
||
uint256 rebate = chainGasAmount; | ||
if (!transaction.sendChainGas) { | ||
// forward erc20 | ||
rebate = 0; | ||
_pullToken(to, token, amount); | ||
} else if (token == UniversalTokenLib.ETH_ADDRESS) { | ||
// lump in gas rebate into amount in native gas token | ||
_pullToken(to, token, amount + rebate); | ||
} else { | ||
// forward erc20 then forward gas rebate in native gas token | ||
_pullToken(to, token, amount); | ||
_pullToken(to, UniversalTokenLib.ETH_ADDRESS, rebate); | ||
} | ||
|
||
emit BridgeRelayed( | ||
transactionId, | ||
relayer, | ||
to, | ||
transaction.originChainId, | ||
transaction.originToken, | ||
transaction.destToken, | ||
transaction.originAmount, | ||
transaction.destAmount, | ||
rebate | ||
); | ||
} | ||
|
||
/// @inheritdoc IFastBridge | ||
function prove(bytes memory request, bytes32 destTxHash) external { | ||
prove(request, destTxHash, msg.sender); | ||
} | ||
|
||
/// @inheritdoc IFastBridgeV2 | ||
function prove(bytes memory request, bytes32 destTxHash, address relayer) public onlyRole(RELAYER_ROLE) { | ||
bytes32 transactionId = keccak256(request); | ||
// update bridge tx status given proof provided | ||
if (bridgeStatuses[transactionId] != BridgeStatus.REQUESTED) revert StatusIncorrect(); | ||
bridgeStatuses[transactionId] = BridgeStatus.RELAYER_PROVED; | ||
bridgeProofs[transactionId] = BridgeProof({timestamp: uint96(block.timestamp), relayer: relayer}); // overflow ok | ||
|
||
emit BridgeProofProvided(transactionId, relayer, destTxHash); | ||
} | ||
|
||
/// @notice Calculates time since proof submitted | ||
/// @dev proof.timestamp stores casted uint96(block.timestamp) block timestamps for gas optimization | ||
/// _timeSince(proof) can accomodate rollover case when block.timestamp > type(uint96).max but | ||
/// proof.timestamp < type(uint96).max via unchecked statement | ||
/// @param proof The bridge proof | ||
/// @return delta Time delta since proof submitted | ||
function _timeSince(BridgeProof memory proof) internal view returns (uint256 delta) { | ||
unchecked { | ||
delta = uint96(block.timestamp) - proof.timestamp; | ||
} | ||
} | ||
|
||
/// @inheritdoc IFastBridge | ||
function canClaim(bytes32 transactionId, address relayer) external view returns (bool) { | ||
if (bridgeStatuses[transactionId] != BridgeStatus.RELAYER_PROVED) revert StatusIncorrect(); | ||
BridgeProof memory proof = bridgeProofs[transactionId]; | ||
if (proof.relayer != relayer) revert SenderIncorrect(); | ||
return _timeSince(proof) > DISPUTE_PERIOD; | ||
} | ||
|
||
/// @inheritdoc IFastBridgeV2 | ||
function claim(bytes memory request) external { | ||
claim(request, address(0)); | ||
} | ||
|
||
/// @inheritdoc IFastBridge | ||
function claim(bytes memory request, address to) public { | ||
bytes32 transactionId = keccak256(request); | ||
BridgeTransaction memory transaction = getBridgeTransaction(request); | ||
|
||
// update bridge tx status if able to claim origin collateral | ||
if (bridgeStatuses[transactionId] != BridgeStatus.RELAYER_PROVED) revert StatusIncorrect(); | ||
|
||
BridgeProof memory proof = bridgeProofs[transactionId]; | ||
|
||
// if "to" is zero addr, permissionlessly send funds to proven relayer | ||
if (to == address(0)) { | ||
to = proof.relayer; | ||
} else if (proof.relayer != msg.sender) { | ||
revert SenderIncorrect(); | ||
} | ||
|
||
if (_timeSince(proof) <= DISPUTE_PERIOD) revert DisputePeriodNotPassed(); | ||
|
||
bridgeStatuses[transactionId] = BridgeStatus.RELAYER_CLAIMED; | ||
|
||
// update protocol fees if origin fee amount exists | ||
if (transaction.originFeeAmount > 0) protocolFees[transaction.originToken] += transaction.originFeeAmount; | ||
|
||
// transfer origin collateral less fee to specified address | ||
address token = transaction.originToken; | ||
uint256 amount = transaction.originAmount; | ||
token.universalTransfer(to, amount); | ||
|
||
emit BridgeDepositClaimed(transactionId, proof.relayer, to, token, amount); | ||
} | ||
|
||
/// @inheritdoc IFastBridge | ||
function dispute(bytes32 transactionId) external onlyRole(GUARD_ROLE) { | ||
if (bridgeStatuses[transactionId] != BridgeStatus.RELAYER_PROVED) revert StatusIncorrect(); | ||
if (_timeSince(bridgeProofs[transactionId]) > DISPUTE_PERIOD) revert DisputePeriodPassed(); | ||
|
||
// @dev relayer gets slashed effectively if dest relay has gone thru | ||
bridgeStatuses[transactionId] = BridgeStatus.REQUESTED; | ||
delete bridgeProofs[transactionId]; | ||
|
||
emit BridgeProofDisputed(transactionId, msg.sender); | ||
} | ||
|
||
/// @inheritdoc IFastBridge | ||
function refund(bytes memory request) external { | ||
bytes32 transactionId = keccak256(request); | ||
BridgeTransaction memory transaction = getBridgeTransaction(request); | ||
|
||
if (hasRole(REFUNDER_ROLE, msg.sender)) { | ||
// Refunder can refund if deadline has passed | ||
if (block.timestamp <= transaction.deadline) revert DeadlineNotExceeded(); | ||
} else { | ||
// Permissionless refund is allowed after REFUND_DELAY | ||
if (block.timestamp <= transaction.deadline + REFUND_DELAY) revert DeadlineNotExceeded(); | ||
} | ||
|
||
// set status to refunded if still in requested state | ||
if (bridgeStatuses[transactionId] != BridgeStatus.REQUESTED) revert StatusIncorrect(); | ||
bridgeStatuses[transactionId] = BridgeStatus.REFUNDED; | ||
|
||
// transfer origin collateral back to original sender | ||
address to = transaction.originSender; | ||
address token = transaction.originToken; | ||
uint256 amount = transaction.originAmount + transaction.originFeeAmount; | ||
token.universalTransfer(to, amount); | ||
|
||
emit BridgeDepositRefunded(transactionId, to, token, 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
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
Oops, something went wrong.