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 #64

Merged
merged 56 commits into from
Sep 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
56 commits
Select commit Hold shift + click to select a range
382c867
Add BridgeProxy contract and state variables
cosmatudor Aug 21, 2024
266aad9
add deposit endpoint
cosmatudor Aug 22, 2024
2e08dd9
add logic for deposit funds in Bridge contract
cosmatudor Aug 22, 2024
c0aa688
moved structs to SharedStructs
cosmatudor Aug 22, 2024
b004524
Merge branch 'CII-56-pending-transactions-management' into CII-53-dep…
cosmatudor Aug 22, 2024
cb71bf9
add counter for transactions
cosmatudor Aug 22, 2024
cfb9263
add execute endpoint
cosmatudor Aug 22, 2024
4ed8787
fixed BridgeMock bug
cosmatudor Aug 22, 2024
36075e9
Merge branch 'CII-53-deposit-endpoint' into CII-54-execute-endpoint
cosmatudor Aug 22, 2024
5f51c43
add executeCallback private method
cosmatudor Aug 22, 2024
c8478bd
add finishExecuteGracefully private method
cosmatudor Aug 22, 2024
873c584
Merge pull request #42 from multiversx/CII-56-pending-transactions-ma…
dragos-rebegea Aug 22, 2024
bf7feab
add refund method and view functions
cosmatudor Aug 23, 2024
35509e2
removed token payments mapping and refactored
cosmatudor Aug 23, 2024
77d9018
Merge branch 'CII-53-deposit-endpoint' into CII-54-execute-endpoint
cosmatudor Aug 23, 2024
dc3b320
Merge branch 'CII-54-execute-endpoint' into CII-55-finish-execute-gra…
cosmatudor Aug 23, 2024
152b651
removed token payment
cosmatudor Aug 23, 2024
2f95346
Merge branch 'CII-55-finish-execute-gracefully' into CII-58-refund-wo…
cosmatudor Aug 23, 2024
2bf24e6
fixed logic after removing token payments
cosmatudor Aug 23, 2024
093e45c
Merge branch 'CII-54-execute-endpoint' into CII-55-finish-execute-gra…
cosmatudor Aug 23, 2024
ea91fda
Merge branch 'CII-55-finish-execute-gracefully' into CII-58-refund-wo…
cosmatudor Aug 23, 2024
5d704da
add setter for Bridge address
cosmatudor Aug 23, 2024
7257749
refactored tests for Bridge, MintBurnERC20 and ERC20Safe contracts
cosmatudor Aug 23, 2024
25faae2
refactored deposit endpoint
cosmatudor Aug 23, 2024
07721ef
removed state variable bridgeAddress
cosmatudor Aug 23, 2024
554ec5f
Merge pull request #45 from multiversx/CII-53-deposit-endpoint
dragos-rebegea Aug 26, 2024
b7a6f53
Merge pull request #47 from multiversx/CII-54-execute-endpoint
dragos-rebegea Aug 26, 2024
b1084e7
Merge pull request #48 from multiversx/CII-55-finish-execute-gracefully
dragos-rebegea Aug 26, 2024
d50a18a
Merge pull request #51 from multiversx/CII-58-refund-workflow
dragos-rebegea Aug 26, 2024
8b28c00
small gas optimization for getPendingTransaction view function
cosmatudor Aug 26, 2024
f05e4da
lowestIndexId and calldata encoding fixes
cosmatudor Aug 26, 2024
79529b5
add unit tests for BridgeProxy contract
cosmatudor Aug 26, 2024
bbc701f
refactored execute endpoint
cosmatudor Aug 26, 2024
1578588
Merge pull request #53 from multiversx/CII-57-bridge-proxy-testing
dragos-rebegea Aug 26, 2024
5d776a4
add logic for deleting pending txns
cosmatudor Aug 27, 2024
82678b5
add test for txns with endpoints with no args
cosmatudor Aug 27, 2024
f6bcaf2
Merge branch 'CII-57-bridge-proxy-testing' into feat-v3.5
cosmatudor Aug 27, 2024
084a44e
Merge branch 'feat/v3' into feat-v3.5
cosmatudor Aug 28, 2024
886c783
Merge remote-tracking branch 'origin/feat/v3' into feat-v3.5
cosmatudor Aug 28, 2024
2b0440d
made BridgeProxy contract upgradable
cosmatudor Aug 28, 2024
3beeb5e
test fixes after merge
cosmatudor Aug 28, 2024
21e4633
add test for BridgeProxy contract upgrade
cosmatudor Aug 28, 2024
af5cc52
Merge pull request #54 from multiversx/merge-3.5-3
dragos-rebegea Aug 29, 2024
f8a2e46
Merge branch 'feat/v3.5' into feat-v3.5
dragos-rebegea Aug 29, 2024
0152098
add isPendingTransaction private function
cosmatudor Aug 29, 2024
1f99ceb
Merge branch 'feat-v3.5' of https://github.com/multiversx/mx-bridge-e…
cosmatudor Aug 29, 2024
4707f2a
temporar fixes
cosmatudor Aug 30, 2024
47cf9bd
fixes after review
cosmatudor Aug 30, 2024
16a2c21
renamed BridgeProxy to BridgeExecutor
cosmatudor Sep 16, 2024
72e586a
more fixes
cosmatudor Sep 16, 2024
7c953fe
refactor execute function
cosmatudor Sep 18, 2024
76999c4
renamed variables
cosmatudor Sep 18, 2024
3e64909
Merge pull request #52 from multiversx/feat-v3.5
cosmatudor Sep 18, 2024
52e63f0
Merge branch 'feat/v3.5.0' into merge-feat-v3.5.0-v3.5-2024.09.18
iulianpascalau Sep 18, 2024
73b1f40
- fixes after merge
iulianpascalau Sep 18, 2024
7e02660
Merge pull request #65 from multiversx/merge-feat-v3.5.0-v3.5-2024.09.18
iulianpascalau Sep 18, 2024
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;
}
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;
}
}
9 changes: 7 additions & 2 deletions contracts/test/BridgeMock.sol
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,13 @@ pragma solidity ^0.8.20;
import "../Bridge.sol";

contract BridgeMock is Bridge {
function initialize(address[] memory board, uint256 initialQuorum, ERC20Safe erc20Safe) public override initializer {
Bridge.initialize(board, initialQuorum, erc20Safe);
function initialize(
address[] memory board,
uint256 initialQuorum,
ERC20Safe erc20Safe,
BridgeExecutor bridgeExecutor
) public override initializer {
Bridge.initialize(board, initialQuorum, erc20Safe, bridgeExecutor);
}

function proxyTransfer(address tokenAddress, uint256 amount, address recipientAddress) external returns (bool) {
Expand Down
2 changes: 1 addition & 1 deletion tasks/deploy/evil-erc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,5 +16,5 @@ task("deploy-evil-erc", "Deploys EvilERC20 contract to use to test the bridge").

//whitelist tokens in safe
console.log("Whitelisting token ", usdcContract.target);
await safe.whitelistToken(usdcContract.target, "25000000", "100000000000", false, true);
await safe.whitelistToken(usdcContract.target, "25000000", "100000000000", false, true, 0, 0, 0);
});
Loading
Loading