diff --git a/adr/adr2001 - trb bridge structure.md b/adr/adr2001 - trb bridge structure.md index beac06373..279fcee88 100644 --- a/adr/adr2001 - trb bridge structure.md +++ b/adr/adr2001 - trb bridge structure.md @@ -13,7 +13,7 @@ Tellor Tributes (TRB) is the tellor token. It exists on Ethereum and cannot be changed. It mints ~4k to the team each month and ~4k to the oracle contract for time based rewards. When starting Layer we will launch a bridging contract where parties can deposit TRB to Layer. Layer will utilize reporters then to report deposit events to itself. When the deposit is made it will be assigned a deposit ID and an event will be kicked off. All reporters will report for that event for a 1 hour window (this is allowed so that reporters are able to wait a certain amount of blocks before reporting so that the state of Ethereum has reached a high level of finality) and then we will optimistically use the report in our system, ensuring that the report is at least 12 hours old before the tokens are minted on Layer. Once the value is 12 hours old anyone can mint the tokens on Layer for the specified deposit ID. -As an additional security measure, the bridge contract will not allow more than 20% of the total supply on Layer to be bridged within a 12 hour period (the function will be locked). This will be to ensure that someone does not bridge over a very large amount to stake/grief the network, manipulate votes, or grief the system via disputes without proper time to analyze the situation. For the reverse direction, parties will burn TRB on Layer, the reporters will then report that it happened and then the bridge contract on Ethereum can use the tellor data as any other user, but this time reading burn events. There will be similar limits in this direction and the bridge contract will also use the data optimistically (12 hours old) to further reduce attack vectors. +As an additional security measure, the bridge contract will not allow more than 20% of the total supply on Layer to be bridged within a 12 hour period (the function will be locked). This will be to ensure that someone does not bridge over a very large amount to stake/grief the network, manipulate votes, or grief the system via disputes without proper time to analyze the situation. For the reverse direction, parties will burn TRB on Layer, the reporters will then report that it happened and then the bridge contract on Ethereum can use the tellor data as any other user, but this time reading burn events. A 20% withdraw limit is also used in this direction and the bridge contract will also use the data optimistically (12 hours old) to further reduce attack vectors. The cycle list helps keep the network alive by providing a list of data requests that reporters can support to receive time-based rewards(inflationary rewards) when there are no tips available to claim. Each data request on the cycle list rotates over time so that each request gets pushed on chain on a regular basis. The bridge deposit data request will not appear in the "next request" of the cycle list, however reporters will be allowed to report for it and claim time based rewards for it. Time based rewards will be split between the data request on the cycle list and the bridged deposit request. Parties can also use the tip functionality to incentivize faster updates for deposits. @@ -38,4 +38,7 @@ It was considered to allow depositors to include a tip to incentivize reporters ## Issues / Notes on Implementation -Note that governance can change the queryId time frame and it should be monitored to make sure that commit times are enough notice on a given tip +Note that governance can change the queryId time frame and it should be monitored to make sure that commit times are enough notice on a given tip. + + +In the case of a broken validator set (a completely compromised layer chain (>2/3 malicious)), a social fork will be necessary to save layer. Users will be aware of this and will be able to handle a failure, however in this case the bridge is the "user" of layer with regards to releasing tokens. We had looked into adding some sort of pause or schelling game to fork the bridge contract, however these solutions seem more like attack vectors than actual options. Ultimately, the TRB token will be used on layer. The only reason it will be on Ethereum is for legacy trading and CEX integrations (once legacy users are moved to layer). This is fine (these users and CEX contracts should never be forced to upgrade to cosmos if possible), however there is no real "break" of the system. Ultimately if you take over our validator set, you will get forked out and lose the tokens. The trading venues and exchanges will need to monitor and freeze the trading should this occur. Although extreme, this is preferable to introducing an attack or censorship vector (e.g. a multisig to freeze the contracts) that would require a fork anyway, but would also not require the attacker to actually buy up tokens to hit 2/3 of the layer validator set. Ultimately, limiting the token withdrawals to a certain percentage per day is the best option to give trading venues time to halt in the case of an attack. diff --git a/evm/contracts/token-bridge/TokenBridge.sol b/evm/contracts/token-bridge/TokenBridge.sol index b1fa9914e..507d26049 100644 --- a/evm/contracts/token-bridge/TokenBridge.sol +++ b/evm/contracts/token-bridge/TokenBridge.sol @@ -21,6 +21,7 @@ contract TokenBridge is LayerTransition{ uint256 public immutable DEPOSIT_LIMIT_DENOMINATOR = 100e18 / 20e18; // 100/depositLimitPercentage mapping(uint256 => bool) public withdrawalClaimed; + mapping(address => uint256) public tokensToClaim; mapping(uint256 => DepositDetails) public deposits; struct DepositDetails { @@ -50,7 +51,7 @@ contract TokenBridge is LayerTransition{ function depositToLayer(uint256 _amount, string memory _layerRecipient) external { require(_amount > 0, "TokenBridge: amount must be greater than 0"); require(token.transferFrom(msg.sender, address(this), _amount), "TokenBridge: transferFrom failed"); - require(_amount <= _refreshDepositLimit(), "TokenBridge: amount exceeds deposit limit"); + require(_amount <= _refreshDepositLimit(), "TokenBridge: amount exceeds deposit limit for time period"); depositId++; depositLimitRecord -= _amount; deposits[depositId] = DepositDetails(msg.sender, _layerRecipient, _amount, block.number); @@ -71,16 +72,41 @@ contract TokenBridge is LayerTransition{ require(_attest.queryId == keccak256(abi.encode("TRBBridge", abi.encode(false, _depositId))), "TokenBridge: invalid queryId"); require(!withdrawalClaimed[_depositId], "TokenBridge: withdrawal already claimed"); require(block.timestamp - _attest.report.timestamp > 12 hours, "TokenBridge: premature attestation"); - //isAnyConsesnusValue here bridge.verifyOracleData(_attest, _valset, _sigs); require(_attest.report.aggregatePower >= bridge.powerThreshold(), "Report aggregate power must be greater than or equal to _minimumPower"); withdrawalClaimed[_depositId] = true; (address _recipient, string memory _layerSender,uint256 _amountLoya) = abi.decode(_attest.report.value, (address, string, uint256)); uint256 _amountConverted = _amountLoya * 1e12; - require(token.transfer(_recipient, _amountConverted), "TokenBridge: transfer failed"); + uint256 _depositLimit = _refreshDepositLimit(); + if(_depositLimit < _amountConverted){ + tokensToClaim[_recipient] = tokensToClaim[_recipient] + (_amountConverted - _depositLimit); + _amountConverted = _depositLimit; + require(token.transfer(_recipient, _amountConverted), "TokenBridge: transfer failed"); + } + else{ + require(token.transfer(_recipient, _amountConverted), "TokenBridge: transfer failed"); + } + depositLimitRecord -= _amountConverted; emit Withdrawal(_depositId, _layerSender, _recipient, _amountConverted); } + function claimExtraWithdraw(address _recipient) external { + uint256 _amountConverted = tokensToClaim[_recipient]; + require(_amountConverted > 0, "amount must be > 0"); + uint256 _depositLimit = _refreshDepositLimit(); + require(_depositLimit > 0, "TokenBridge: depositLimit must be > 0"); + if(_depositLimit < _amountConverted){ + tokensToClaim[_recipient] = tokensToClaim[_recipient] - _depositLimit; + _amountConverted = _depositLimit; + require(token.transfer(_recipient, _amountConverted), "TokenBridge: transfer failed"); + } + else{ + tokensToClaim[_recipient] = 0; + require(token.transfer(_recipient, _amountConverted), "TokenBridge: transfer failed"); + } + depositLimitRecord -= _amountConverted; + } + /// @notice refreshes the deposit limit every 12 hours so no one can spam layer with new tokens function depositLimit() external view returns (uint256) { if (block.timestamp - depositLimitUpdateTime > DEPOSIT_LIMIT_UPDATE_INTERVAL) { @@ -98,9 +124,7 @@ contract TokenBridge is LayerTransition{ uint256 _layerTokenSupply = token.balanceOf(address(this)) + INITIAL_LAYER_TOKEN_SUPPLY; depositLimitRecord = _layerTokenSupply / DEPOSIT_LIMIT_DENOMINATOR; depositLimitUpdateTime = block.timestamp; - } + } return depositLimitRecord; } - - } diff --git a/evm/hardhat.config.js b/evm/hardhat.config.js index dd1a33d80..8ce8fe912 100644 --- a/evm/hardhat.config.js +++ b/evm/hardhat.config.js @@ -1,8 +1,8 @@ -require("@nomicfoundation/hardhat-toolbox"); -require("hardhat-gas-reporter"); +//require("@nomicfoundation/hardhat-toolbox"); +//require("hardhat-gas-reporter"); require("dotenv").config(); require("@nomiclabs/hardhat-ethers"); -require("@nomiclabs/hardhat-web3"); +// require("@nomiclabs/hardhat-web3"); module.exports = { solidity: { diff --git a/evm/package.json b/evm/package.json index c333ea795..54c1bd993 100644 --- a/evm/package.json +++ b/evm/package.json @@ -13,9 +13,8 @@ "author": "", "license": "ISC", "devDependencies": { + "@nomiclabs/hardhat-ethers": "^2.0.4", "@nomicfoundation/hardhat-network-helpers": "^1.0.10", - "@nomicfoundation/hardhat-toolbox": "^5.0.0", - "@nomiclabs/hardhat-ethers": "^2.2.3", "@nomiclabs/hardhat-web3": "^2.0.0", "axios": "^1.6.8", "chai": "^4.3.10", diff --git a/evm/test/TokenBridgeFunctionTestsHH.js b/evm/test/TokenBridgeFunctionTestsHH.js index 4a034c7cf..accde2043 100644 --- a/evm/test/TokenBridgeFunctionTestsHH.js +++ b/evm/test/TokenBridgeFunctionTestsHH.js @@ -146,4 +146,75 @@ describe("TokenBridge - Function Tests", async function () { await tbridge.refreshDepositLimit() assert.equal(BigInt(await tbridge.depositLimitRecord()), expectedDepositLimit2); }) + it("claim extraWithdraw", async function () { + await tbridge.refreshDepositLimit() + expectedDepositLimit = BigInt(100e18) * BigInt(2) / BigInt(10) + depositAmount = expectedDepositLimit + await h.expectThrow(tbridge.depositToLayer(depositAmount, LAYER_RECIPIENT)) // not approved + await token.approve(await tbridge.address, h.toWei("100")) + await tbridge.depositToLayer(depositAmount, LAYER_RECIPIENT) + await h.advanceTime(43200) + await token.approve(await tbridge.address, h.toWei("100")) + await tbridge.depositToLayer(depositAmount, LAYER_RECIPIENT) + let _addy = await accounts[2].address + value = h.getWithdrawValue(_addy,LAYER_RECIPIENT,40000000) + blocky = await h.getBlock() + timestamp = blocky.timestamp - 2 + aggregatePower = 3 + attestTimestamp = timestamp + 1 + previousTimestamp = 0 + nextTimestamp = 0 + newValHash = await h.calculateValHash(initialValAddrs, initialPowers) + valCheckpoint = await h.calculateValCheckpoint(newValHash, threshold, valTimestamp) + dataDigest = await h.getDataDigest( + WITHDRAW1_QUERY_ID, + value, + timestamp, + aggregatePower, + previousTimestamp, + nextTimestamp, + valCheckpoint, + attestTimestamp + ) + currentValSetArray = await h.getValSetStructArray(initialValAddrs, initialPowers) + sig1 = await h.layerSign(dataDigest, val1.privateKey) + sig2 = await h.layerSign(dataDigest, val2.privateKey) + sigStructArray = await h.getSigStructArray([sig1, sig2]) + oracleDataStruct = await h.getOracleDataStruct( + WITHDRAW1_QUERY_ID, + value, + timestamp, + aggregatePower, + previousTimestamp, + nextTimestamp, + attestTimestamp + ) + await h.advanceTime(43200) + await tbridge.refreshDepositLimit() + let _limit = await tbridge.depositLimit.call() + assert(await token.balanceOf(_addy) == 0) + await tbridge.withdrawFromLayer( + oracleDataStruct, + currentValSetArray, + sigStructArray, + 1, + ) + recipientBal = await token.balanceOf(_addy) + assert(recipientBal - _limit == 0, "token balance should be correct") + tokensToClaim = await tbridge.tokensToClaim(accounts[2].address) + assert(tokensToClaim == BigInt(40e18) - BigInt(recipientBal), "tokensToClaim should be correct") + await h.expectThrow(tbridge.claimExtraWithdraw(await accounts[2].address)) + await h.advanceTime(43200) + await tbridge.claimExtraWithdraw(await accounts[2].address); + await h.expectThrow(tbridge.claimExtraWithdraw(await accounts[2].address)) + recipientBal = await token.balanceOf(await accounts[2].address) + assert(recipientBal == BigInt(40e18), "token balance should be correct") + await h.advanceTime(43200) + await tbridge.refreshDepositLimit() + _limit = await tbridge.depositLimit() + assert(BigInt(await tbridge.depositLimitRecord()) - expectedDepositLimit == BigInt(0)); + assert(_limit == expectedDepositLimit, "deposit Limit should be correct") + tokensToClaim = await tbridge.tokensToClaim(accounts[2].address) + assert(tokensToClaim == BigInt(0), "tokensToClaim should be correct") + }) }) diff --git a/evm/test/TokenBridgeTransition-FunctionTests.js b/evm/test/TokenBridgeTransition-FunctionTests.js index 24552bf55..abd93be9d 100644 --- a/evm/test/TokenBridgeTransition-FunctionTests.js +++ b/evm/test/TokenBridgeTransition-FunctionTests.js @@ -110,7 +110,7 @@ describe("Function Tests - NewTransition", function() { await h.expectThrow(tbridge.connect(accounts[0]).addStakingRewards(h.toWei("1"))) // not approved await tellor.connect(accounts[0]).approve(await tbridge.address, h.toWei("1")) await tbridge.connect(accounts[0]).addStakingRewards(h.toWei("1")) - expect(await tellor.balanceOf(await tbridge.address)).to.equal(h.toWei("1")) + assert(await tellor.balanceOf(await tbridge.address) == h.toWei("1"), "staking rewards should be added") }) it("getDataBefore()", async function () { @@ -146,69 +146,57 @@ describe("Function Tests - NewTransition", function() { it("getIndexForDataBefore()", async function () { indexBefore = await tbridge.getIndexForDataBefore(ETH_QUERY_ID, blocky0.timestamp) assert.equal(indexBefore[0], true) - expect(indexBefore[1]).to.be.greaterThan(0) - + assert(indexBefore[1] > 0, "should be positive") indexBefore1 = await tbridge.getIndexForDataBefore(ETH_QUERY_ID, blocky1.timestamp) assert.equal(indexBefore1[0], true) assert.equal(indexBefore1[1], BigInt(indexBefore[1]) + BigInt(1)) - indexBefore2 = await tbridge.getIndexForDataBefore(ETH_QUERY_ID, blocky2.timestamp) assert.equal(indexBefore2[0], true) assert.equal(indexBefore2[1], BigInt(indexBefore1[1]) + BigInt(1)) }) - it("getNewValueCountbyQueryId()", async function () { count = await tbridge.getNewValueCountbyQueryId(ETH_QUERY_ID) - expect(count).to.be.greaterThan(0) - + assert(count > 0, "count should be positive") await h.advanceTime(43200) await flex.submitValue(ETH_QUERY_ID, h.uintTob32("103"), 0, ETH_QUERY_DATA) count1 = await tbridge.getNewValueCountbyQueryId(ETH_QUERY_ID) - expect(count1).to.equal(BigInt(count) + BigInt(1)) + assert(count1 == BigInt(count) + BigInt(1), "new value count should increase") }) - it("getReporterByTimestamp()", async function () { reporter = await tbridge.getReporterByTimestamp(ETH_QUERY_ID, blocky0.timestamp) assert.equal(reporter, await accounts[0].address) }) - it("getTimestampbyQueryIdandIndex()", async function () { count = await tbridge.getNewValueCountbyQueryId(ETH_QUERY_ID) - expect(BigInt(count)).to.be.greaterThan(BigInt(0)) - + assert(BigInt(count) > 0, "cound should be positive") timestamp = await tbridge.getTimestampbyQueryIdandIndex(ETH_QUERY_ID, BigInt(count) - BigInt(1)) - expect(BigInt(timestamp)).to.equal(BigInt(blocky2.timestamp)) + assert(BigInt(timestamp) == BigInt(blocky2.timestamp), "getTimestampbyQueryIdAndIndex should work") }) - it("getTimeOfLastNewValue()", async function () { time = await tbridge.getTimeOfLastNewValue() - expect(time).to.equal(blocky2.timestamp) + assert(time == blocky2.timestamp, "timestamp should be correct") }) - it("isInDispute()", async function () { assert.equal(await tbridge.isInDispute(ETH_QUERY_ID, blocky2.timestamp), false) await tellor.connect(bigWallet).approve(GOVERNANCE_FLEX, h.toWei("100")) await govflex.connect(bigWallet).beginDispute(ETH_QUERY_ID, blocky2.timestamp) assert.equal(await tbridge.isInDispute(ETH_QUERY_ID, blocky2.timestamp), true) }) - it("verify()", async function () { - expect(await tbridge.verify()).to.equal(9999) + assert(await tbridge.verify() == 9999, "verify should be correct") }) - it("mintToOracle()", async function () { - expect(await tellor.balanceOf(await tbridge.address)).to.equal(0) + assert(await tellor.balanceOf(await tbridge.address) == 0) await tellor.mintToOracle() - expect(await tellor.balanceOf(await tbridge.address)).to.be.greaterThan(0) - + assert(await tellor.balanceOf(await tbridge.address) > 0, "tokens should be minted") }) it("mintToTeam()", async function () { - expect(await tellor.balanceOf(await tbridge.address)).to.equal(0) + assert(await tellor.balanceOf(await tbridge.address) == 0, "tellor balance should be right") await tellor.mintToOracle() - expect(await tellor.balanceOf(await tbridge.address)).to.be.greaterThan(0) + assert(await tellor.balanceOf(await tbridge.address) > 0, "should mint some"); let teamBal = await tellor.balanceOf(DEV_WALLET) await tellor.mintToTeam() - expect(await tellor.balanceOf(DEV_WALLET) > teamBal, "mint to team should work") + assert(await tellor.balanceOf(DEV_WALLET) > teamBal, "mint to team should work") }) }) \ No newline at end of file