From 19f2a959ce1fd27405f548d27a487b474a201e78 Mon Sep 17 00:00:00 2001 From: kiseln <3428059+kiseln@users.noreply.github.com> Date: Thu, 22 Aug 2024 17:44:44 +0400 Subject: [PATCH 1/3] Upgrade the bridge to work with chain signatures instead of light client proofs --- erc20-bridge-token/contracts/Borsh.sol | 36 ++++++ .../contracts/BridgeTokenFactory.sol | 109 ++++++++++-------- .../contracts/ProofConsumer.sol | 72 ------------ .../contracts/ResultsDecoder.sol | 51 -------- .../contracts/test/NearProverMock.sol | 13 --- 5 files changed, 97 insertions(+), 184 deletions(-) create mode 100644 erc20-bridge-token/contracts/Borsh.sol delete mode 100644 erc20-bridge-token/contracts/ProofConsumer.sol delete mode 100644 erc20-bridge-token/contracts/ResultsDecoder.sol delete mode 100644 erc20-bridge-token/contracts/test/NearProverMock.sol diff --git a/erc20-bridge-token/contracts/Borsh.sol b/erc20-bridge-token/contracts/Borsh.sol new file mode 100644 index 00000000..666653f3 --- /dev/null +++ b/erc20-bridge-token/contracts/Borsh.sol @@ -0,0 +1,36 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity ^0.8.24; + +library Borsh { + function encodeUint32(uint32 val) internal pure returns (bytes memory) { + return abi.encodePacked(swapBytes4(val)); + } + + function encodeUint128(uint128 val) internal pure returns (bytes memory) { + return abi.encodePacked(swapBytes16(val)); + } + + function encodeString(string memory val) internal pure returns (bytes memory) { + bytes memory b = bytes(val); + return bytes.concat( + encodeUint32(uint32(b.length)), + bytes(val) + ); + } + + function encodeAddress(address val) internal pure returns (bytes20) { + return bytes20(val); + } + + function swapBytes4(uint32 v) internal pure returns (uint32) { + v = ((v & 0x00ff00ff) << 8) | ((v & 0xff00ff00) >> 8); + return (v << 16) | (v >> 16); + } + + function swapBytes16(uint128 v) internal pure returns (uint128) { + v = ((v & 0x00ff00ff00ff00ff00ff00ff00ff00ff) << 8) | ((v & 0xff00ff00ff00ff00ff00ff00ff00ff00) >> 8); + v = ((v & 0x0000ffff0000ffff0000ffff0000ffff) << 16) | ((v & 0xffff0000ffff0000ffff0000ffff0000) >> 16); + v = ((v & 0x00000000ffffffff00000000ffffffff) << 32) | ((v & 0xffffffff00000000ffffffff00000000) >> 32); + return (v << 64) | (v >> 64); + } +} \ No newline at end of file diff --git a/erc20-bridge-token/contracts/BridgeTokenFactory.sol b/erc20-bridge-token/contracts/BridgeTokenFactory.sol index 17e35448..3a9fd7c7 100644 --- a/erc20-bridge-token/contracts/BridgeTokenFactory.sol +++ b/erc20-bridge-token/contracts/BridgeTokenFactory.sol @@ -4,17 +4,13 @@ pragma solidity ^0.8.24; import {AccessControlUpgradeable} from "@openzeppelin/contracts-upgradeable/access/AccessControlUpgradeable.sol"; import {ERC1967Proxy} from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"; import {UUPSUpgradeable} from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; +import {ECDSA} from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; -import "rainbow-bridge-sol/nearprover/contracts/INearProver.sol"; -import "rainbow-bridge-sol/nearprover/contracts/ProofDecoder.sol"; - -import "./ProofConsumer.sol"; import "./BridgeToken.sol"; -import "./ResultsDecoder.sol"; import "./SelectivePausableUpgradable.sol"; +import "./Borsh.sol"; contract BridgeTokenFactory is - ProofConsumer, UUPSUpgradeable, AccessControlUpgradeable, SelectivePausableUpgradable @@ -35,12 +31,28 @@ contract BridgeTokenFactory is bool private _isWhitelistModeEnabled; address public tokenImplementationAddress; + address public nearBridgeDerivedAddress; bytes32 public constant PAUSABLE_ADMIN_ROLE = keccak256("PAUSABLE_ADMIN_ROLE"); uint constant UNPAUSED_ALL = 0; uint constant PAUSED_WITHDRAW = 1 << 0; uint constant PAUSED_DEPOSIT = 1 << 1; + struct BridgeDeposit { + uint128 nonce; + string token; + uint128 amount; + address recipient; + address relayer; + } + + struct MetadataPayload { + string token; + string name; + string symbol; + uint8 decimals; + } + // Event when funds are withdrawn from Ethereum back to NEAR. event Withdraw( string token, @@ -60,21 +72,16 @@ contract BridgeTokenFactory is uint8 decimals ); + error InvalidSignature(); + // BridgeTokenFactory is linked to the bridge token factory on NEAR side. // It also links to the prover that it uses to unlock the tokens. function initialize( address _tokenImplementationAddress, - bytes memory _nearTokenLocker, - INearProver _prover, - uint64 _minBlockAcceptanceHeight + address _nearBridgeDerivedAddress ) external initializer { - require(_nearTokenLocker.length > 0, "Invalid Near Token Locker address"); - require(address(_prover) != address(0), "Invalid Near prover address"); - - nearTokenLocker = _nearTokenLocker; - prover = _prover; - minBlockAcceptanceHeight = _minBlockAcceptanceHeight; tokenImplementationAddress = _tokenImplementationAddress; + nearBridgeDerivedAddress = _nearBridgeDerivedAddress; __UUPSUpgradeable_init(); __AccessControl_init(); @@ -97,43 +104,44 @@ contract BridgeTokenFactory is return _nearToEthToken[nearTokenId]; } - function newBridgeToken( - bytes memory proofData, - uint64 proofBlockHeight - ) external returns (address) { - ProofDecoder.ExecutionStatus memory status = _parseAndConsumeProof( - proofData, - proofBlockHeight - ); - ResultsDecoder.MetadataResult memory result = ResultsDecoder.decodeMetadataResult( - status.successValue + function newBridgeToken(bytes calldata signatureData, MetadataPayload calldata metadata) external returns (address) { + bytes memory borshEncoded = bytes.concat( + Borsh.encodeString(metadata.token), + Borsh.encodeString(metadata.name), + Borsh.encodeString(metadata.symbol), + bytes1(metadata.decimals) ); + bytes32 hashed = keccak256(borshEncoded); + + if (ECDSA.recover(hashed, signatureData) != nearBridgeDerivedAddress) { + revert InvalidSignature(); + } - require(!_isBridgeToken[_nearToEthToken[result.token]], "ERR_TOKEN_EXIST"); + require(!_isBridgeToken[_nearToEthToken[metadata.token]], "ERR_TOKEN_EXIST"); address bridgeTokenProxy = address( new ERC1967Proxy( tokenImplementationAddress, abi.encodeWithSelector( BridgeToken.initialize.selector, - result.name, - result.symbol, - result.decimals + metadata.name, + metadata.symbol, + metadata.decimals ) ) ); emit SetMetadata( bridgeTokenProxy, - result.token, - result.name, - result.symbol, - result.decimals + metadata.token, + metadata.name, + metadata.symbol, + metadata.decimals ); _isBridgeToken[address(bridgeTokenProxy)] = true; - _ethToNearToken[address(bridgeTokenProxy)] = result.token; - _nearToEthToken[result.token] = address(bridgeTokenProxy); + _ethToNearToken[address(bridgeTokenProxy)] = metadata.token; + _nearToEthToken[metadata.token] = address(bridgeTokenProxy); return bridgeTokenProxy; } @@ -158,22 +166,27 @@ contract BridgeTokenFactory is ); } - function deposit( - bytes memory proofData, - uint64 proofBlockHeight - ) external whenNotPaused(PAUSED_DEPOSIT) { - ProofDecoder.ExecutionStatus memory status = _parseAndConsumeProof( - proofData, - proofBlockHeight - ); - ResultsDecoder.LockResult memory result = ResultsDecoder.decodeLockResult( - status.successValue + function deposit(bytes calldata signatureData, BridgeDeposit calldata bridgeDeposit) external whenNotPaused(PAUSED_DEPOSIT) { + bytes memory borshEncoded = bytes.concat( + Borsh.encodeUint128(bridgeDeposit.nonce), + Borsh.encodeString(bridgeDeposit.token), + Borsh.encodeUint128(bridgeDeposit.amount), + bytes1(0x00), // variant 1 in rust enum + Borsh.encodeAddress(bridgeDeposit.recipient), + bridgeDeposit.relayer == address(0) // None or Some(Address) in rust + ? bytes("\x00") + : bytes.concat(bytes("\x01"), Borsh.encodeAddress(bridgeDeposit.relayer)) ); + bytes32 hashed = keccak256(borshEncoded); + + if (ECDSA.recover(hashed, signatureData) != nearBridgeDerivedAddress) { + revert InvalidSignature(); + } - require(_isBridgeToken[_nearToEthToken[result.token]], "ERR_NOT_BRIDGE_TOKEN"); - BridgeToken(_nearToEthToken[result.token]).mint(result.recipient, result.amount); + require(_isBridgeToken[_nearToEthToken[bridgeDeposit.token]], "ERR_NOT_BRIDGE_TOKEN"); + BridgeToken(_nearToEthToken[bridgeDeposit.token]).mint(bridgeDeposit.recipient, bridgeDeposit.amount); - emit Deposit(result.token, result.amount, result.recipient); + emit Deposit(bridgeDeposit.token, bridgeDeposit.amount, bridgeDeposit.recipient); } function withdraw( diff --git a/erc20-bridge-token/contracts/ProofConsumer.sol b/erc20-bridge-token/contracts/ProofConsumer.sol deleted file mode 100644 index 5e3a8a8a..00000000 --- a/erc20-bridge-token/contracts/ProofConsumer.sol +++ /dev/null @@ -1,72 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity ^0.8.24; - -import "rainbow-bridge-sol/nearprover/contracts/INearProver.sol"; -import "rainbow-bridge-sol/nearprover/contracts/ProofDecoder.sol"; -import "rainbow-bridge-sol/nearbridge/contracts/Borsh.sol"; - -contract ProofConsumer { - using Borsh for Borsh.Data; - using ProofDecoder for Borsh.Data; - - INearProver public prover; - bytes public nearTokenLocker; - - /// Proofs from blocks that are below the acceptance height will be rejected. - // If `minBlockAcceptanceHeight` value is zero - proofs from block with any height are accepted. - uint64 public minBlockAcceptanceHeight; - - // OutcomeReciptId -> Used - mapping(bytes32 => bool) public usedProofs; - - /// Parses the provided proof and consumes it if it's not already used. - /// The consumed event cannot be reused for future calls. - function _parseAndConsumeProof( - bytes memory proofData, - uint64 proofBlockHeight - ) internal returns (ProofDecoder.ExecutionStatus memory result) { - require( - prover.proveOutcome(proofData, proofBlockHeight), - "Proof should be valid" - ); - - // Unpack the proof and extract the execution outcome. - Borsh.Data memory borshData = Borsh.from(proofData); - ProofDecoder.FullOutcomeProof memory fullOutcomeProof = borshData - .decodeFullOutcomeProof(); - borshData.done(); - - require( - fullOutcomeProof.block_header_lite.inner_lite.height >= - minBlockAcceptanceHeight, - "Proof is from the ancient block" - ); - - bytes32 receiptId = fullOutcomeProof - .outcome_proof - .outcome_with_id - .outcome - .receipt_ids[0]; - require(!usedProofs[receiptId], "The burn event proof cannot be reused"); - usedProofs[receiptId] = true; - - require( - keccak256( - fullOutcomeProof.outcome_proof.outcome_with_id.outcome.executor_id - ) == keccak256(nearTokenLocker), - "Can only unlock tokens/set metadata from the linked proof produced on Near blockchain" - ); - - result = fullOutcomeProof.outcome_proof.outcome_with_id.outcome.status; - require( - !result.failed, - "Can't use failed execution outcome for unlocking the tokens or set metadata" - ); - require( - !result.unknown, - "Can't use unknown execution outcome for unlocking the tokens or set metadata" - ); - } - - uint256[50] private __gap; -} diff --git a/erc20-bridge-token/contracts/ResultsDecoder.sol b/erc20-bridge-token/contracts/ResultsDecoder.sol deleted file mode 100644 index 323f4725..00000000 --- a/erc20-bridge-token/contracts/ResultsDecoder.sol +++ /dev/null @@ -1,51 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity ^0.8.24; -import "rainbow-bridge-sol/nearbridge/contracts/Borsh.sol"; - -library ResultsDecoder { - using Borsh for Borsh.Data; - bytes32 public constant RESULT_PREFIX_LOCK = - 0x0a9eb877458579dbce83ea57d556be50d1c3160bb5f1719fb172bd3300ac8623; - bytes32 public constant RESULT_PREFIX_METADATA = - 0xb315d4d6e8f235f5fabb0b1a0f118507f6c8542fae8e1a9566abe60762047c16; - - struct LockResult { - string token; - uint128 amount; - address recipient; - } - struct MetadataResult { - string token; - string name; - string symbol; - uint8 decimals; - uint64 blockHeight; - } - - function decodeLockResult( - bytes memory data - ) internal pure returns (LockResult memory result) { - Borsh.Data memory borshData = Borsh.from(data); - bytes32 prefix = borshData.decodeBytes32(); - require(prefix == RESULT_PREFIX_LOCK, "ERR_INVALID_LOCK_PREFIX"); - result.token = string(borshData.decodeBytes()); - result.amount = borshData.decodeU128(); - bytes20 recipient = borshData.decodeBytes20(); - result.recipient = address(uint160(recipient)); - borshData.done(); - } - - function decodeMetadataResult( - bytes memory data - ) internal pure returns (MetadataResult memory result) { - Borsh.Data memory borshData = Borsh.from(data); - bytes32 prefix = borshData.decodeBytes32(); - require(prefix == RESULT_PREFIX_METADATA, "ERR_INVALID_METADATA_PREFIX"); - result.token = string(borshData.decodeBytes()); - result.name = string(borshData.decodeBytes()); - result.symbol = string(borshData.decodeBytes()); - result.decimals = borshData.decodeU8(); - result.blockHeight = borshData.decodeU64(); - borshData.done(); - } -} diff --git a/erc20-bridge-token/contracts/test/NearProverMock.sol b/erc20-bridge-token/contracts/test/NearProverMock.sol deleted file mode 100644 index c274d915..00000000 --- a/erc20-bridge-token/contracts/test/NearProverMock.sol +++ /dev/null @@ -1,13 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity ^0.8.24; - -import "rainbow-bridge-sol/nearprover/contracts/INearProver.sol"; - -contract NearProverMock is INearProver { - function proveOutcome( - bytes memory /*proofData*/, - uint64 /*blockHeight*/ - ) external pure override returns (bool) { - return true; - } -} From 8aaa89dc2d19e805f299f5e858236b3ae822967a Mon Sep 17 00:00:00 2001 From: kiseln <3428059+kiseln@users.noreply.github.com> Date: Wed, 4 Sep 2024 20:43:15 +0400 Subject: [PATCH 2/3] Fix tests for omni bridge --- erc20-bridge-token/test/BridgeToken.js | 494 +++++++++++-------------- erc20-bridge-token/test/helpers.js | 79 ---- erc20-bridge-token/test/signatures.js | 79 ++++ 3 files changed, 285 insertions(+), 367 deletions(-) delete mode 100644 erc20-bridge-token/test/helpers.js create mode 100644 erc20-bridge-token/test/signatures.js diff --git a/erc20-bridge-token/test/BridgeToken.js b/erc20-bridge-token/test/BridgeToken.js index ac2ef2e3..d2a13818 100644 --- a/erc20-bridge-token/test/BridgeToken.js +++ b/erc20-bridge-token/test/BridgeToken.js @@ -1,8 +1,6 @@ const { expect } = require('chai') const { ethers, upgrades } = require('hardhat') -const { serialize } = require('rainbow-bridge-lib/borsh.js'); -const { borshifyOutcomeProof } = require('rainbow-bridge-lib/borshify-proof.js'); -const { SCHEMA, createEmptyToken, generateRandomBase58, RESULT_PREFIX_LOCK } = require('./helpers.js'); +const { metadataSignature, depositSignature } = require('./signatures') const WhitelistMode = { NotInitialized: 0, @@ -18,100 +16,76 @@ const PauseMode = { } describe('BridgeToken', () => { - const nearTokenId = 'nearfuntoken' - const minBlockAcceptanceHeight = 0 + const wrappedNearId = 'wrap.testnet' let BridgeTokenInstance let BridgeTokenFactory let adminAccount - let user + let user1 + let user2 beforeEach(async function () { - [adminAccount, user] = await ethers.getSigners() + [adminAccount] = await ethers.getSigners() + user1 = await ethers.getImpersonatedSigner('0x3A445243376C32fAba679F63586e236F77EA601e') + user2 = await ethers.getImpersonatedSigner('0x5a08feed678c056650b3eb4a5cb1b9bb6f0fe265'); + + await fundAddress(user1.address, "1"); + await fundAddress(user2.address, "1"); BridgeTokenInstance = await ethers.getContractFactory('BridgeToken') const bridgeToken = await BridgeTokenInstance.deploy() await bridgeToken.waitForDeployment() - const proverMock = await (await ethers.getContractFactory('NearProverMock')).deploy() - await proverMock.waitForDeployment() - BridgeTokenFactory = await ethers.getContractFactory('BridgeTokenFactory') BridgeTokenFactory = await upgrades.deployProxy(BridgeTokenFactory, [ await bridgeToken.getAddress(), - Buffer.from('nearfuntoken', 'utf-8'), - await proverMock.getAddress(), - minBlockAcceptanceHeight + "0xa966f32b64caaee9211d674e698cb72100b5e792" ], { initializer: 'initialize' }); await BridgeTokenFactory.waitForDeployment(); }) - function getProofTemplate() { - return { - proof: require("./proof_template.json"), - proofBlockHeight: 1089, - }; - } - - async function createToken(nearTokenId) { - const tokenInfo = await createEmptyToken( - nearTokenId, - BridgeTokenFactory, - BridgeTokenInstance - ); - - return tokenInfo; + async function fundAddress(address, amount) { + const tx = await adminAccount.sendTransaction({ + to: address, + value: ethers.parseEther(amount) + }); + await tx.wait(); } - async function deposit(nearTokenId, amountToLock, recipientAddress) { - const { proof: lockResultProof, proofBlockHeight } = getProofTemplate(); - lockResultProof.outcome_proof.outcome.status.SuccessValue = serialize( - SCHEMA, - "LockResult", - { - prefix: RESULT_PREFIX_LOCK, - token: nearTokenId, - amount: amountToLock, - recipient: ethers.getBytes(recipientAddress), - } - ).toString("base64"); - lockResultProof.outcome_proof.outcome.receipt_ids[0] = generateRandomBase58(64); - await BridgeTokenFactory.deposit( - borshifyOutcomeProof(lockResultProof), - proofBlockHeight - ); + async function createToken(tokenId) { + const { signature, payload } = metadataSignature(tokenId); + + await BridgeTokenFactory.newBridgeToken(signature, payload); + const tokenProxyAddress = await BridgeTokenFactory.nearToEthToken(tokenId) + const token = BridgeTokenInstance.attach(tokenProxyAddress) + return { tokenProxyAddress, token } } - it('can create an empty token', async function () { - await createEmptyToken(nearTokenId, BridgeTokenFactory, BridgeTokenInstance) - const tokenProxyAddress = await BridgeTokenFactory.nearToEthToken(nearTokenId) + it('can create a token', async function () { + await createToken(wrappedNearId); + const tokenProxyAddress = await BridgeTokenFactory.nearToEthToken(wrappedNearId) const token = BridgeTokenInstance.attach(tokenProxyAddress) - expect(await token.name()).to.be.equal('NEAR ERC20') - expect(await token.symbol()).to.be.equal('NEAR') - expect((await token.decimals()).toString()).to.be.equal('18') + expect(await token.name()).to.be.equal('Wrapped NEAR fungible token') + expect(await token.symbol()).to.be.equal('wNEAR') + expect((await token.decimals()).toString()).to.be.equal('24') }) it('can\'t create token if token already exists', async function () { - await createEmptyToken(nearTokenId, BridgeTokenFactory, BridgeTokenInstance) - await expect( - createEmptyToken(nearTokenId, BridgeTokenFactory, BridgeTokenInstance) - ).to.be.revertedWith('ERR_TOKEN_EXIST') + await createToken(wrappedNearId); + await expect(createToken(wrappedNearId)) + .to.be.revertedWith('ERR_TOKEN_EXIST') }) it("can update token's metadata", async function() { - const { token } = await createEmptyToken( - nearTokenId, - BridgeTokenFactory, - BridgeTokenInstance - ); + const { token } = await createToken(wrappedNearId); - await BridgeTokenFactory.setMetadata(nearTokenId, 'Circle USDC Bridged', 'USDC.E'); + await BridgeTokenFactory.setMetadata(wrappedNearId, 'Circle USDC Bridged', 'USDC.E'); expect(await token.name()).to.equal('Circle USDC Bridged'); expect(await token.symbol()).to.equal('USDC.E'); }); it('can\'t update metadata of non-existent token', async function () { - await createEmptyToken(nearTokenId, BridgeTokenFactory, BridgeTokenInstance) + await createToken(wrappedNearId); await expect( BridgeTokenFactory.setMetadata('non-existing', 'Circle USDC', 'USDC') @@ -119,65 +93,38 @@ describe('BridgeToken', () => { }) it('can\'t update metadata as a normal user', async function () { - await createEmptyToken(nearTokenId, BridgeTokenFactory, BridgeTokenInstance) + await createToken(wrappedNearId); await expect( - BridgeTokenFactory.connect(user).setMetadata(nearTokenId, 'Circle USDC', 'USDC') + BridgeTokenFactory.connect(user1).setMetadata(wrappedNearId, 'Circle USDC', 'USDC') ).to.be.revertedWithCustomError(BridgeTokenFactory, 'AccessControlUnauthorizedAccount'); }) it('deposit token', async function () { - const { token } = await createEmptyToken( - nearTokenId, - BridgeTokenFactory, - BridgeTokenInstance - ); - - const amountToTransfer = 100; - const { proof: lockResultProof, proofBlockHeight } = getProofTemplate(); - lockResultProof.outcome_proof.outcome.status.SuccessValue = - serialize( - SCHEMA, 'LockResult', { - prefix: RESULT_PREFIX_LOCK, - token: nearTokenId, - amount: amountToTransfer, - recipient: ethers.getBytes(user.address), - } - ) - .toString('base64'); + const { token } = await createToken(wrappedNearId); - lockResultProof.outcome_proof.outcome.receipt_ids[0] = 'B'.repeat(44); + const { signature, payload } = depositSignature(wrappedNearId, user1.address); - // Deposit and verify event is emitted await expect( - BridgeTokenFactory - .deposit(borshifyOutcomeProof(lockResultProof), proofBlockHeight) + BridgeTokenFactory + .deposit(signature, payload) ) .to .emit(BridgeTokenFactory, 'Deposit') - .withArgs(nearTokenId, amountToTransfer, user.address); + .withArgs(wrappedNearId, 1, payload.recipient); expect( - (await token.balanceOf(user.address)) + (await token.balanceOf(payload.recipient)) .toString() ) .to .be - .equal(amountToTransfer.toString()) + .equal(payload.amount.toString()) }) it('can\'t deposit if the contract is paused', async function () { - await createEmptyToken(nearTokenId, BridgeTokenFactory, BridgeTokenInstance) - - const amountToTransfer = 100; - const { proof: lockResultProof, proofBlockHeight } = getProofTemplate(); - lockResultProof.outcome_proof.outcome.status.SuccessValue = serialize(SCHEMA, 'LockResult', { - prefix: RESULT_PREFIX_LOCK, - token: nearTokenId, - amount: amountToTransfer, - recipient: ethers.getBytes(adminAccount.address), - }).toString('base64'); - lockResultProof.outcome_proof.outcome.receipt_ids[0] = 'C'.repeat(44); + await createToken(wrappedNearId); + await expect ( BridgeTokenFactory.pauseDeposit() ) @@ -185,73 +132,60 @@ describe('BridgeToken', () => { .emit(BridgeTokenFactory, 'Paused') .withArgs(adminAccount.address, PauseMode.PausedDeposit); + const { signature, payload } = depositSignature(wrappedNearId, user1.address); + await expect( - BridgeTokenFactory.deposit(borshifyOutcomeProof(lockResultProof), proofBlockHeight) + BridgeTokenFactory + .deposit(signature, payload) ) .to .be .revertedWith('Pausable: paused'); }) + it('withdraw token', async function () { - const { token } = await createEmptyToken(nearTokenId, BridgeTokenFactory, BridgeTokenInstance) - - const amountToTransfer = 100; - const recipient = "testrecipient.near"; - const { proof: lockResultProof, proofBlockHeight } = getProofTemplate(); - lockResultProof.outcome_proof.outcome.status.SuccessValue = serialize(SCHEMA, 'LockResult', { - prefix: RESULT_PREFIX_LOCK, - token: nearTokenId, - amount: amountToTransfer, - recipient: ethers.getBytes(user.address), - }).toString('base64'); - lockResultProof.outcome_proof.outcome.receipt_ids[0] = 'D'.repeat(44); - - await BridgeTokenFactory.setTokenWhitelistMode(nearTokenId, WhitelistMode.CheckToken); + const { token } = await createToken(wrappedNearId); + + await BridgeTokenFactory.setTokenWhitelistMode(wrappedNearId, WhitelistMode.CheckToken); expect( - await BridgeTokenFactory.getTokenWhitelistMode(nearTokenId) + await BridgeTokenFactory.getTokenWhitelistMode(wrappedNearId) ).to.be.equal(WhitelistMode.CheckToken); - await BridgeTokenFactory.deposit(borshifyOutcomeProof(lockResultProof), proofBlockHeight); + const { signature, payload } = depositSignature(wrappedNearId, user1.address); + await BridgeTokenFactory.deposit(signature, payload); + const recipient = 'testrecipient.near'; await expect( - BridgeTokenFactory.connect(user).withdraw( - nearTokenId, - amountToTransfer, + BridgeTokenFactory.connect(user1).withdraw( + wrappedNearId, + payload.amount, recipient ) ) .to .emit(BridgeTokenFactory, "Withdraw") .withArgs( - nearTokenId, - user.address, - amountToTransfer, + wrappedNearId, + user1.address, + payload.amount, recipient, - await BridgeTokenFactory.nearToEthToken(nearTokenId) + await BridgeTokenFactory.nearToEthToken(wrappedNearId) ); - expect((await token.balanceOf(user.address)).toString()).to.be.equal('0') + expect((await token.balanceOf(user1.address)).toString()).to.be.equal('0') }) it('cant withdraw token when paused', async function () { - await createEmptyToken(nearTokenId, BridgeTokenFactory, BridgeTokenInstance) - - const amountToTransfer = 100; - const { proof: lockResultProof, proofBlockHeight } = getProofTemplate(); - lockResultProof.outcome_proof.outcome.status.SuccessValue = serialize(SCHEMA, 'LockResult', { - prefix: RESULT_PREFIX_LOCK, - token: nearTokenId, - amount: amountToTransfer, - recipient: ethers.getBytes(user.address), - }).toString('base64'); - lockResultProof.outcome_proof.outcome.receipt_ids[0] = 'F'.repeat(44); - - await BridgeTokenFactory.setTokenWhitelistMode(nearTokenId, WhitelistMode.CheckToken); + await createToken(wrappedNearId); + + await BridgeTokenFactory.setTokenWhitelistMode(wrappedNearId, WhitelistMode.CheckToken); expect( - await BridgeTokenFactory.getTokenWhitelistMode(nearTokenId) + await BridgeTokenFactory.getTokenWhitelistMode(wrappedNearId) ).to.be.equal(WhitelistMode.CheckToken); - await BridgeTokenFactory.deposit(borshifyOutcomeProof(lockResultProof), proofBlockHeight); + const { signature, payload } = depositSignature(wrappedNearId, user1.address); + await BridgeTokenFactory.deposit(signature, payload); + await expect( BridgeTokenFactory.pauseWithdraw() ) @@ -259,7 +193,7 @@ describe('BridgeToken', () => { .emit(BridgeTokenFactory, 'Paused') .withArgs(adminAccount.address, PauseMode.PausedWithdraw); await expect( - BridgeTokenFactory.withdraw(nearTokenId, amountToTransfer, 'testrecipient.near') + BridgeTokenFactory.withdraw(wrappedNearId, payload.amount, 'testrecipient.near') ) .to .be @@ -267,24 +201,16 @@ describe('BridgeToken', () => { }) it('can deposit and withdraw after unpausing', async function () { - const { token } = await createEmptyToken(nearTokenId, BridgeTokenFactory, BridgeTokenInstance) - - const amountToTransfer = 100; - const { proof: lockResultProof, proofBlockHeight } = getProofTemplate(); - lockResultProof.outcome_proof.outcome.status.SuccessValue = serialize(SCHEMA, 'LockResult', { - prefix: RESULT_PREFIX_LOCK, - token: nearTokenId, - amount: amountToTransfer, - recipient: ethers.getBytes(user.address), - }).toString('base64'); - lockResultProof.outcome_proof.outcome.receipt_ids[0] = 'G'.repeat(44); - - await BridgeTokenFactory.setTokenWhitelistMode(nearTokenId, WhitelistMode.CheckToken); + const { token } = await createToken(wrappedNearId); + + await BridgeTokenFactory.setTokenWhitelistMode(wrappedNearId, WhitelistMode.CheckToken); expect( - await BridgeTokenFactory.getTokenWhitelistMode(nearTokenId) + await BridgeTokenFactory.getTokenWhitelistMode(wrappedNearId) ).to.be.equal(WhitelistMode.CheckToken); - await BridgeTokenFactory.deposit(borshifyOutcomeProof(lockResultProof), proofBlockHeight); + const { signature, payload } = depositSignature(wrappedNearId, user1.address); + await BridgeTokenFactory.deposit(signature, payload); + await expect( BridgeTokenFactory.pauseWithdraw() ) @@ -292,13 +218,6 @@ describe('BridgeToken', () => { .emit(BridgeTokenFactory, 'Paused') .withArgs(adminAccount.address, PauseMode.PausedWithdraw); - await expect( - BridgeTokenFactory.connect(user).withdraw(nearTokenId, amountToTransfer, 'testrecipient.near') - ) - .to - .be - .revertedWith('Pausable: paused'); - await expect( BridgeTokenFactory.pause(PauseMode.UnpausedAll) ) @@ -306,60 +225,39 @@ describe('BridgeToken', () => { .emit(BridgeTokenFactory, 'Paused') .withArgs(adminAccount.address, PauseMode.UnpausedAll); - - await BridgeTokenFactory.connect(user).withdraw(nearTokenId, amountToTransfer, 'testrecipient.near') - expect((await token.balanceOf(user.address)).toString()).to.be.equal('0') + const recipient = 'testrecipient.near'; + await BridgeTokenFactory.connect(user1).withdraw( + wrappedNearId, + payload.amount, + recipient + ); + + expect((await token.balanceOf(user1.address)).toString()).to.be.equal('0') }) it('upgrade token contract', async function () { - const { tokenProxyAddress, token } = await createEmptyToken(nearTokenId, BridgeTokenFactory, BridgeTokenInstance) - - const amountToTransfer = 100; - const { proof: lockResultProof, proofBlockHeight } = getProofTemplate(); - lockResultProof.outcome_proof.outcome.status.SuccessValue = serialize(SCHEMA, 'LockResult', { - prefix: RESULT_PREFIX_LOCK, - token: nearTokenId, - amount: amountToTransfer, - recipient: ethers.getBytes(user.address), - }).toString('base64'); - lockResultProof.outcome_proof.outcome.receipt_ids[0] = 'B'.repeat(44); - await BridgeTokenFactory.deposit(borshifyOutcomeProof(lockResultProof), proofBlockHeight); - - expect((await token.balanceOf(user.address)).toString()).to.be.equal(amountToTransfer.toString()) + const { tokenProxyAddress } = await createToken(wrappedNearId); const BridgeTokenV2Instance = await ethers.getContractFactory("TestBridgeToken"); const BridgeTokenV2 = await BridgeTokenV2Instance.deploy(); await BridgeTokenV2.waitForDeployment(); - await BridgeTokenFactory.upgradeToken(nearTokenId, await BridgeTokenV2.getAddress()) + await BridgeTokenFactory.upgradeToken(wrappedNearId, await BridgeTokenV2.getAddress()) const BridgeTokenV2Proxied = BridgeTokenV2Instance.attach(tokenProxyAddress) expect(await BridgeTokenV2Proxied.returnTestString()).to.equal('test') - expect(await BridgeTokenV2Proxied.name()).to.equal('NEAR ERC20') - expect(await BridgeTokenV2Proxied.symbol()).to.equal('NEAR') - expect((await BridgeTokenV2Proxied.decimals()).toString()).to.equal('18') + expect(await BridgeTokenV2Proxied.name()).to.equal('Wrapped NEAR fungible token') + expect(await BridgeTokenV2Proxied.symbol()).to.equal('wNEAR') + expect((await BridgeTokenV2Proxied.decimals()).toString()).to.equal('24') }) it('user cant upgrade token contract', async function () { - const amountToTransfer = 100; - const { token } = await createEmptyToken(nearTokenId, BridgeTokenFactory, BridgeTokenInstance) - - const { proof: lockResultProof, proofBlockHeight } = getProofTemplate(); - lockResultProof.outcome_proof.outcome.status.SuccessValue = serialize(SCHEMA, 'LockResult', { - prefix: RESULT_PREFIX_LOCK, - token: nearTokenId, - amount: amountToTransfer, - recipient: ethers.getBytes(user.address), - }).toString('base64'); - lockResultProof.outcome_proof.outcome.receipt_ids[0] = 'C'.repeat(44); - await BridgeTokenFactory.deposit(borshifyOutcomeProof(lockResultProof), proofBlockHeight); - - expect((await token.balanceOf(user.address)).toString()).to.be.equal(amountToTransfer.toString()) + await createToken(wrappedNearId); const BridgeTokenV2Instance = await ethers.getContractFactory("TestBridgeToken"); const BridgeTokenV2 = await BridgeTokenV2Instance.deploy(); await BridgeTokenV2.waitForDeployment(); - await expect(BridgeTokenFactory.connect(user).upgradeToken(nearTokenId, await BridgeTokenV2.getAddress())) + await expect(BridgeTokenFactory.connect(user1).upgradeToken(wrappedNearId, await BridgeTokenV2.getAddress())) .to.be.revertedWithCustomError(BridgeTokenFactory, 'AccessControlUnauthorizedAccount'); }) @@ -536,83 +434,90 @@ describe('BridgeToken', () => { }); describe("Whitelist", function() { - let tokenInfo; - const recipient = "testrecipient.near"; - const amountToLock = 100; - beforeEach(async function() { - BridgeTokenFactory.enableWhitelistMode() - tokenInfo = await createToken(nearTokenId); - await deposit(nearTokenId, amountToLock, user.address); + await BridgeTokenFactory.enableWhitelistMode() }); it("Test account in whitelist", async function() { - const amountToTransfer = amountToLock; - await BridgeTokenFactory.setTokenWhitelistMode(nearTokenId, WhitelistMode.CheckAccountAndToken); + const tokenInfo = await createToken(wrappedNearId); + + const { signature, payload } = depositSignature(wrappedNearId, user1.address); + await BridgeTokenFactory.deposit(signature, payload); + + const recipient = payload.recipient; + const amountToTransfer = payload.amount; + + await BridgeTokenFactory.setTokenWhitelistMode(wrappedNearId, WhitelistMode.CheckAccountAndToken); expect( - await BridgeTokenFactory.getTokenWhitelistMode(nearTokenId) + await BridgeTokenFactory.getTokenWhitelistMode(wrappedNearId) ).to.be.equal(WhitelistMode.CheckAccountAndToken); await BridgeTokenFactory.addAccountToWhitelist( - nearTokenId, - user.address + wrappedNearId, + user1.address ); expect( await BridgeTokenFactory.isAccountWhitelistedForToken( - nearTokenId, - user.address + wrappedNearId, + user1.address ) ).to.be.true; - await BridgeTokenFactory.connect(user).withdraw(nearTokenId, amountToTransfer, recipient); + await BridgeTokenFactory.connect(user1).withdraw(wrappedNearId, amountToTransfer, recipient); expect( - (await tokenInfo.token.balanceOf(user.address)).toString() + (await tokenInfo.token.balanceOf(user1.address)).toString() ).to.be.equal("0"); }); it("Test token in whitelist", async function() { - const amountToTransfer = amountToLock; - await BridgeTokenFactory.setTokenWhitelistMode(nearTokenId, WhitelistMode.CheckToken); + const tokenInfo = await createToken(wrappedNearId); + + const { signature, payload } = depositSignature(wrappedNearId, user1.address); + await BridgeTokenFactory.deposit(signature, payload); + + const recipient = payload.recipient; + const amountToTransfer = payload.amount; + + await BridgeTokenFactory.setTokenWhitelistMode(wrappedNearId, WhitelistMode.CheckToken); expect( - await BridgeTokenFactory.getTokenWhitelistMode(nearTokenId) + await BridgeTokenFactory.getTokenWhitelistMode(wrappedNearId) ).to.be.equal(WhitelistMode.CheckToken); - await BridgeTokenFactory.connect(user).withdraw(nearTokenId, amountToTransfer, recipient); + await BridgeTokenFactory.connect(user1).withdraw(wrappedNearId, amountToTransfer, recipient); expect( - (await tokenInfo.token.balanceOf(user.address)).toString() + (await tokenInfo.token.balanceOf(user1.address)).toString() ).to.be.equal("0"); }); it("Test multiple tokens", async function() { - const amountToTransfer = amountToLock; const whitelistTokens = [ - "near-token1.near", - "near-token2.near", - "near-token3.near", - "near-token4.near", + "wrap.testnet", + "token-bridge-test.testnet", ]; const blacklistTokens = [ - "near-token5.near", - "near-token6.near", - "near-token7.near", - "near-token8.near", + "blacklisted1.testnet", + "blacklisted2.testnet", ]; - const tokensInfo = []; for (token of whitelistTokens) { - tokensInfo.push(await createToken(token)); - await deposit(token, amountToTransfer, user.address); + await createToken(token); + + const { signature, payload } = depositSignature(token, user1.address); + await BridgeTokenFactory.deposit(signature, payload); + await BridgeTokenFactory.setTokenWhitelistMode(token, WhitelistMode.CheckToken); expect( await BridgeTokenFactory.getTokenWhitelistMode(token) ).to.be.equal(WhitelistMode.CheckToken); } + const amountToWithdraw = 1; + const recipient = "testrecipient.near"; for (token of blacklistTokens) { await expect( - BridgeTokenFactory.connect(user).withdraw( + BridgeTokenFactory.connect(user1).withdraw( token, - amountToTransfer, + amountToWithdraw, recipient ) ).to.be.revertedWith("ERR_NOT_INITIALIZED_WHITELIST_TOKEN"); @@ -620,9 +525,9 @@ describe('BridgeToken', () => { for (token of whitelistTokens) { await expect( - BridgeTokenFactory.connect(user).withdraw( + BridgeTokenFactory.connect(user1).withdraw( token, - amountToTransfer, + amountToWithdraw, recipient ) ) @@ -630,33 +535,27 @@ describe('BridgeToken', () => { .emit(BridgeTokenFactory, "Withdraw") .withArgs( token, - user.address, - amountToTransfer, + user1.address, + amountToWithdraw, recipient, await BridgeTokenFactory.nearToEthToken(token) ); } - - for (tokenInfo of tokensInfo) { - expect( - (await tokenInfo.token.balanceOf(user.address)).toString() - ).to.be.equal("0"); - } }); it("Test multiple accounts", async function() { - const amountToTransfer = amountToLock; const whitelistTokens = [ - "near-token1.near", - "near-token2.near", - "near-token3.near", - "near-token4.near", + "wrap.testnet", + "token-bridge-test.testnet", + ]; + + const whitelistAccounts = [ + user1, + user2 ]; const signers = await ethers.getSigners(); - const numOfSigners = 3; - const whitelistAccounts = signers.slice(0, numOfSigners); - const blacklistAccounts = signers.slice(numOfSigners, numOfSigners * 2); + const blacklistAccounts = signers.slice(0, 2); const tokensInfo = []; for (token of whitelistTokens) { @@ -667,7 +566,9 @@ describe('BridgeToken', () => { ).to.be.equal(WhitelistMode.CheckAccountAndToken); for (const account of whitelistAccounts) { - await deposit(token, amountToTransfer, account.address); + const { signature, payload } = depositSignature(token, account.address); + await BridgeTokenFactory.deposit(signature, payload); + await BridgeTokenFactory.addAccountToWhitelist( token, account.address @@ -681,12 +582,14 @@ describe('BridgeToken', () => { } } + const amountToWithdraw = 1; + const recipient = "testrecipient.near"; for (token of whitelistTokens) { for (const account of whitelistAccounts) { await expect( BridgeTokenFactory.connect(account).withdraw( token, - amountToTransfer, + amountToWithdraw, recipient ) ) @@ -695,7 +598,7 @@ describe('BridgeToken', () => { .withArgs( token, account.address, - amountToTransfer, + amountToWithdraw, recipient, await BridgeTokenFactory.nearToEthToken(token) ); @@ -705,7 +608,7 @@ describe('BridgeToken', () => { await expect( BridgeTokenFactory.connect(account).withdraw( token, - amountToTransfer, + amountToWithdraw, recipient ) ).revertedWith("ERR_ACCOUNT_NOT_IN_WHITELIST"); @@ -714,96 +617,111 @@ describe('BridgeToken', () => { }); it("Test remove account from whitelist", async function() { - const amountToTransfer = amountToLock / 2; - await BridgeTokenFactory.setTokenWhitelistMode(nearTokenId, WhitelistMode.CheckAccountAndToken); + const tokenInfo = await createToken(wrappedNearId); + + const { signature, payload } = depositSignature(wrappedNearId, user2.address); + await BridgeTokenFactory.deposit(signature, payload); + + await BridgeTokenFactory.setTokenWhitelistMode(wrappedNearId, WhitelistMode.CheckAccountAndToken); expect( - await BridgeTokenFactory.getTokenWhitelistMode(nearTokenId) + await BridgeTokenFactory.getTokenWhitelistMode(wrappedNearId) ).to.be.equal(WhitelistMode.CheckAccountAndToken); await BridgeTokenFactory.addAccountToWhitelist( - nearTokenId, - user.address + wrappedNearId, + user2.address ); expect( await BridgeTokenFactory.isAccountWhitelistedForToken( - nearTokenId, - user.address + wrappedNearId, + user2.address ) ).to.be.true; - await BridgeTokenFactory.connect(user).withdraw(nearTokenId, amountToTransfer, recipient); + const amountToWithdraw = 10; + const recipient = "testrecipient.near"; + + await BridgeTokenFactory.connect(user2).withdraw(wrappedNearId, amountToWithdraw, recipient); - await BridgeTokenFactory.removeAccountFromWhitelist(nearTokenId, adminAccount.address); + await BridgeTokenFactory.removeAccountFromWhitelist(wrappedNearId, user2.address); expect( await BridgeTokenFactory.isAccountWhitelistedForToken( - nearTokenId, - adminAccount.address + wrappedNearId, + user2.address ) ).to.be.false; await expect( - BridgeTokenFactory.withdraw(nearTokenId, amountToTransfer, recipient) + BridgeTokenFactory.connect(user2).withdraw(wrappedNearId, amountToWithdraw, recipient) ).to.be.revertedWith("ERR_ACCOUNT_NOT_IN_WHITELIST"); expect( - (await tokenInfo.token.balanceOf(user.address)).toString() - ).to.be.equal(amountToTransfer.toString()); + (await tokenInfo.token.balanceOf(user2.address)).toString() + ).to.be.equal((payload.amount - amountToWithdraw).toString()); }); it("Test token or account not in whitelist", async function() { - const amountToTransfer = amountToLock / 2; + const tokenId = "token-bridge-test.testnet"; + const tokenInfo = await createToken(tokenId); + + const { signature, payload } = depositSignature(tokenId, user2.address); + await BridgeTokenFactory.deposit(signature, payload); + + const amountToWithdraw = payload.amount / 2; + const recipient = "testrecipient.near"; + await expect( - BridgeTokenFactory.withdraw(nearTokenId, amountToTransfer, recipient) + BridgeTokenFactory.withdraw(tokenId, amountToWithdraw, recipient) ).to.be.revertedWith("ERR_NOT_INITIALIZED_WHITELIST_TOKEN"); - await BridgeTokenFactory.setTokenWhitelistMode(nearTokenId, WhitelistMode.Blocked); + await BridgeTokenFactory.setTokenWhitelistMode(tokenId, WhitelistMode.Blocked); expect( - await BridgeTokenFactory.getTokenWhitelistMode(nearTokenId) + await BridgeTokenFactory.getTokenWhitelistMode(tokenId) ).to.be.equal(WhitelistMode.Blocked); await expect( - BridgeTokenFactory.withdraw(nearTokenId, amountToTransfer, recipient) + BridgeTokenFactory.withdraw(tokenId, amountToWithdraw, recipient) ).to.be.revertedWith("ERR_WHITELIST_TOKEN_BLOCKED"); - await BridgeTokenFactory.setTokenWhitelistMode(nearTokenId, WhitelistMode.CheckAccountAndToken); + await BridgeTokenFactory.setTokenWhitelistMode(tokenId, WhitelistMode.CheckAccountAndToken); expect( - await BridgeTokenFactory.getTokenWhitelistMode(nearTokenId) + await BridgeTokenFactory.getTokenWhitelistMode(tokenId) ).to.be.equal(WhitelistMode.CheckAccountAndToken); await expect( - BridgeTokenFactory.withdraw(nearTokenId, amountToTransfer, recipient) + BridgeTokenFactory.withdraw(tokenId, amountToWithdraw, recipient) ).to.be.revertedWith("ERR_ACCOUNT_NOT_IN_WHITELIST"); // Disable whitelist mode await BridgeTokenFactory.disableWhitelistMode(); expect(await BridgeTokenFactory.isWhitelistModeEnabled()).to.be.false; - await BridgeTokenFactory.connect(user).withdraw(nearTokenId, amountToTransfer, recipient); + await BridgeTokenFactory.connect(user2).withdraw(tokenId, amountToWithdraw, recipient); expect( - (await tokenInfo.token.balanceOf(user.address)).toString() - ).to.be.equal(amountToTransfer.toString()); + (await tokenInfo.token.balanceOf(user2.address)).toString() + ).to.be.equal(amountToWithdraw.toString()); // Enable whitelist mode await BridgeTokenFactory.enableWhitelistMode(); expect(await BridgeTokenFactory.isWhitelistModeEnabled()).to.be.true; await expect( - BridgeTokenFactory.withdraw(nearTokenId, amountToTransfer, recipient) + BridgeTokenFactory.withdraw(tokenId, amountToWithdraw, recipient) ).to.be.revertedWith("ERR_ACCOUNT_NOT_IN_WHITELIST"); await BridgeTokenFactory.addAccountToWhitelist( - nearTokenId, - user.address + tokenId, + user2.address ); expect( await BridgeTokenFactory.isAccountWhitelistedForToken( - nearTokenId, - user.address + tokenId, + user2.address ) ).to.be.true; - await BridgeTokenFactory.connect(user).withdraw(nearTokenId, amountToTransfer, recipient); + await BridgeTokenFactory.connect(user2).withdraw(tokenId, amountToWithdraw, recipient); expect( - (await tokenInfo.token.balanceOf(user.address)).toString() + (await tokenInfo.token.balanceOf(user2.address)).toString() ).to.be.equal("0"); }); }); diff --git a/erc20-bridge-token/test/helpers.js b/erc20-bridge-token/test/helpers.js deleted file mode 100644 index bcaefd1d..00000000 --- a/erc20-bridge-token/test/helpers.js +++ /dev/null @@ -1,79 +0,0 @@ -const { serialize } = require('rainbow-bridge-lib/borsh.js'); -const { borshifyOutcomeProof } = require('rainbow-bridge-lib/borshify-proof.js'); - -const SCHEMA = { - 'SetMetadataResult': { - kind: 'struct', fields: [ - ['prefix', [32]], - ['token', 'string'], - ['name', 'string'], - ['symbol', 'string'], - ['decimals', 'u8'], - ['blockHeight', 'u64'], - ] - }, - 'LockResult': { - kind: 'struct', fields: [ - ['prefix', [32]], - ['token', 'string'], - ['amount', 'u128'], - ['recipient', [20]], - ] - } -}; - -const ADMIN_ROLE = '0x0000000000000000000000000000000000000000000000000000000000000000'; -const RESULT_PREFIX_LOCK = Buffer.from("0a9eb877458579dbce83ea57d556be50d1c3160bb5f1719fb172bd3300ac8623", "hex"); -const RESULT_PREFIX_METADATA = Buffer.from("b315d4d6e8f235f5fabb0b1a0f118507f6c8542fae8e1a9566abe60762047c16", "hex"); - -const createEmptyToken = async (nearTokenId, BridgeTokenFactory, BridgeTokenInstance) => { - const { metadataProof, proofBlockHeight } = getMetadataProof(nearTokenId) - await BridgeTokenFactory.newBridgeToken(borshifyOutcomeProof(metadataProof), proofBlockHeight) - const tokenProxyAddress = await BridgeTokenFactory.nearToEthToken(nearTokenId) - const token = BridgeTokenInstance.attach(tokenProxyAddress) - return { tokenProxyAddress, token } -} - -function getMetadataProof(nearTokenId) { - const { proof, proofBlockHeight } = getProofTemplate(); - const metadata = createDefaultERC20Metadata(nearTokenId, proofBlockHeight); - proof.outcome_proof.outcome.receipt_ids[0] = generateRandomBase58(64); - proof.outcome_proof.outcome.status.SuccessValue = serialize( - SCHEMA, - "SetMetadataResult", - metadata - ).toString("base64"); - - return { metadataProof: proof, proofBlockHeight }; -} - -function getProofTemplate() { - return { - proof: require("./proof_template.json"), - proofBlockHeight: 1089, - }; -} - -const createDefaultERC20Metadata = (nearTokenId, blockHeight) => { - return { - prefix: RESULT_PREFIX_METADATA, - token: nearTokenId, - name: 'NEAR ERC20', - symbol: 'NEAR', - decimals: 18, - blockHeight - } -} - -const generateRandomBase58 = (rawSize) => { - var rawInput = "0x"; - var alphabet = "123456789abcdef"; - - for (var i = 0; i < rawSize; i++) { - rawInput += alphabet.charAt(Math.floor(Math.random() * alphabet.length)); - } - - return ethers.encodeBase58(rawInput); -} - -module.exports = { SCHEMA, createEmptyToken, createDefaultERC20Metadata, generateRandomBase58, ADMIN_ROLE, RESULT_PREFIX_LOCK, RESULT_PREFIX_METADATA }; \ No newline at end of file diff --git a/erc20-bridge-token/test/signatures.js b/erc20-bridge-token/test/signatures.js new file mode 100644 index 00000000..fc2d85a9 --- /dev/null +++ b/erc20-bridge-token/test/signatures.js @@ -0,0 +1,79 @@ +function metadataSignature(tokenId) { + const signatures = [ + { + payload: { + token: "wrap.testnet", + name: "Wrapped NEAR fungible token", + symbol: "wNEAR", + decimals: 24 + }, + signature: "0x43D447B8FF105D740FA7B68D506163D33D8AB2831250DB66A074E45FCF218E0C2EC50105AB9AEDD43556D75C22E790CAEF7F6DC486953D5B266859E23D36C3AB1C" + }, + { + payload: { + token: "token-bridge-test.testnet", + name: "Bridge Token", + symbol: "TBT", + decimals: 8 + }, + signature: "0x1D665C94803E5508D7EB34C43F54CB7503B6C14573B8A39F46EFEAF10CF2F68724F18F44F53C8AE3729470B7DDC256D0169F7BBF82CE39F48A95351DAA076C861C" + } + ]; + + const data = signatures.find(s => s.payload.token === tokenId); + if (data === undefined) throw new Error(`Metadata not found for token ${tokenId}`); + + return data; +} + +function depositSignature(tokenId, recipient) { + const signatures = [ + { + payload: { + nonce: 2, + token: "wrap.testnet", + amount: 1, + recipient: "0x3A445243376C32fAba679F63586e236F77EA601e", + relayer: "0x0000000000000000000000000000000000000000", + }, + signature: "0x4B7305FD501E44EEF53E876DE0F8F4F848C00179FD27B0E4942EC2C94816C5CA33C583D7D386B77BF7AD121ED4FE0DB5AF8C730CC7B2D505987616B532F492AF1B" + }, + { + payload: { + nonce: 10, + token: "token-bridge-test.testnet", + amount: 200, + recipient: "0x5a08feed678c056650b3eb4a5cb1b9bb6f0fe265", + relayer: "0x0000000000000000000000000000000000000000", + }, + signature: "0xE5C500D3D21289C620BF7CA0E9049B24B7D4D9864C5E3F09477BCCB7E6524E5810DDCDFE5988F08C36B6AC70D81E443D91CBA0E43935ECEDB8F14BD4222464FA1C" + }, + { + payload: { + nonce: 11, + token: "wrap.testnet", + amount: 25, + recipient: "0x5a08feed678c056650b3eb4a5cb1b9bb6f0fe265", + relayer: "0x0000000000000000000000000000000000000000", + }, + signature: "0x316AFD2FFC056DD266296B023E2509222FC6ED9FAE44583414BE6E478BF62C5238E413341093B0E8C1A6192EFF1C0C4FFFB0D405C48993555E99B11A987891C61C" + }, + { + payload: { + nonce: 12, + token: "token-bridge-test.testnet", + amount: 10, + recipient: "0x3a445243376c32faba679f63586e236f77ea601e", + relayer: "0x0000000000000000000000000000000000000000", + }, + signature: "0x15E146799FF4D5FC190A72633A0FAC14C399D2D9CFCEA1DAC8C1D0913C6698832C2AEF2C5CA6AA731089EA31559A57AE2586744DDDE7A196A3BDA26A38B8387A1C" + } + ]; + + const data = signatures.find(s => s.payload.token === tokenId && s.payload.recipient.toLowerCase() === recipient.toLowerCase()); + if (data === undefined) throw new Error(`Deposit not found for token ${tokenId} and recipient ${recipient}`); + + return data; +} + +module.exports = { metadataSignature, depositSignature }; \ No newline at end of file From ff7fdbeb3b87dce0529bcddd3472d5dc9ad18750 Mon Sep 17 00:00:00 2001 From: kiseln <3428059+kiseln@users.noreply.github.com> Date: Wed, 4 Sep 2024 20:55:40 +0400 Subject: [PATCH 3/3] Add nonce verification for deposits --- .../contracts/BridgeTokenFactory.sol | 13 ++++ erc20-bridge-token/test/BridgeToken.js | 72 +++++++++++++++++++ 2 files changed, 85 insertions(+) diff --git a/erc20-bridge-token/contracts/BridgeTokenFactory.sol b/erc20-bridge-token/contracts/BridgeTokenFactory.sol index 3a9fd7c7..94bc1fac 100644 --- a/erc20-bridge-token/contracts/BridgeTokenFactory.sol +++ b/erc20-bridge-token/contracts/BridgeTokenFactory.sol @@ -22,6 +22,10 @@ contract BridgeTokenFactory is CheckAccountAndToken } + // We removed ProofConsumer from the list of parent contracts and added this gap + // to preserve storage layout when upgrading to the new contract version. + uint256[54] private __gap; + mapping(address => string) private _ethToNearToken; mapping(string => address) private _nearToEthToken; mapping(address => bool) private _isBridgeToken; @@ -33,6 +37,8 @@ contract BridgeTokenFactory is address public tokenImplementationAddress; address public nearBridgeDerivedAddress; + mapping(uint128 => bool) private _completedTransfers; + bytes32 public constant PAUSABLE_ADMIN_ROLE = keccak256("PAUSABLE_ADMIN_ROLE"); uint constant UNPAUSED_ALL = 0; uint constant PAUSED_WITHDRAW = 1 << 0; @@ -73,6 +79,7 @@ contract BridgeTokenFactory is ); error InvalidSignature(); + error NonceAlreadyUsed(); // BridgeTokenFactory is linked to the bridge token factory on NEAR side. // It also links to the prover that it uses to unlock the tokens. @@ -167,6 +174,10 @@ contract BridgeTokenFactory is } function deposit(bytes calldata signatureData, BridgeDeposit calldata bridgeDeposit) external whenNotPaused(PAUSED_DEPOSIT) { + if (_completedTransfers[bridgeDeposit.nonce]) { + revert NonceAlreadyUsed(); + } + bytes memory borshEncoded = bytes.concat( Borsh.encodeUint128(bridgeDeposit.nonce), Borsh.encodeString(bridgeDeposit.token), @@ -186,6 +197,8 @@ contract BridgeTokenFactory is require(_isBridgeToken[_nearToEthToken[bridgeDeposit.token]], "ERR_NOT_BRIDGE_TOKEN"); BridgeToken(_nearToEthToken[bridgeDeposit.token]).mint(bridgeDeposit.recipient, bridgeDeposit.amount); + _completedTransfers[bridgeDeposit.nonce] = true; + emit Deposit(bridgeDeposit.token, bridgeDeposit.amount, bridgeDeposit.recipient); } diff --git a/erc20-bridge-token/test/BridgeToken.js b/erc20-bridge-token/test/BridgeToken.js index d2a13818..ac46a0d9 100644 --- a/erc20-bridge-token/test/BridgeToken.js +++ b/erc20-bridge-token/test/BridgeToken.js @@ -143,6 +143,78 @@ describe('BridgeToken', () => { .revertedWith('Pausable: paused'); }) + it('can\'t deposit twice with the same signature', async function () { + await createToken(wrappedNearId); + + const { signature, payload } = depositSignature(wrappedNearId, user1.address); + await BridgeTokenFactory.deposit(signature, payload); + + await expect( + BridgeTokenFactory.deposit(signature, payload) + ) + .to.be.revertedWithCustomError(BridgeTokenFactory, 'NonceAlreadyUsed'); + }) + + it('can\'t deposit with invalid amount', async function () { + await createToken(wrappedNearId); + + const { signature, payload } = depositSignature(wrappedNearId, user1.address); + payload.amount = 100000; + + await expect( + BridgeTokenFactory.deposit(signature, payload) + ) + .to.be.revertedWithCustomError(BridgeTokenFactory, 'InvalidSignature'); + }) + + it('can\'t deposit with invalid nonce', async function () { + await createToken(wrappedNearId); + + const { signature, payload } = depositSignature(wrappedNearId, user1.address); + payload.nonce = 99; + + await expect( + BridgeTokenFactory.deposit(signature, payload) + ) + .to.be.revertedWithCustomError(BridgeTokenFactory, 'InvalidSignature'); + }) + + it('can\'t deposit with invalid token', async function () { + await createToken(wrappedNearId); + + const { signature, payload } = depositSignature(wrappedNearId, user1.address); + payload.token = 'test-token.testnet'; + + await expect( + BridgeTokenFactory.deposit(signature, payload) + ) + .to.be.revertedWithCustomError(BridgeTokenFactory, 'InvalidSignature'); + }) + + it('can\'t deposit with invalid recipient', async function () { + await createToken(wrappedNearId); + + const { signature, payload } = depositSignature(wrappedNearId, user1.address); + payload.recipient = user2.address; + + await expect( + BridgeTokenFactory.deposit(signature, payload) + ) + .to.be.revertedWithCustomError(BridgeTokenFactory, 'InvalidSignature'); + }) + + it('can\'t deposit with invalid relayer', async function () { + await createToken(wrappedNearId); + + const { signature, payload } = depositSignature(wrappedNearId, user1.address); + payload.relayer = user2.address; + + await expect( + BridgeTokenFactory.deposit(signature, payload) + ) + .to.be.revertedWithCustomError(BridgeTokenFactory, 'InvalidSignature'); + }) + it('withdraw token', async function () { const { token } = await createToken(wrappedNearId);