Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
63 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
a5ff5d6
Merge remote-tracking branch 'refs/remotes/origin/feat/v3' into delay…
evelinemolnar Sep 13, 2024
f111b4f
Merge remote-tracking branch 'refs/remotes/origin/feat-v3.5' into del…
evelinemolnar Sep 13, 2024
eb048d8
gas limit test fails
evelinemolnar Sep 16, 2024
073c8a7
rename event
evelinemolnar Sep 16, 2024
3e5d85f
refactor
evelinemolnar Sep 16, 2024
9de28c9
get&set
evelinemolnar Sep 16, 2024
5f5e808
to be optimised
evelinemolnar Sep 16, 2024
878dbbe
modify execution time
evelinemolnar Sep 17, 2024
c325b44
update execution time part 2
evelinemolnar Sep 17, 2024
8a20e23
relocate struct
evelinemolnar Sep 18, 2024
5eb2187
add todo's
evelinemolnar Sep 18, 2024
6cf1b01
Merge branch 'feat/v3.5.0' into delayed-transactions-v1
evelinemolnar Sep 19, 2024
656a8df
fixes after merge
evelinemolnar Sep 19, 2024
0d49614
comm
evelinemolnar Sep 19, 2024
c7a980e
calldata -> memory + solve events todo's
evelinemolnar Sep 20, 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
244 changes: 234 additions & 10 deletions contracts/ERC20Safe.sol
Original file line number Diff line number Diff line change
Expand Up @@ -38,10 +38,16 @@ contract ERC20Safe is Initializable, BridgeRole, Pausable {
uint16 private constant maxBatchSize = 100;
uint8 public batchBlockLimit;
uint8 public batchSettleLimit;
uint256 public numBuckets;
uint256 public blocksInBucket;
uint256 public defaultSingleTransactionThreshold;
uint256 public defaultAggregateValueThreshold;

// Reserved storage slots for future upgrades
uint256[10] private __gap;

mapping(uint256 => uint256) public bucketLastUpdatedNonce;
mapping(uint256 => mapping(address => uint256)) public aggregatedValue;
mapping(uint256 => Batch) public batches;
mapping(address => bool) public whitelistedTokens;
mapping(address => bool) public mintBurnTokens;
Expand All @@ -52,9 +58,29 @@ contract ERC20Safe is Initializable, BridgeRole, Pausable {
mapping(address => uint256) public mintBalances;
mapping(address => uint256) public burnBalances;
mapping(uint256 => Deposit[]) public batchDeposits;
mapping(address => uint256) public singleTransactionThreshold;
mapping(address => uint256) public aggregateValueThreshold;

event ERC20Deposit(uint112 batchId, uint112 depositNonce);
event ERC20SCDeposit(uint112 indexed batchId, uint112 depositNonce, bytes callData);
event TransactionDelayed(
address indexed sender,
address indexed tokenAddress,
uint256 amount,
bytes32 recipientAddress,
bool isLarge
);

//optional
event DelayedTransactionProcessed(
address indexed sender,
address indexed tokenAddress,
uint256 amount,
bytes32 recipientAddress,
bool isLarge
);

DelayedTransaction[] public delayedTransactions;

function initialize() public initializer {
__BridgeRole_init();
Expand All @@ -66,6 +92,10 @@ contract ERC20Safe is Initializable, BridgeRole, Pausable {
batchSize = 10;
batchBlockLimit = 40;
batchSettleLimit = 40;
numBuckets = 24;
blocksInBucket = 300; // 300 blocks = 3600 seconds/12 seconds per block
defaultSingleTransactionThreshold = 1000; //to be set correctly
defaultAggregateValueThreshold = 10000; //to be set correctly
}

/**
Expand All @@ -83,7 +113,9 @@ contract ERC20Safe is Initializable, BridgeRole, Pausable {
bool native,
uint256 totalBalance,
uint256 mintBalance,
uint256 burnBalance
uint256 burnBalance,
uint256 singleTxThreshold,
uint256 aggregateThreshold
) external onlyAdmin {
if (!mintBurn) {
require(native, "Only native tokens can be stored!");
Expand All @@ -101,6 +133,18 @@ contract ERC20Safe is Initializable, BridgeRole, Pausable {
require(burnBalance == 0, "Stored tokens must have 0 burn balance!");
initSupply(token, totalBalance);
}

if (singleTxThreshold == 0) {
singleTransactionThreshold[token] = defaultSingleTransactionThreshold;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Question: do you think it will be better that, instead of using default values here, we should error as to prevent as much as we can bad values?
Valid also for aggregateThreshold

} else {
singleTransactionThreshold[token] = singleTxThreshold;
}

if (aggregateThreshold == 0) {
aggregateValueThreshold[token] = defaultAggregateValueThreshold;
} else {
aggregateValueThreshold[token] = aggregateThreshold;
}
}

/**
Expand Down Expand Up @@ -186,8 +230,7 @@ contract ERC20Safe is Initializable, BridgeRole, Pausable {
function deposit(address tokenAddress, uint256 amount, bytes32 recipientAddress) public whenNotPaused {
uint112 batchNonce;
uint112 depositNonce;
(batchNonce, depositNonce) = _deposit_common(tokenAddress, amount, recipientAddress);
emit ERC20Deposit(depositNonce, batchNonce);
(batchNonce, depositNonce) = _deposit_common(tokenAddress, amount, recipientAddress, "");
}

/*
Expand All @@ -205,19 +248,132 @@ contract ERC20Safe is Initializable, BridgeRole, Pausable {
* 0x + endpoint_name_length (4 bytes) + endpoint_name + gas_limit (8 bytes) +
* 00 (ArgumentsPresentProtocolMarker)
*/
function depositWithSCExecution(address tokenAddress, uint256 amount, bytes32 recipientAddress, bytes calldata callData) public whenNotPaused {
function depositWithSCExecution(
address tokenAddress,
uint256 amount,
bytes32 recipientAddress,
bytes memory callData
) public whenNotPaused {
uint112 batchNonce;
uint112 depositNonce;
(batchNonce, depositNonce) = _deposit_common(tokenAddress, amount, recipientAddress);
emit ERC20SCDeposit(batchNonce, depositNonce, callData);
(batchNonce, depositNonce) = _deposit_common(tokenAddress, amount, recipientAddress, callData);
}


function _deposit_common(address tokenAddress, uint256 amount, bytes32 recipientAddress) internal returns (uint112 batchNonce, uint112 depositNonce) {
function _deposit_common(
address tokenAddress,
uint256 amount,
bytes32 recipientAddress,
bytes memory callData
) internal returns (uint112 batchNonce, uint112 depositNonce) {
require(whitelistedTokens[tokenAddress], "Unsupported token");
require(amount >= tokenMinLimits[tokenAddress], "Tried to deposit an amount below the minimum specified limit");
require(amount <= tokenMaxLimits[tokenAddress], "Tried to deposit an amount above the maximum specified limit");

_processDelayedTransactions(tokenAddress);

uint256 currentBlock = block.number;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you think it will be better to work with block.timestamp instead of block.number, or, at least keep also the timestamp when the deposit was made just to simplify the logic of time computing when we decide we can execute a transaction?

uint256 bucketId = (currentBlock / blocksInBucket) % numBuckets;

_resetBucketIfNeeded(bucketId, tokenAddress, currentBlock);

if (amount >= singleTransactionThreshold[tokenAddress]) {
_addDelayedTransaction(tokenAddress, amount, recipientAddress, true, callData);
emit TransactionDelayed(msg.sender, tokenAddress, amount, recipientAddress, true);
return (0, 0);
} else {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

useless else? :)

uint256 totalAggregatedValue = _getTotalAggregatedValue(tokenAddress, currentBlock);
if (totalAggregatedValue + amount <= aggregateValueThreshold[tokenAddress]) {
aggregatedValue[bucketId][tokenAddress] += amount;
(batchNonce, depositNonce) = _processDeposit(tokenAddress, amount, recipientAddress, callData);
return (batchNonce, depositNonce);
} else {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

useless else?

_addDelayedTransaction(tokenAddress, amount, recipientAddress, false, callData);
emit TransactionDelayed(msg.sender, tokenAddress, amount, recipientAddress, false);
return (0, 0);
}
}
}

function _addDelayedTransaction(
address tokenAddress,
uint256 amount,
bytes32 recipientAddress,
bool isLarge,
bytes memory callData
) internal {
DelayedTransaction memory dt = DelayedTransaction({
amount: amount,
tokenAddress: tokenAddress,
sender: msg.sender,
recipientAddress: recipientAddress,
blockAdded: block.number,
isLarge: isLarge,
callData: callData
});
delayedTransactions.push(dt);
}

function _processDelayedTransactions(address tokenAddress) internal {
uint256 i = 0;
while (i < delayedTransactions.length) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess this will consume a lot of gas. And the user y will actually pay for executing the deposit operations for user x, and so on. Thinking of a way to improve this and do the computation only when trying to assemble the current batch (I do not have a good idea now)

This method also has the disadvantage that, if a transaction is placed in a bucket, it won't be executed until a new deposit will be made. Which, if it will happen in 3 days, that will be the moment when it will actually be placed in a batch.

DelayedTransaction storage dt = delayedTransactions[i];

if (dt.tokenAddress != tokenAddress) {
i++;
continue;
}

if (_canProcessDelayedTransaction(dt)) {
_processDeposit(dt.tokenAddress, dt.amount, dt.recipientAddress, dt.callData);

delayedTransactions[i] = delayedTransactions[delayedTransactions.length - 1];
delayedTransactions.pop();

emit DelayedTransactionProcessed(
dt.sender,
dt.tokenAddress,
dt.amount,
dt.recipientAddress,
dt.isLarge
);
} else {
i++;
}
}
}

function _canProcessDelayedTransaction(DelayedTransaction storage dt) internal returns (bool) {
uint256 currentBlock = block.number;
uint256 averageBlockTime = 12;

if (dt.isLarge) {
uint256 blocksIn24Hours = (24 * 60 * 60) / averageBlockTime;
return currentBlock >= dt.blockAdded + blocksIn24Hours;
} else {
uint256 bucketId = (currentBlock / blocksInBucket) % numBuckets;

_resetBucketIfNeeded(bucketId, dt.tokenAddress, currentBlock);

uint256 totalAggregatedValue = _getTotalAggregatedValue(dt.tokenAddress, currentBlock);
uint256 threshold = aggregateValueThreshold[dt.tokenAddress];

if (totalAggregatedValue + dt.amount <= threshold) {
aggregatedValue[bucketId][dt.tokenAddress] += dt.amount;
return true;
} else if (currentBlock >= dt.blockAdded + ((24 * 60 * 60) / averageBlockTime)) {
return true;
} else {
return false;
}
}
}

function _processDeposit(
address tokenAddress,
uint256 amount,
bytes32 recipientAddress,
bytes memory callData
) internal returns (uint112 batchNonce, uint112 depositNonce) {
uint64 currentBlockNumber = uint64(block.number);

Batch storage batch;
Expand All @@ -230,7 +386,7 @@ contract ERC20Safe is Initializable, BridgeRole, Pausable {
batch = batches[batchesCount - 1];
}

uint112 depositNonce = depositsCount + 1;
depositNonce = depositsCount + 1;
batchDeposits[batchesCount - 1].push(
Deposit(depositNonce, tokenAddress, amount, msg.sender, recipientAddress, DepositStatus.Pending)
);
Expand All @@ -253,7 +409,68 @@ contract ERC20Safe is Initializable, BridgeRole, Pausable {
totalBalances[tokenAddress] += amount;
erc20.safeTransferFrom(msg.sender, address(this), amount);
}
return (batch.nonce, depositNonce);

batchNonce = batch.nonce;
if (callData.length > 0) {
emit ERC20SCDeposit(batchNonce, depositNonce, callData);
} else {
emit ERC20Deposit(batchNonce, depositNonce);
}
return (batchNonce, depositNonce);
}

function _getTotalAggregatedValue(
address tokenAddress,
uint256 currentBlock
) internal returns (uint256 totalAggregatedValue) {
totalAggregatedValue = 0;

for (uint256 i = 0; i < numBuckets; i++) {
_resetBucketIfNeeded(i, tokenAddress, currentBlock);
totalAggregatedValue += aggregatedValue[i][tokenAddress];
}
}

/**
@notice Updates the threshold for a particular token
@param token Address of the ERC20 token
@param amount New threshold for the aggregated value
*/
function setThreshold(address token, uint256 amount) external onlyAdmin {
aggregateValueThreshold[token] = amount;
}

function getThreshold(address tokenAddress) public view returns (uint256) {
return aggregateValueThreshold[tokenAddress];
}

/**
@notice Updates the single transaction threshold for a particular token
@param token Address of the ERC20 token
@param amount New threshold for the aggregated value
*/
function setSingleTxThreshold(address token, uint256 amount) external onlyAdmin {
singleTransactionThreshold[token] = amount;
}

function getSingleTxThreshold(address tokenAddress) public view returns (uint256) {
return singleTransactionThreshold[tokenAddress];
}

/**
@notice Process a delayed transaction immediately
@param index Index of the delayed transaction
*/
function processDelayedTransactionImmediately(uint256 index) external onlyAdmin {
require(index < delayedTransactions.length, "Invalid index");
DelayedTransaction storage dt = delayedTransactions[index];

_processDeposit(dt.tokenAddress, dt.amount, dt.recipientAddress, dt.callData);

delayedTransactions[index] = delayedTransactions[delayedTransactions.length - 1];
delayedTransactions.pop();

emit DelayedTransactionProcessed(dt.sender, dt.tokenAddress, dt.amount, dt.recipientAddress, dt.isLarge);
}

/**
Expand Down Expand Up @@ -420,4 +637,11 @@ contract ERC20Safe is Initializable, BridgeRole, Pausable {
function _isTokenMintBurn(address token) internal view returns (bool) {
return mintBurnTokens[token];
}

function _resetBucketIfNeeded(uint256 bucketId, address tokenAddress, uint256 currentBlock) internal {
if (currentBlock - bucketLastUpdatedNonce[bucketId] > blocksInBucket) {
aggregatedValue[bucketId][tokenAddress] = 0;
bucketLastUpdatedNonce[bucketId] = currentBlock;
}
}
}
10 changes: 10 additions & 0 deletions contracts/SharedStructs.sol
Original file line number Diff line number Diff line change
Expand Up @@ -43,3 +43,13 @@ struct MvxTransaction {
uint256 depositNonce;
bytes callData;
}

struct DelayedTransaction {
uint256 amount;
address tokenAddress;
address sender;
bytes32 recipientAddress;
uint256 blockAdded;
bool isLarge;
bytes callData;
}
2 changes: 1 addition & 1 deletion test/Bridge.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ describe("Bridge", async function () {
genericErc20 = await deployContract(adminWallet, "GenericERC20", ["TSC", "TSC", 6]);
await genericErc20.mint(adminWallet.address, 1000);
await genericErc20.approve(erc20Safe.address, 1000);
await erc20Safe.whitelistToken(genericErc20.address, 0, 100, false, true, 0, 0, 0);
await erc20Safe.whitelistToken(genericErc20.address, 0, 100, false, true, 0, 0, 0, 0, 0);
await erc20Safe.unpause();
}

Expand Down
6 changes: 3 additions & 3 deletions test/BridgeExecutor.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,9 @@ describe("BridgeExecutor", function () {

async function setupErc20Token() {
genericErc20 = await deployContract(adminWallet, "GenericERC20", ["TSC", "TSC", 6]);
await genericErc20.mint(adminWallet.address, 1000);
await genericErc20.approve(erc20Safe.address, 1000);
await erc20Safe.whitelistToken(genericErc20.address, 0, 1000, false, true, 0, 0, 0);
await genericErc20.mint(adminWallet.address, 2000);
await genericErc20.approve(erc20Safe.address, 2000);
await erc20Safe.whitelistToken(genericErc20.address, 0, 10000, false, true, 1000, 0, 0, 0, 0);
await erc20Safe.unpause();
}

Expand Down
2 changes: 1 addition & 1 deletion test/MintBurnERC20.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ describe("ERC20Safe, MintBurnERC20, and Bridge Interaction", function () {

async function setupErc20Token() {
mintBurnErc20 = await deployUpgradableContract(adminWallet, "MintBurnERC20", ["Test Token", "TST", 6]);
await erc20Safe.whitelistToken(mintBurnErc20.address, 0, 100, true, false, 0, 0, 0);
await erc20Safe.whitelistToken(mintBurnErc20.address, 0, 100, true, false, 0, 0, 0, 0, 0);
await erc20Safe.unpause();
}

Expand Down
Loading