Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat/v3.5.0 #66

Open
wants to merge 8 commits into
base: feat/v3
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
92 changes: 68 additions & 24 deletions contracts/Bridge.sol
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import "./SharedStructs.sol";
import "./ERC20Safe.sol";
import "./access/RelayerRole.sol";
import "./lib/Pausable.sol";
import "./BridgeExecutor.sol";

/**
@title Bridge
Expand Down Expand Up @@ -40,6 +41,7 @@ contract Bridge is Initializable, RelayerRole, Pausable {
uint256[10] private __gap;

ERC20Safe internal safe;
BridgeExecutor internal bridgeExecutor;

mapping(uint256 => bool) public executedBatches;
mapping(uint256 => CrossTransferStatus) public crossTransferStatuses;
Expand All @@ -53,19 +55,30 @@ contract Bridge is Initializable, RelayerRole, Pausable {
* - add/remove relayers
* - add/remove tokens that can be bridged
*/
function initialize(address[] memory board, uint256 initialQuorum, ERC20Safe erc20Safe) public virtual initializer {
function initialize(
address[] memory board,
uint256 initialQuorum,
ERC20Safe erc20Safe,
BridgeExecutor _bridgeExecutor
) public virtual initializer {
__RelayerRole_init();
__Bridge__init_unchained(board, initialQuorum, erc20Safe);
__Bridge__init_unchained(board, initialQuorum, erc20Safe, _bridgeExecutor);
}

function __Bridge__init_unchained(address[] memory board, uint256 initialQuorum, ERC20Safe erc20Safe) internal onlyInitializing {
function __Bridge__init_unchained(
address[] memory board,
uint256 initialQuorum,
ERC20Safe erc20Safe,
BridgeExecutor _bridgeExecutor
) internal onlyInitializing {
require(initialQuorum >= minimumQuorum, "Quorum is too low.");
require(board.length >= initialQuorum, "The board should be at least the quorum size.");

_addRelayers(board);

quorum = initialQuorum;
safe = erc20Safe;
bridgeExecutor = _bridgeExecutor;

batchSettleBlockCount = 40;
}
Expand Down Expand Up @@ -109,20 +122,19 @@ contract Bridge is Initializable, RelayerRole, Pausable {
}

/**
@notice Executes transfers that were signed by the relayers.
@dev This is for the MultiversX to Ethereum flow
@dev Arrays here try to mimmick the structure of a batch. A batch represents the values from the same index in all the arrays.
@param tokens Array containing all the token addresses that the batch interacts with. Can even contain duplicates.
@param recipients Array containing all the destinations from the batch. Can be duplicates.
@param amounts Array containing all the amounts that will be transfered.
@param batchNonceMvx Nonce for the batch. This identifies a batch created on the MultiversX chain that bridges tokens from MultiversX to Ethereum
@param signatures Signatures from all the relayers for the execution. This mimics a delegated multisig contract. For the execution to take place, there must be enough valid signatures to achieve quorum.
@notice Executes a batch of transfers
@param mvxTransactions List of transactions from MultiversX side. Each transaction consists of:
- token address
- sender
- recipient
- amount
- deposit nonce
- call data
@param batchNonceMvx Nonce for the batch
@param signatures List of signatures from the relayers
*/
function executeTransfer(
address[] calldata tokens,
address[] calldata recipients,
uint256[] calldata amounts,
uint256[] calldata depositNonces,
MvxTransaction[] calldata mvxTransactions,
uint256 batchNonceMvx,
bytes[] calldata signatures
) public whenNotPaused onlyRelayer {
Expand All @@ -132,16 +144,12 @@ contract Bridge is Initializable, RelayerRole, Pausable {

_validateQuorum(
signatures,
_getHashedDepositData(
abi.encode(recipients, tokens, amounts, depositNonces, batchNonceMvx, executeTransferAction)
)
_getHashedDepositData(abi.encode(mvxTransactions, batchNonceMvx, executeTransferAction))
);

DepositStatus[] memory statuses = new DepositStatus[](tokens.length);
for (uint256 j = 0; j < tokens.length; j++) {
statuses[j] = safe.transfer(tokens[j], amounts[j], recipients[j])
? DepositStatus.Executed
: DepositStatus.Rejected;
DepositStatus[] memory statuses = new DepositStatus[](mvxTransactions.length);
for (uint256 j = 0; j < mvxTransactions.length; j++) {
statuses[j] = _processDeposit(mvxTransactions[j]);
}

CrossTransferStatus storage crossStatus = crossTransferStatuses[batchNonceMvx];
Expand All @@ -154,7 +162,9 @@ contract Bridge is Initializable, RelayerRole, Pausable {
@param batchNonceMvx Nonce for the batch
@return a list of statuses for each transfer in the batch provided and a boolean that indicates if the information is final
*/
function getStatusesAfterExecution(uint256 batchNonceMvx) external view returns (DepositStatus[] memory, bool isFinal) {
function getStatusesAfterExecution(
uint256 batchNonceMvx
) external view returns (DepositStatus[] memory, bool isFinal) {
CrossTransferStatus memory crossStatus = crossTransferStatuses[batchNonceMvx];
return (crossStatus.statuses, _isMvxBatchFinal(crossStatus.createdBlockNumber));
}
Expand All @@ -176,6 +186,40 @@ contract Bridge is Initializable, RelayerRole, Pausable {
return keccak256(abi.encodePacked(prefix, keccak256(encodedData)));
}

function _processDeposit(MvxTransaction calldata mvxTransaction) private returns (DepositStatus) {
address recipient;
bool isScCall = _isScCall(mvxTransaction.callData);

if (isScCall) {
recipient = address(bridgeExecutor);
} else {
recipient = mvxTransaction.recipient;
}

if (mvxTransaction.amount == 0) {
return DepositStatus.Rejected;
}

bool transferSuccess = safe.transfer(mvxTransaction.token, mvxTransaction.amount, recipient);
if (!transferSuccess) {
return DepositStatus.Rejected;
}

// If the recipient is a smart contract, attempt to deposit the funds in bridgeExecutor
if (isScCall) {
bool depositSuccess = bridgeExecutor.deposit(mvxTransaction);
if (!depositSuccess) {
return DepositStatus.Rejected;
}
}

return DepositStatus.Executed;
}

function _isScCall(bytes calldata _data) private pure returns (bool) {
return _data.length > 0;
}

function _validateQuorum(bytes[] memory signatures, bytes32 data) private view {
uint256 signersCount;
address[] memory validSigners = new address[](signatures.length);
Expand Down
139 changes: 139 additions & 0 deletions contracts/BridgeExecutor.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
//SPDX-License-Identifier: UNLICENSED

pragma solidity ^0.8.20;

import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
import "./lib/Pausable.sol";
import "./SharedStructs.sol";
import "./lib/BoolTokenTransfer.sol";
import "./access/AdminRole.sol";
import "./access/BridgeRole.sol";

contract BridgeExecutor is Initializable, Pausable, BridgeRole {
using BoolTokenTransfer for IERC20;

/*========================= CONTRACT STATE =========================*/
uint128 public constant MIN_GAS_LIMIT_FOR_SC_CALL = 10_000_000;
uint128 public constant DEFAULT_GAS_LIMIT_FOR_REFUND_CALLBACK = 20_000_000;
uint256 private lowestTxId;
uint256 private currentTxId;

mapping(uint256 => MvxTransaction) private pendingTransactions;

/*========================= PUBLIC API =========================*/
function initialize() public initializer {
__BridgeRole_init();
__Pausable_init();
}

function __BridgeExecutor__init_unchained() internal onlyInitializing {
lowestTxId = 0;
currentTxId = 0;
}

function deposit(MvxTransaction calldata txn) external payable whenNotPaused onlyBridge returns (bool) {
pendingTransactions[currentTxId] = txn;
currentTxId++;

IERC20 token = IERC20(txn.token);
bool approvalSuccess = token.approve(txn.recipient, txn.amount);

return approvalSuccess;
}

function execute(uint256 txId) external whenNotPaused {
require(txId < currentTxId, "BridgeExecutor: Invalid transaction ID");
MvxTransaction memory txn = pendingTransactions[txId];

require(txn.recipient != address(0), "BridgeExecutor: Transaction does not exist");

if (txn.callData.length == 0) {
_refundAndDeleteTxn(txId);
return;
}

(bytes memory selector, uint256 gasLimit, bytes memory args) = abi.decode(
txn.callData,
(bytes, uint256, bytes)
);

if (selector.length == 0 || gasLimit == 0 || gasLimit < MIN_GAS_LIMIT_FOR_SC_CALL) {
_refundAndDeleteTxn(txId);
return;
}

bytes memory data;
if (args.length > 0) {
data = abi.encodePacked(selector, args);
} else {
data = selector;
}

_updateLowestTxId();

delete pendingTransactions[txId];

(bool success, ) = txn.recipient.call{ gas: gasLimit }(data);

if (!success) {
_refundTransaction(txn.token, txn.amount);
return;
}
}

/*========================= PRIVATE API =========================*/
function _refundAndDeleteTxn(uint256 txId) private {
MvxTransaction memory txn = pendingTransactions[txId];
_refundTransaction(txn.token, txn.amount);

_updateLowestTxId();

delete pendingTransactions[txId];
}

function _refundTransaction(address token, uint256 amount) private {
IERC20 erc20 = IERC20(token);
bool transferExecuted = erc20.boolTransfer(this.bridge(), amount);
require(transferExecuted, "BridgeExecutor: Refund failed");
}

function _updateLowestTxId() private {
uint256 newLowestTxId = lowestTxId;

while (newLowestTxId < currentTxId && pendingTransactions[newLowestTxId].amount == 0) {
newLowestTxId++;
}

lowestTxId = newLowestTxId;
}

function _isPendingTransaction(uint256 txId) private view returns (bool) {
return pendingTransactions[txId].amount != 0;
}

/*========================= VIEW FUNCTIONS =========================*/
function getPendingTransactionById(uint256 txId) public view returns (MvxTransaction memory) {
return pendingTransactions[txId];
}

function getPendingTransactions() public view returns (MvxTransaction[] memory) {
uint256 pendingTransactionsCount = currentTxId - lowestTxId;
MvxTransaction[] memory txns = new MvxTransaction[](pendingTransactionsCount);
uint256 index = 0;

for (uint256 i = lowestTxId; i < currentTxId; i++) {
if (_isPendingTransaction(i)) {
txns[index] = pendingTransactions[i];
index++;
}
}

// Resize the array to the actual number of pending transactions
assembly {
mstore(txns, index)
}

return txns;
}
}
9 changes: 9 additions & 0 deletions contracts/SharedStructs.sol
Original file line number Diff line number Diff line change
Expand Up @@ -34,3 +34,12 @@ struct Batch {
struct DepositSCExtension {
string depositData;
}

struct MvxTransaction {
address token;
bytes32 sender;
address recipient;
uint256 amount;
uint256 depositNonce;
bytes callData;
}
28 changes: 28 additions & 0 deletions contracts/TestCaller.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

struct CalledData {
uint256 size;
address addr;
address tokenIdentifier;
}

contract TestCaller {
CalledData[] private calledDataParams;

constructor() {}

function upgrade() public {}

function callPayable() public {}

function callPayableWithParams(uint256 size, address addr, address tokenIdentifier) public {
CalledData memory data = CalledData({ size: size, addr: addr, tokenIdentifier: tokenIdentifier });

calledDataParams.push(data);
}

function getCalledDataParams() public view returns (CalledData[] memory) {
return calledDataParams;
}
}
27 changes: 27 additions & 0 deletions contracts/test/BridgeExecutorTestContract.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
//SPDX-License-Identifier: UNLICENSED

pragma solidity ^0.8.20;

interface IERC20 {
function transferFrom(address sender, address recipient, uint256 amount) external returns (bool);
}

contract BridgeExecutorTestContract {
uint256 public count;
address public bridgeExecutor;

constructor(address _bridgeExecutor) {
count = 0;
bridgeExecutor = _bridgeExecutor;
}

function increment() public {
count += 1;
}

function withdraw(address tokenAddress, uint256 amount) external {
IERC20 token = IERC20(tokenAddress);
// The test contract should be approved to spend the tokens on behalf of the bridgeExecutor
require(token.transferFrom(bridgeExecutor, address(this), amount), "TransferFrom failed");
}
}
19 changes: 19 additions & 0 deletions contracts/test/BridgeExecutorUpgrade.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
//SPDX-License-Identifier: UNLICENSED

pragma solidity ^0.8.20;

import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";

import "../BridgeExecutor.sol";

contract BridgeExecutorUpgrade is Initializable, BridgeExecutor {
uint256 public someValue;
// New initialization function for the upgrade
function initializeV2(uint256 val) public reinitializer(2) {
someValue = val;
}

function afterUpgrade() public view returns (uint256) {
return someValue;
}
}
Loading
Loading