Skip to content

Commit

Permalink
Merge pull request #183 from tellor-io/withdrawLimit
Browse files Browse the repository at this point in the history
fixed tests and added withdraw limit
  • Loading branch information
akremstudy authored Jun 25, 2024
2 parents 843103b + 9865c77 commit 61f0bc8
Show file tree
Hide file tree
Showing 6 changed files with 123 additions and 38 deletions.
7 changes: 5 additions & 2 deletions adr/adr2001 - trb bridge structure.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand All @@ -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.
36 changes: 30 additions & 6 deletions evm/contracts/token-bridge/TokenBridge.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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);
Expand All @@ -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) {
Expand All @@ -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;
}


}
6 changes: 3 additions & 3 deletions evm/hardhat.config.js
Original file line number Diff line number Diff line change
@@ -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: {
Expand Down
3 changes: 1 addition & 2 deletions evm/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
71 changes: 71 additions & 0 deletions evm/test/TokenBridgeFunctionTestsHH.js
Original file line number Diff line number Diff line change
Expand Up @@ -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")
})
})
38 changes: 13 additions & 25 deletions evm/test/TokenBridgeTransition-FunctionTests.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 () {
Expand Down Expand Up @@ -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")
})

})

0 comments on commit 61f0bc8

Please sign in to comment.