diff --git a/eth-custodian/contracts/EthCustodianProxy.sol b/eth-custodian/contracts/EthCustodianProxy.sol index 9d0912a..ae8d922 100644 --- a/eth-custodian/contracts/EthCustodianProxy.sol +++ b/eth-custodian/contracts/EthCustodianProxy.sol @@ -34,6 +34,8 @@ contract EthCustodianProxy is event Withdrawn(address indexed recipient, uint128 amount); error AlreadyMigrated(); + error ProducerAccountIdTooLong(bytes newProducerAccount); + error ProofFromPostMergeBlock(); struct BurnResult { uint128 amount; @@ -87,15 +89,14 @@ contract EthCustodianProxy is bytes calldata proofData, uint64 proofBlockHeight ) external whenNotPaused(PAUSED_WITHDRAW_PRE_MIGRATION) { - require( - proofBlockHeight < migrationBlockHeight, - 'Proof is from a post merge block' - ); + if (proofBlockHeight >= migrationBlockHeight) { + revert ProofFromPostMergeBlock(); + } bytes memory postMigrationProducer = ethCustodianImpl.nearProofProducerAccount_(); - ethCustodianImpl.adminSstore(1, uint(bytes32(preMigrationProducerAccount))); + writeProofProducerSlot(preMigrationProducerAccount); ethCustodianImpl.withdraw(proofData, proofBlockHeight); - ethCustodianImpl.adminSstore(1, uint(bytes32(postMigrationProducer))); + writeProofProducerSlot(postMigrationProducer); } function migrateToNewProofProducer( @@ -106,9 +107,14 @@ contract EthCustodianProxy is revert AlreadyMigrated(); } + // Needs to fit in one slot + if (newProducerAccount.length > 31) { + revert ProducerAccountIdTooLong(newProducerAccount); + } + migrationBlockHeight = migrationBlockNumber; preMigrationProducerAccount = ethCustodianImpl.nearProofProducerAccount_(); - ethCustodianImpl.adminSstore(1, uint(bytes32(newProducerAccount))); + writeProofProducerSlot(newProducerAccount); } function pauseAll() external onlyRole(PAUSABLE_ADMIN_ROLE) { @@ -139,4 +145,9 @@ contract EthCustodianProxy is function _authorizeUpgrade( address newImplementation ) internal override onlyRole(DEFAULT_ADMIN_ROLE) {} + + function writeProofProducerSlot(bytes memory proofProducer) private { + uint dataLength = proofProducer.length * 2; + ethCustodianImpl.adminSstore(1, uint(bytes32(proofProducer)) + dataLength); + } } diff --git a/eth-custodian/test/EthCustodianProxy.js b/eth-custodian/test/EthCustodianProxy.js new file mode 100644 index 0000000..16f8d55 --- /dev/null +++ b/eth-custodian/test/EthCustodianProxy.js @@ -0,0 +1,272 @@ +const { ethers } = require('hardhat'); +const { expect } = require('chai'); +const { serialize } = require('rainbow-bridge-lib/rainbow/borsh.js'); +const { borshifyOutcomeProof } = require('rainbow-bridge-lib/rainbow/borshify-proof.js'); + +const UNPAUSED_ALL = 0; +const PAUSED_DEPOSIT_TO_EVM = 1 << 0; +const PAUSED_DEPOSIT_TO_NEAR = 1 << 1; +const PAUSED_WITHDRAW = 1 << 2; +const PAUSED_WITHDRAW_PRE_MIGRATION = 1 << 3; +const PAUSED_ALL = PAUSED_DEPOSIT_TO_EVM | PAUSED_DEPOSIT_TO_NEAR | PAUSED_WITHDRAW | PAUSED_WITHDRAW_PRE_MIGRATION; + +const SCHEMA = { + 'Withdrawn': { + kind: 'struct', fields: [ + ['amount', 'u128'], + ['recipient', [20]], + ['ethCustodian', [20]], + ] + } +}; + +describe('EthCustodianProxy contract', () => { + let ethCustodian; + let ethCustodianProxy; + let nearProver; + + let adminAccount; + let ethRecipientOnNear; + let user1; + let user2; + + const nearEvmAccount = Buffer.from('v1.eth-connector.testnet'); + const newProofProducerData = Buffer.from('new-producer.testnet'); + const migrationBlock = 19672697; + + beforeEach(async () => { + [adminAccount, ethRecipientOnNear, user1, user2] = await ethers.getSigners(); + + const nearProverMockContractFactory = await ethers.getContractFactory('NearProverMock') + nearProver = await nearProverMockContractFactory + .connect(adminAccount) + .deploy(); + + const ethCustodianContractFactory = await ethers.getContractFactory('EthCustodian'); + ethCustodian = await ethCustodianContractFactory + .connect(adminAccount) + .deploy( + nearEvmAccount, + nearProver.address, + 0, + adminAccount.address, + UNPAUSED_ALL); + + const ethCustodianProxyContractFactory = await ethers.getContractFactory('EthCustodianProxy'); + ethCustodianProxy = await upgrades.deployProxy( + ethCustodianProxyContractFactory, + [ethCustodian.address] + ); + + const nominateTx = await ethCustodian.nominateAdmin(ethCustodianProxy.address); + await nominateTx.wait(); + + const acceptTx = await ethCustodian.acceptAdmin(ethCustodianProxy.address); + await acceptTx.wait(); + + const pauseTx = await ethCustodianProxy.pauseImpl(PAUSED_ALL); + await pauseTx.wait(); + }); + + describe('EthCustodian', () => { + it('Should be paused', async () => { + const paused = await ethCustodian.paused(); + + expect(paused & PAUSED_DEPOSIT_TO_EVM).to.not.equal(0); + expect(paused & PAUSED_DEPOSIT_TO_NEAR).to.not.equal(0); + expect(paused & PAUSED_WITHDRAW).to.not.equal(0); + }); + }); + + describe('Deposit', () => { + it('to EVM Should change the balance and emit the event', async () => { + const amountToTransfer = 50000; + const balanceBefore = ethers.BigNumber.from( + await ethers.provider.getBalance(ethCustodian.address)); + + const protocolMessage = nearEvmAccount + ':' + String(ethRecipientOnNear.address); + const options = { value: amountToTransfer }; + await expect( + ethCustodianProxy + .connect(user1) + .depositToEVM(ethRecipientOnNear.address, options) + ) + .to.emit(ethCustodian, 'Deposited') + .withArgs(ethCustodianProxy.address, protocolMessage, amountToTransfer, 0); + + const balanceAfter = ethers.BigNumber.from( + await ethers.provider.getBalance(ethCustodian.address)); + const balanceDiff = balanceAfter.sub(balanceBefore); + expect(balanceDiff).to.equal(amountToTransfer); + }); + + it('to Near Should change the balance and emit the event', async () => { + const nearRecipientAccountId = 'recipient.near'; + + const amountToTransfer = 50000; + const balanceBefore = ethers.BigNumber.from( + await ethers.provider.getBalance(ethCustodian.address)); + + const options = { value: amountToTransfer }; + await expect( + ethCustodianProxy + .connect(user1) + .depositToNear(nearRecipientAccountId, options) + ) + .to.emit(ethCustodian, 'Deposited') + .withArgs(ethCustodianProxy.address, nearRecipientAccountId, amountToTransfer, 0); + + const balanceAfter = ethers.BigNumber.from( + await ethers.provider.getBalance(ethCustodian.address)); + const balanceDiff = balanceAfter.sub(balanceBefore); + expect(balanceDiff).to.equal(amountToTransfer); + }); + }); + + describe('Pause', () => { + it('Should pause deposit to NEAR', async () => { + await ethCustodianProxy.pauseProxy(PAUSED_DEPOSIT_TO_NEAR); + + await expect(ethCustodianProxy.depositToNear('recipient.near', { value: 50000 })) + .to.be.revertedWith('Pausable: paused'); + }); + + it('Should pause deposit to EVM', async () => { + await ethCustodianProxy.pauseProxy(PAUSED_DEPOSIT_TO_EVM); + + await expect(ethCustodianProxy.depositToEVM(ethRecipientOnNear.address, { value: 50000 })) + .to.be.revertedWith('Pausable: paused'); + }); + + it('Should pause withdraw', async () => { + await ethCustodianProxy.pauseProxy(PAUSED_WITHDRAW); + const proof = require('./proof_template_from_testnet.json'); + + await expect(ethCustodianProxy.withdraw(borshifyOutcomeProof(proof), 1099)) + .to.be.revertedWith('Pausable: paused'); + }); + + it('Should pause withdraw pre-migration', async () => { + await ethCustodianProxy.pauseProxy(PAUSED_WITHDRAW_PRE_MIGRATION); + const proof = require('./proof_template_from_testnet.json'); + + await expect(ethCustodianProxy.withdrawPreMigration(borshifyOutcomeProof(proof), 1099)) + .to.be.revertedWith('Pausable: paused'); + }); + + it('Should pause all', async () => { + await ethCustodianProxy.pauseAll(); + + await expect(ethCustodianProxy.depositToNear('recipient.near', { value: 50000 })) + .to.be.revertedWith('Pausable: paused'); + + await expect(ethCustodianProxy.depositToEVM(ethRecipientOnNear.address, { value: 50000 })) + .to.be.revertedWith('Pausable: paused'); + + const proof = require('./proof_template_from_testnet.json'); + + await expect(ethCustodianProxy.withdraw(borshifyOutcomeProof(proof), 1099)) + .to.be.revertedWith('Pausable: paused'); + + await expect(ethCustodianProxy.withdrawPreMigration(borshifyOutcomeProof(proof), 1099)) + .to.be.revertedWith('Pausable: paused'); + }); + }); + + describe('Migrate', () => { + it('Should change proof producer for EthCustodian', async () => { + const oldProofProducer = await ethCustodian.nearProofProducerAccount_(); + + await ethCustodianProxy.migrateToNewProofProducer(newProofProducerData, migrationBlock); + + expect(await ethCustodianProxy.migrationBlockHeight()) + .to.equal(migrationBlock); + + expect(await ethCustodianProxy.preMigrationProducerAccount()) + .to.equal(oldProofProducer); + + expect(await ethCustodian.nearProofProducerAccount_()) + .to.equal('0x' + newProofProducerData.toString('hex')); + }); + + it('Should fail when invoked for the second time', async () => { + await ethCustodianProxy.migrateToNewProofProducer(newProofProducerData, migrationBlock); + + await expect(ethCustodianProxy.migrateToNewProofProducer(newProofProducerData, migrationBlock)) + .to.be.revertedWith('AlreadyMigrated'); + }); + + it('Should fail when block producer id is too long', async () => { + await expect( + ethCustodianProxy.migrateToNewProofProducer(Buffer.from('new-loooooooong-producer.testnet'), 19672697) + ) + .to.be.revertedWith('ProducerAccountIdTooLong'); + }); + }); + + describe('Withdraw', () => { + const amount = 5000; + const proof = require('./proof_template_from_testnet.json'); + + beforeEach(async () => { + await ethCustodianProxy + .connect(user1) + .depositToEVM(ethRecipientOnNear.address, { value: 200000 }); + + await ethCustodianProxy.migrateToNewProofProducer(newProofProducerData, migrationBlock); + + proof.outcome_proof.outcome.status.SuccessValue = serialize(SCHEMA, 'Withdrawn', { + amount: amount, + recipient: ethers.utils.arrayify(user2.address), + ethCustodian: ethers.utils.arrayify(ethCustodian.address), + }).toString('base64'); + }); + + it('Should successfully withdraw and emit the event', async () => { + const postMigrationProof = structuredClone(proof); + postMigrationProof.outcome_proof.outcome.executor_id = 'new-producer.testnet'; + + const proofProducerBefore = await ethCustodian.nearProofProducerAccount_(); + const balanceBefore = ethers.BigNumber.from( + await ethers.provider.getBalance(user2.address)); + + await expect( + ethCustodianProxy.withdraw(borshifyOutcomeProof(postMigrationProof), 1099) + ) + .to.emit(ethCustodian, 'Withdrawn') + .withArgs(user2.address, amount); + + const proofProducerAfter = await ethCustodian.nearProofProducerAccount_(); + const balanceAfter = ethers.BigNumber.from( + await ethers.provider.getBalance(user2.address)); + const balanceDiff = balanceAfter.sub(balanceBefore); + + expect(proofProducerBefore).to.equal(proofProducerAfter); + expect(balanceDiff).to.equal(amount) + }); + + describe('Pre-migration', () => { + it('Should successfully withdraw and emit the event', async () => { + const balanceBefore = ethers.BigNumber.from(await ethers.provider.getBalance(user2.address)); + + await expect( + ethCustodianProxy.withdrawPreMigration(borshifyOutcomeProof(proof), 1099) + ) + .to.emit(ethCustodian, 'Withdrawn') + .withArgs(user2.address, amount); + + const balanceAfter = ethers.BigNumber.from(await ethers.provider.getBalance(user2.address)); + const balanceDiff = balanceAfter.sub(balanceBefore); + + expect(balanceDiff).to.equal(amount) + }); + + it('Should fail when block is older than migration block', async () => { + await expect( + ethCustodianProxy.withdrawPreMigration(borshifyOutcomeProof(proof), migrationBlock) + ) + .to.be.revertedWith('ProofFromPostMergeBlock'); + }); + }) + }); +}); \ No newline at end of file