diff --git a/contracts/interfaces/IToposCore.sol b/contracts/interfaces/IToposCore.sol index 90b2b5f..0114d5d 100644 --- a/contracts/interfaces/IToposCore.sol +++ b/contracts/interfaces/IToposCore.sol @@ -25,7 +25,7 @@ interface IToposCore { event CertStored(CertificateId certId, bytes32 receiptRoot); - event CrossSubnetMessageSent(SubnetId indexed targetSubnetId); + event CrossSubnetMessageSent(SubnetId indexed targetSubnetId, SubnetId sourceSubnetId, uint256 nonce); event Upgraded(address indexed implementation); diff --git a/contracts/interfaces/IToposMessaging.sol b/contracts/interfaces/IToposMessaging.sol index f70e408..63d3f3a 100644 --- a/contracts/interfaces/IToposMessaging.sol +++ b/contracts/interfaces/IToposMessaging.sol @@ -27,7 +27,7 @@ interface IToposMessaging { function validateMerkleProof( bytes memory proofBlob, bytes32 receiptRoot - ) external returns (bytes memory receiptRaw); + ) external returns (bytes memory receiptTrieNodeRaw); function toposCore() external view returns (address); } diff --git a/contracts/topos-core/CodeHash.sol b/contracts/topos-core/CodeHash.sol index da20dd9..0c02da2 100644 --- a/contracts/topos-core/CodeHash.sol +++ b/contracts/topos-core/CodeHash.sol @@ -4,10 +4,10 @@ pragma solidity ^0.8.9; contract CodeHash { /// @notice gets the codehash of a contract address /// @param contractAddr a contract address - function getCodeHash(address contractAddr) public view returns (bytes32) { + function getCodeHash(address contractAddr) public view returns (bytes32 codeHash) { // does not fail with wallet addresses if (contractAddr.codehash.length != 0) { - return contractAddr.codehash; + codeHash = contractAddr.codehash; } } } diff --git a/contracts/topos-core/ToposCore.sol b/contracts/topos-core/ToposCore.sol index 65acb8a..6e6798b 100644 --- a/contracts/topos-core/ToposCore.sol +++ b/contracts/topos-core/ToposCore.sol @@ -18,6 +18,10 @@ contract ToposCore is IToposCore, AdminMultisigBase, Initializable { /// @notice The subnet ID of the subnet this contract is to be deployed on SubnetId public networkSubnetId; + /// @notice Nonce for cross subnet message, meant to be used in combination with `sourceSubnetId` + /// so that the message can be uniquely identified per subnet + uint256 private nonce; + /// @notice Set of certificate IDs Bytes32SetsLib.Set certificateSet; @@ -113,7 +117,8 @@ contract ToposCore is IToposCore, AdminMultisigBase, Initializable { /// @notice Emits an event to signal a cross subnet message has been sent /// @param targetSubnetId The subnet ID of the target subnet function emitCrossSubnetMessage(SubnetId targetSubnetId) external { - emit CrossSubnetMessageSent(targetSubnetId); + nonce += 1; + emit CrossSubnetMessageSent(targetSubnetId, networkSubnetId, nonce); } /// @notice Returns the admin epoch diff --git a/contracts/topos-core/ToposMessaging.sol b/contracts/topos-core/ToposMessaging.sol index 8bf75c7..b77c33d 100644 --- a/contracts/topos-core/ToposMessaging.sol +++ b/contracts/topos-core/ToposMessaging.sol @@ -37,11 +37,11 @@ contract ToposMessaging is IToposMessaging, EternalStorage { if (!IToposCore(_toposCoreAddr).certificateExists(certId)) revert CertNotPresent(); // the raw receipt bytes are taken out of the proof - bytes memory receiptRaw = validateMerkleProof(proofBlob, receiptRoot); - if (receiptRaw.length == uint256(0)) revert InvalidMerkleProof(); + bytes memory receiptTrieNodeRaw = validateMerkleProof(proofBlob, receiptRoot); + if (receiptTrieNodeRaw.length == uint256(0)) revert InvalidMerkleProof(); - bytes32 receiptHash = keccak256(abi.encodePacked(receiptRaw)); - if (_isTxExecuted(receiptHash, receiptRoot)) revert TransactionAlreadyExecuted(); + bytes32 receiptTrieNodeHash = keccak256(abi.encodePacked(receiptTrieNodeRaw)); + if (_isTxExecuted(receiptTrieNodeHash, receiptRoot)) revert TransactionAlreadyExecuted(); ( uint256 status, // uint256 cumulativeGasUsed // bytes memory logsBloom @@ -50,7 +50,7 @@ contract ToposMessaging is IToposMessaging, EternalStorage { address[] memory logsAddress, bytes32[][] memory logsTopics, bytes[] memory logsData - ) = _decodeReceipt(receiptRaw); + ) = _decodeReceipt(receiptTrieNodeRaw); if (status != 1) revert InvalidTransactionStatus(); // verify that provided indexes are within the range of the number of event logs @@ -61,7 +61,7 @@ contract ToposMessaging is IToposMessaging, EternalStorage { SubnetId networkSubnetId = IToposCore(_toposCoreAddr).networkSubnetId(); // prevent re-entrancy - _setTxExecuted(receiptHash, receiptRoot); + _setTxExecuted(receiptTrieNodeHash, receiptRoot); _execute(logIndexes, logsAddress, logsData, logsTopics, networkSubnetId); } @@ -76,10 +76,10 @@ contract ToposMessaging is IToposMessaging, EternalStorage { function validateMerkleProof( bytes memory proofBlob, bytes32 receiptRoot - ) public pure override returns (bytes memory receiptRaw) { + ) public pure override returns (bytes memory receiptTrieNodeRaw) { Proof memory proof = _decodeProofBlob(proofBlob); if (proof.kind != 1) revert UnsupportedProofKind(); - receiptRaw = MerklePatriciaProofVerifier.extractProofValue(receiptRoot, proof.mptKey, proof.stack); + receiptTrieNodeRaw = MerklePatriciaProofVerifier.extractProofValue(receiptRoot, proof.mptKey, proof.stack); } /// @notice Execute the message on a target subnet @@ -103,18 +103,18 @@ contract ToposMessaging is IToposMessaging, EternalStorage { } /// @notice Set a flag to indicate that the asset transfer transaction has been executed - /// @param receiptHash receipt hash + /// @param receiptTrieNodeHash receipt hash /// @param receiptRoot receipt root - function _setTxExecuted(bytes32 receiptHash, bytes32 receiptRoot) internal { - bytes32 suffix = keccak256(abi.encodePacked(receiptHash, receiptRoot)); + function _setTxExecuted(bytes32 receiptTrieNodeHash, bytes32 receiptRoot) internal { + bytes32 suffix = keccak256(abi.encodePacked(receiptTrieNodeHash, receiptRoot)); _setBool(_getTxExecutedKey(suffix), true); } /// @notice Get the flag to indicate that the transaction has been executed - /// @param receiptHash receipt hash + /// @param receiptTrieNodeHash receipt hash /// @param receiptRoot receipt root - function _isTxExecuted(bytes32 receiptHash, bytes32 receiptRoot) internal view returns (bool) { - bytes32 suffix = keccak256(abi.encodePacked(receiptHash, receiptRoot)); + function _isTxExecuted(bytes32 receiptTrieNodeHash, bytes32 receiptRoot) internal view returns (bool) { + bytes32 suffix = keccak256(abi.encodePacked(receiptTrieNodeHash, receiptRoot)); return getBool(_getTxExecutedKey(suffix)); } @@ -171,9 +171,9 @@ contract ToposMessaging is IToposMessaging, EternalStorage { } /// @notice Decode the receipt into its components - /// @param receiptRaw RLP encoded receipt + /// @param receiptTrieNodeRaw RLP encoded receipt function _decodeReceipt( - bytes memory receiptRaw + bytes memory receiptTrieNodeRaw ) internal pure @@ -186,7 +186,7 @@ contract ToposMessaging is IToposMessaging, EternalStorage { bytes[] memory logsData ) { - RLPReader.RLPItem[] memory receipt = receiptRaw.toRlpItem().toList(); + RLPReader.RLPItem[] memory receipt = receiptTrieNodeRaw.toRlpItem().toList(); status = receipt[0].toUint(); cumulativeGasUsed = receipt[1].toUint(); diff --git a/test/topos-core/ToposMessaging.test.ts b/test/topos-core/ToposMessaging.test.ts index 323292d..a12442c 100644 --- a/test/topos-core/ToposMessaging.test.ts +++ b/test/topos-core/ToposMessaging.test.ts @@ -82,10 +82,7 @@ describe('ToposMessaging', () => { await toposCoreProxy.waitForDeployment() const toposCoreProxyAddress = await toposCoreProxy.getAddress() - const toposCore = await ToposCore__factory.connect( - toposCoreProxyAddress, - admin - ) + const toposCore = ToposCore__factory.connect(toposCoreProxyAddress, admin) await toposCore.initialize(adminAddresses, adminThreshold) const erc20Messaging = await new ERC20Messaging__factory(admin).deploy( @@ -214,7 +211,7 @@ describe('ToposMessaging', () => { (l) => (l as EventLog).eventName === 'TokenDeployed' ) as EventLog const tokenAddress = log.args.tokenAddress - const erc20 = await ERC20__factory.connect(tokenAddress, admin) + const erc20 = ERC20__factory.connect(tokenAddress, admin) await erc20.approve(erc20MessagingAddress, tc.SEND_AMOUNT_50) const sendToken = await sendTokenTx( @@ -223,7 +220,8 @@ describe('ToposMessaging', () => { receiver.address, admin, cc.SOURCE_SUBNET_ID_2, - tc.TOKEN_SYMBOL_X + tc.TOKEN_SYMBOL_X, + tc.SEND_AMOUNT_50 ) const { proofBlob, receiptsRoot } = await getReceiptMptProof( @@ -498,7 +496,7 @@ describe('ToposMessaging', () => { const deployedToken = await erc20Messaging.getTokenBySymbol( tc.TOKEN_SYMBOL_X ) - const erc20 = await ERC20__factory.connect(deployedToken.addr, admin) + const erc20 = ERC20__factory.connect(deployedToken.addr, admin) await erc20.approve(erc20MessagingAddress, tc.SEND_AMOUNT_50) const sendToken = await sendTokenTx( @@ -507,7 +505,8 @@ describe('ToposMessaging', () => { receiver.address, admin, cc.SOURCE_SUBNET_ID_2, - tc.TOKEN_SYMBOL_X + tc.TOKEN_SYMBOL_X, + tc.SEND_AMOUNT_50 ) const { proofBlob, receiptsRoot } = await getReceiptMptProof( @@ -547,7 +546,7 @@ describe('ToposMessaging', () => { const deployedToken = await erc20Messaging.getTokenBySymbol( tc.TOKEN_SYMBOL_X ) - const erc20 = await ERC20__factory.connect(deployedToken.addr, admin) + const erc20 = ERC20__factory.connect(deployedToken.addr, admin) await erc20.approve(erc20MessagingAddress, tc.SEND_AMOUNT_50) const sendToken = await sendTokenTx( @@ -556,7 +555,8 @@ describe('ToposMessaging', () => { ethers.ZeroAddress, // sending to a zero address admin, cc.SOURCE_SUBNET_ID_2, - tc.TOKEN_SYMBOL_X + tc.TOKEN_SYMBOL_X, + tc.SEND_AMOUNT_50 ) const { proofBlob, receiptsRoot } = await getReceiptMptProof( @@ -582,6 +582,80 @@ describe('ToposMessaging', () => { ).to.be.revertedWith('ERC20: mint to the zero address') }) + it('can execute a transaction with same inputs twice', async () => { + const { admin, receiver, defaultToken, toposCore, erc20Messaging } = + await loadFixture(deployERC20MessagingFixture) + await toposCore.setNetworkSubnetId(cc.SOURCE_SUBNET_ID_2) + // first transaction + const { erc20, proofBlob, receiptsRoot } = await deployDefaultToken( + admin, + receiver, + defaultToken, + erc20Messaging + ) + const certificate = testUtils.encodeCertParam( + cc.PREV_CERT_ID_0, + cc.SOURCE_SUBNET_ID_1, + cc.STATE_ROOT_MAX, + cc.TX_ROOT_MAX, + receiptsRoot, + [cc.SOURCE_SUBNET_ID_2], + cc.VERIFIER, + cc.CERT_ID_1, + cc.DUMMY_STARK_PROOF, + cc.DUMMY_SIGNATURE + ) + await toposCore.pushCertificate(certificate, cc.CERT_POS_1) + await expect( + erc20Messaging.execute([tc.TOKEN_SENT_INDEX_2], proofBlob, receiptsRoot) + ) + .to.emit(erc20, 'Transfer') + .withArgs(ethers.ZeroAddress, receiver.address, tc.SEND_AMOUNT_50) + await expect(erc20.balanceOf(receiver.address)).to.eventually.equal( + tc.SEND_AMOUNT_50 + ) + + // second transaction + await erc20.approve(await erc20Messaging.getAddress(), tc.SEND_AMOUNT_50) + const sendToken = await sendTokenTx( + erc20Messaging, + ethers.provider, + await receiver.getAddress(), + admin, + cc.SOURCE_SUBNET_ID_2, + await erc20.symbol(), + tc.SEND_AMOUNT_50 + ) + + const { proofBlob: proofBlob2, receiptsRoot: receiptsRoot2 } = + await getReceiptMptProof(sendToken, ethers.provider) + const certificate2 = testUtils.encodeCertParam( + cc.CERT_ID_1, + cc.SOURCE_SUBNET_ID_1, + cc.STATE_ROOT_MAX, + cc.TX_ROOT_MAX, + receiptsRoot2, + [cc.SOURCE_SUBNET_ID_2], + cc.VERIFIER, + cc.CERT_ID_2, + cc.DUMMY_STARK_PROOF, + cc.DUMMY_SIGNATURE + ) + await toposCore.pushCertificate(certificate2, cc.CERT_POS_2) + await expect( + erc20Messaging.execute( + [tc.TOKEN_SENT_INDEX_2], + proofBlob2, + receiptsRoot2 + ) + ) + .to.emit(erc20, 'Transfer') + .withArgs(ethers.ZeroAddress, receiver.address, tc.SEND_AMOUNT_50) + await expect(erc20.balanceOf(receiver.address)).to.eventually.equal( + tc.SEND_AMOUNT_50 * 2 + ) + }) + it('emits the Transfer success event', async () => { const { admin, receiver, defaultToken, toposCore, erc20Messaging } = await loadFixture(deployERC20MessagingFixture) @@ -680,7 +754,7 @@ describe('ToposMessaging', () => { (l) => (l as EventLog).eventName === 'TokenDeployed' ) as EventLog const tokenAddress = log.args.tokenAddress - const erc20 = await ERC20__factory.connect(tokenAddress, admin) + const erc20 = ERC20__factory.connect(tokenAddress, admin) await erc20.approve(erc20MessagingAddress, tc.SEND_AMOUNT_50) await expect( @@ -702,7 +776,7 @@ describe('ToposMessaging', () => { tc.SEND_AMOUNT_50 ) .to.emit(toposCore, 'CrossSubnetMessageSent') - .withArgs(cc.TARGET_SUBNET_ID_4) + .withArgs(cc.TARGET_SUBNET_ID_4, cc.SOURCE_SUBNET_ID_2, 1) }) }) @@ -719,7 +793,7 @@ describe('ToposMessaging', () => { (l) => (l as EventLog).eventName === 'TokenDeployed' ) as EventLog const tokenAddress = log.args.tokenAddress - const erc20 = await ERC20__factory.connect(tokenAddress, admin) + const erc20 = ERC20__factory.connect(tokenAddress, admin) await erc20.approve(erc20MessagingAddress, tc.SEND_AMOUNT_50) const receiverAddress = await receiver.getAddress() @@ -730,7 +804,8 @@ describe('ToposMessaging', () => { receiverAddress, admin, cc.SOURCE_SUBNET_ID_2, - await erc20.symbol() + await erc20.symbol(), + tc.SEND_AMOUNT_50 ) const { proofBlob, receiptsRoot } = await getReceiptMptProof( @@ -746,20 +821,21 @@ describe('ToposMessaging', () => { receiver: string, signer: Signer, targetSubnetId: string, - symbol: string + symbol: string, + amount: number ) { const estimatedGasLimit = await erc20Messaging.sendToken.estimateGas( targetSubnetId, symbol, receiver, - tc.SEND_AMOUNT_50, + amount, { gasLimit: 4_000_000 } ) const TxUnsigned = await erc20Messaging.sendToken.populateTransaction( targetSubnetId, symbol, receiver, - tc.SEND_AMOUNT_50, + amount, { gasLimit: 4_000_000 } ) TxUnsigned.chainId = BigInt(31337) // Hardcoded chainId for Hardhat local testing