diff --git a/src/TeleportConstantFee.sol b/src/TeleportConstantFee.sol new file mode 100644 index 0000000..2c6e26c --- /dev/null +++ b/src/TeleportConstantFee.sol @@ -0,0 +1,48 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later +// Copyright (C) 2021 Dai Foundation +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +pragma solidity 0.8.14; + +import {TeleportFees} from "./TeleportFees.sol"; +import {TeleportGUID} from "./TeleportGUID.sol"; + +contract TeleportConstantFee is TeleportFees { + uint256 immutable public fee; + uint256 immutable public ttl; + + /** + * @param _fee Constant fee in WAD + * @param _ttl Time in seconds to finalize flush (not teleport) + **/ + constructor(uint256 _fee, uint256 _ttl) { + fee = _fee; + ttl = _ttl; + } + + function getFee(TeleportGUID calldata guid, uint256, int256, uint256, uint256 amtToTake) override external view returns (uint256) { + // is slow withdrawal? + if (block.timestamp >= uint256(guid.timestamp) + ttl) { + return 0; + } + + // is empty teleport? + if (guid.amount == 0) { + return 0; + } + + return fee * amtToTake / guid.amount; + } +} diff --git a/src/TeleportFees.sol b/src/TeleportFees.sol new file mode 100644 index 0000000..6526541 --- /dev/null +++ b/src/TeleportFees.sol @@ -0,0 +1,36 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later +// Copyright (C) 2021 Dai Foundation +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +pragma solidity 0.8.14; + +import "./TeleportGUID.sol"; + +// Calculate fees for a given Teleport GUID +interface TeleportFees { + /** + * @dev Return fee for particular teleport. It should return 0 for teleports that are being slow withdrawn. + * note: We define slow withdrawal as teleport older than x. x has to be enough to finalize flush (not teleport itself). + * @param teleportGUID Struct which contains the whole teleport data + * @param line Debt ceiling + * @param debt Current debt + * @param pending Amount left to withdraw + * @param amtToTake Amount to take. Can be less or equal to teleportGUID.amount b/c of debt ceiling or because it is pending + * @return fees Fee amount [WAD] + **/ + function getFee( + TeleportGUID calldata teleportGUID, uint256 line, int256 debt, uint256 pending, uint256 amtToTake + ) external view returns (uint256 fees); +} diff --git a/src/TeleportGUID.sol b/src/TeleportGUID.sol new file mode 100644 index 0000000..9848304 --- /dev/null +++ b/src/TeleportGUID.sol @@ -0,0 +1,51 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later +// Copyright (C) 2021 Dai Foundation +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +pragma solidity 0.8.14; + +// Standard Maker Teleport GUID +struct TeleportGUID { + bytes32 sourceDomain; + bytes32 targetDomain; + bytes32 receiver; + bytes32 operator; + uint128 amount; + uint80 nonce; + uint48 timestamp; +} + +// solhint-disable-next-line func-visibility +function bytes32ToAddress(bytes32 addr) pure returns (address) { + return address(uint160(uint256(addr))); +} + +// solhint-disable-next-line func-visibility +function addressToBytes32(address addr) pure returns (bytes32) { + return bytes32(uint256(uint160(addr))); +} + +// solhint-disable-next-line func-visibility +function getGUIDHash(TeleportGUID memory teleportGUID) pure returns (bytes32 guidHash) { + guidHash = keccak256(abi.encode( + teleportGUID.sourceDomain, + teleportGUID.targetDomain, + teleportGUID.receiver, + teleportGUID.operator, + teleportGUID.amount, + teleportGUID.nonce, + teleportGUID.timestamp + )); +} diff --git a/src/TeleportJoin.sol b/src/TeleportJoin.sol new file mode 100644 index 0000000..f607c02 --- /dev/null +++ b/src/TeleportJoin.sol @@ -0,0 +1,273 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later +// Copyright (C) 2021 Dai Foundation +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +pragma solidity 0.8.14; + +import "./TeleportGUID.sol"; + +interface VatLike { + function dai(address) external view returns (uint256); + function live() external view returns (uint256); + function urns(bytes32, address) external view returns (uint256, uint256); + function frob(bytes32, address, address, address, int256, int256) external; + function hope(address) external; + function move(address, address, uint256) external; + function nope(address) external; + function slip(bytes32, address, int256) external; +} + +interface DaiJoinLike { + function dai() external view returns (TokenLike); + function exit(address, uint256) external; + function join(address, uint256) external; +} + +interface TokenLike { + function approve(address, uint256) external returns (bool); +} + +interface FeesLike { + function getFee(TeleportGUID calldata, uint256, int256, uint256, uint256) external view returns (uint256); +} + +// Primary control for extending Teleport credit +contract TeleportJoin { + mapping (address => uint256) public wards; // Auth + mapping (bytes32 => address) public fees; // Fees contract per source domain + mapping (bytes32 => uint256) public line; // Debt ceiling per source domain + mapping (bytes32 => int256) public debt; // Outstanding debt per source domain (can be < 0 when settlement occurs before mint) + mapping (bytes32 => TeleportStatus) public teleports; // Approved teleports and pending unpaid + + address public vow; + + uint256 internal art; // We need to preserve the last art value before the position being skimmed (End) + + VatLike immutable public vat; + DaiJoinLike immutable public daiJoin; + bytes32 immutable public ilk; + bytes32 immutable public domain; + + uint256 constant public WAD = 10 ** 18; + uint256 constant public RAY = 10 ** 27; + + event Rely(address indexed usr); + event Deny(address indexed usr); + event File(bytes32 indexed what, address data); + event File(bytes32 indexed what, bytes32 indexed domain, address data); + event File(bytes32 indexed what, bytes32 indexed domain, uint256 data); + event Register(bytes32 indexed hashGUID, TeleportGUID teleportGUID); + event Mint( + bytes32 indexed hashGUID, TeleportGUID teleportGUID, uint256 amount, uint256 maxFeePercentage, uint256 operatorFee, address originator + ); + event Settle(bytes32 indexed sourceDomain, uint256 batchedDaiToFlush); + + struct TeleportStatus { + bool blessed; + uint248 pending; + } + + constructor(address vat_, address daiJoin_, bytes32 ilk_, bytes32 domain_) { + wards[msg.sender] = 1; + emit Rely(msg.sender); + vat = VatLike(vat_); + daiJoin = DaiJoinLike(daiJoin_); + vat.hope(daiJoin_); + daiJoin.dai().approve(daiJoin_, type(uint256).max); + ilk = ilk_; + domain = domain_; + } + + function _min(uint256 x, uint256 y) internal pure returns (uint256 z) { + z = x <= y ? x : y; + } + + modifier auth { + require(wards[msg.sender] == 1, "TeleportJoin/not-authorized"); + _; + } + + function rely(address usr) external auth { + wards[usr] = 1; + emit Rely(usr); + } + + function deny(address usr) external auth { + wards[usr] = 0; + emit Deny(usr); + } + + function file(bytes32 what, address data) external auth { + if (what == "vow") { + vow = data; + } else { + revert("TeleportJoin/file-unrecognized-param"); + } + emit File(what, data); + } + + function file(bytes32 what, bytes32 domain_, address data) external auth { + if (what == "fees") { + fees[domain_] = data; + } else { + revert("TeleportJoin/file-unrecognized-param"); + } + emit File(what, domain_, data); + } + + function file(bytes32 what, bytes32 domain_, uint256 data) external auth { + if (what == "line") { + require(data <= 2 ** 255 - 1, "TeleportJoin/not-allowed-bigger-int256"); + line[domain_] = data; + } else { + revert("TeleportJoin/file-unrecognized-param"); + } + emit File(what, domain_, data); + } + + /** + * @dev External view function to get the total debt used by this contract [RAD] + **/ + function cure() external view returns (uint256 cure_) { + cure_ = art * RAY; + } + + /** + * @dev Internal function that executes the mint after a teleport is registered + * @param teleportGUID Struct which contains the whole teleport data + * @param hashGUID Hash of the prev struct + * @param maxFeePercentage Max percentage of the withdrawn amount (in WAD) to be paid as fee (e.g 1% = 0.01 * WAD) + * @param operatorFee The amount of DAI to pay to the operator + * @return postFeeAmount The amount of DAI sent to the receiver after taking out fees + * @return totalFee The total amount of DAI charged as fees + **/ + function _mint( + TeleportGUID calldata teleportGUID, + bytes32 hashGUID, + uint256 maxFeePercentage, + uint256 operatorFee + ) internal returns (uint256 postFeeAmount, uint256 totalFee) { + require(teleportGUID.targetDomain == domain, "TeleportJoin/incorrect-domain"); + + bool vatLive = vat.live() == 1; + + uint256 line_ = vatLive ? line[teleportGUID.sourceDomain] : 0; + + int256 debt_ = debt[teleportGUID.sourceDomain]; + + // Stop execution if there isn't anything available to withdraw + uint248 pending = teleports[hashGUID].pending; + if (int256(line_) <= debt_ || pending == 0) { + emit Mint(hashGUID, teleportGUID, 0, maxFeePercentage, operatorFee, msg.sender); + return (0, 0); + } + + uint256 amtToTake = _min( + pending, + uint256(int256(line_) - debt_) + ); + + uint256 fee = vatLive ? FeesLike(fees[teleportGUID.sourceDomain]).getFee(teleportGUID, line_, debt_, pending, amtToTake) : 0; + require(fee <= maxFeePercentage * amtToTake / WAD, "TeleportJoin/max-fee-exceed"); + + // No need of overflow check here as amtToTake is bounded by teleports[hashGUID].pending + // which is already a uint248. Also int256 >> uint248. Then both castings are safe. + debt[teleportGUID.sourceDomain] += int256(amtToTake); + teleports[hashGUID].pending -= uint248(amtToTake); + + if (debt_ >= 0 || uint256(-debt_) < amtToTake) { + uint256 amtToGenerate = debt_ < 0 + ? uint256(int256(amtToTake) + debt_) // amtToTake - |debt_| + : amtToTake; + // amtToGenerate doesn't need overflow check as it is bounded by amtToTake + vat.slip(ilk, address(this), int256(amtToGenerate)); + vat.frob(ilk, address(this), address(this), address(this), int256(amtToGenerate), int256(amtToGenerate)); + // Query the actual value as someone might have repaid debt without going through settle (if vat.live == 0 prev frob will revert) + (, art) = vat.urns(ilk, address(this)); + } + totalFee = fee + operatorFee; + postFeeAmount = amtToTake - totalFee; + daiJoin.exit(bytes32ToAddress(teleportGUID.receiver), postFeeAmount); + + if (fee > 0) { + vat.move(address(this), vow, fee * RAY); + } + if (operatorFee > 0) { + daiJoin.exit(bytes32ToAddress(teleportGUID.operator), operatorFee); + } + + emit Mint(hashGUID, teleportGUID, amtToTake, maxFeePercentage, operatorFee, msg.sender); + } + + /** + * @dev External authed function that registers the teleport and executes the mint after + * @param teleportGUID Struct which contains the whole teleport data + * @param maxFeePercentage Max percentage of the withdrawn amount (in WAD) to be paid as fee (e.g 1% = 0.01 * WAD) + * @param operatorFee The amount of DAI to pay to the operator + * @return postFeeAmount The amount of DAI sent to the receiver after taking out fees + * @return totalFee The total amount of DAI charged as fees + **/ + function requestMint( + TeleportGUID calldata teleportGUID, + uint256 maxFeePercentage, + uint256 operatorFee + ) external auth returns (uint256 postFeeAmount, uint256 totalFee) { + bytes32 hashGUID = getGUIDHash(teleportGUID); + require(!teleports[hashGUID].blessed, "TeleportJoin/already-blessed"); + teleports[hashGUID].blessed = true; + teleports[hashGUID].pending = teleportGUID.amount; + emit Register(hashGUID, teleportGUID); + (postFeeAmount, totalFee) = _mint(teleportGUID, hashGUID, maxFeePercentage, operatorFee); + } + + /** + * @dev External function that executes the mint of any pending and available amount (only callable by operator or receiver) + * @param teleportGUID Struct which contains the whole teleport data + * @param maxFeePercentage Max percentage of the withdrawn amount (in WAD) to be paid as fee (e.g 1% = 0.01 * WAD) + * @param operatorFee The amount of DAI to pay to the operator + * @return postFeeAmount The amount of DAI sent to the receiver after taking out fees + * @return totalFee The total amount of DAI charged as fees + **/ + function mintPending( + TeleportGUID calldata teleportGUID, + uint256 maxFeePercentage, + uint256 operatorFee + ) external returns (uint256 postFeeAmount, uint256 totalFee) { + require(bytes32ToAddress(teleportGUID.receiver) == msg.sender || + bytes32ToAddress(teleportGUID.operator) == msg.sender, "TeleportJoin/not-receiver-nor-operator"); + (postFeeAmount, totalFee) = _mint(teleportGUID, getGUIDHash(teleportGUID), maxFeePercentage, operatorFee); + } + + /** + * @dev External function that repays debt with DAI previously pushed to this contract (in general coming from the bridges) + * @param sourceDomain domain where the DAI is coming from + * @param batchedDaiToFlush Amount of DAI that is being processed for repayment + **/ + function settle(bytes32 sourceDomain, uint256 batchedDaiToFlush) external { + require(batchedDaiToFlush <= 2 ** 255, "TeleportJoin/overflow"); + daiJoin.join(address(this), batchedDaiToFlush); + if (vat.live() == 1) { + (, uint256 art_) = vat.urns(ilk, address(this)); // rate == RAY => normalized debt == actual debt + uint256 amtToPayBack = _min(batchedDaiToFlush, art_); + vat.frob(ilk, address(this), address(this), address(this), -int256(amtToPayBack), -int256(amtToPayBack)); + vat.slip(ilk, address(this), -int256(amtToPayBack)); + unchecked { + art = art_ - amtToPayBack; // Always safe operation + } + } + debt[sourceDomain] -= int256(batchedDaiToFlush); + emit Settle(sourceDomain, batchedDaiToFlush); + } +} diff --git a/src/TeleportOracleAuth.sol b/src/TeleportOracleAuth.sol new file mode 100644 index 0000000..40911f0 --- /dev/null +++ b/src/TeleportOracleAuth.sol @@ -0,0 +1,174 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later +// Copyright (C) 2021 Dai Foundation +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +pragma solidity 0.8.14; + +import "./TeleportGUID.sol"; + +interface TeleportJoinLike { + function requestMint( + TeleportGUID calldata teleportGUID, + uint256 maxFeePercentage, + uint256 operatorFee + ) external returns (uint256 postFeeAmount, uint256 totalFee); +} + +// TeleportOracleAuth provides user authentication for TeleportJoin, by means of Maker Oracle Attestations +contract TeleportOracleAuth { + + mapping (address => uint256) public wards; // Auth + mapping (address => uint256) public signers; // Oracle feeds + + TeleportJoinLike immutable public teleportJoin; + + uint256 public threshold; + + event Rely(address indexed usr); + event Deny(address indexed usr); + event File(bytes32 indexed what, uint256 data); + event SignersAdded(address[] signers); + event SignersRemoved(address[] signers); + + modifier auth { + require(wards[msg.sender] == 1, "TeleportOracleAuth/not-authorized"); + _; + } + + constructor(address teleportJoin_) { + wards[msg.sender] = 1; + emit Rely(msg.sender); + teleportJoin = TeleportJoinLike(teleportJoin_); + } + + function rely(address usr) external auth { + wards[usr] = 1; + emit Rely(usr); + } + + function deny(address usr) external auth { + wards[usr] = 0; + emit Deny(usr); + } + + function file(bytes32 what, uint256 data) external auth { + if (what == "threshold") { + threshold = data; + } else { + revert("TeleportOracleAuth/file-unrecognized-param"); + } + emit File(what, data); + } + + function addSigners(address[] calldata signers_) external auth { + for(uint i; i < signers_.length; i++) { + signers[signers_[i]] = 1; + } + emit SignersAdded(signers_); + } + + function removeSigners(address[] calldata signers_) external auth { + for(uint i; i < signers_.length; i++) { + signers[signers_[i]] = 0; + } + emit SignersRemoved(signers_); + } + + /** + * @notice Verify oracle signatures and call TeleportJoin to mint DAI if the signatures are valid + * (only callable by teleport's operator or receiver) + * @param teleportGUID The teleport GUID to register + * @param signatures The byte array of concatenated signatures ordered by increasing signer addresses. + * Each signature is {bytes32 r}{bytes32 s}{uint8 v} + * @param maxFeePercentage Max percentage of the withdrawn amount (in WAD) to be paid as fee (e.g 1% = 0.01 * WAD) + * @param operatorFee The amount of DAI to pay to the operator + * @return postFeeAmount The amount of DAI sent to the receiver after taking out fees + * @return totalFee The total amount of DAI charged as fees + */ + function requestMint( + TeleportGUID calldata teleportGUID, + bytes calldata signatures, + uint256 maxFeePercentage, + uint256 operatorFee + ) external returns (uint256 postFeeAmount, uint256 totalFee) { + require(bytes32ToAddress(teleportGUID.receiver) == msg.sender || + bytes32ToAddress(teleportGUID.operator) == msg.sender, "TeleportOracleAuth/not-receiver-nor-operator"); + require(isValid(getSignHash(teleportGUID), signatures, threshold), "TeleportOracleAuth/not-enough-valid-sig"); + (postFeeAmount, totalFee) = teleportJoin.requestMint(teleportGUID, maxFeePercentage, operatorFee); + } + + /** + * @notice Returns true if `signatures` contains at least `threshold_` valid signatures of a given `signHash` + * @param signHash The signed message hash + * @param signatures The byte array of concatenated signatures ordered by increasing signer addresses. + * Each signature is {bytes32 r}{bytes32 s}{uint8 v} + * @param threshold_ The minimum number of valid signatures required for the method to return true + * @return valid Signature verification result + */ + function isValid(bytes32 signHash, bytes calldata signatures, uint threshold_) public view returns (bool valid) { + uint256 count = signatures.length / 65; + require(count >= threshold_, "TeleportOracleAuth/not-enough-sig"); + + uint8 v; + bytes32 r; + bytes32 s; + uint256 numValid; + address lastSigner; + for (uint256 i; i < count;) { + (v,r,s) = splitSignature(signatures, i); + address recovered = ecrecover(signHash, v, r, s); + require(recovered > lastSigner, "TeleportOracleAuth/bad-sig-order"); // make sure signers are different + lastSigner = recovered; + if (signers[recovered] == 1) { + unchecked { numValid += 1; } + if (numValid >= threshold_) { + return true; + } + } + unchecked { i++; } + } + } + + /** + * @notice This has to match what oracles are signing + * @param teleportGUID The teleport GUID to calculate hash + */ + function getSignHash(TeleportGUID memory teleportGUID) public pure returns (bytes32 signHash) { + signHash = keccak256(abi.encodePacked( + "\x19Ethereum Signed Message:\n32", + getGUIDHash(teleportGUID) + )); + } + + /** + * @notice Parses the signatures and extract (r, s, v) for a signature at a given index. + * @param signatures concatenated signatures. Each signature is {bytes32 r}{bytes32 s}{uint8 v} + * @param index which signature to read (0, 1, 2, ...) + */ + function splitSignature(bytes calldata signatures, uint256 index) internal pure returns (uint8 v, bytes32 r, bytes32 s) { + // we jump signatures.offset to get the first slot of signatures content + // we jump 65 (0x41) per signature + // for v we load 32 bytes ending with v (the first 31 come from s) then apply a mask + uint256 start; + // solhint-disable-next-line no-inline-assembly + assembly { + start := mul(0x41, index) + r := calldataload(add(signatures.offset, start)) + s := calldataload(add(signatures.offset, add(0x20, start))) + v := and(calldataload(add(signatures.offset, add(0x21, start))), 0xff) + } + require(v == 27 || v == 28, "TeleportOracleAuth/bad-v"); + } +} diff --git a/src/TeleportRouter.sol b/src/TeleportRouter.sol new file mode 100644 index 0000000..2b49f09 --- /dev/null +++ b/src/TeleportRouter.sol @@ -0,0 +1,156 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later +// Copyright (C) 2021 Dai Foundation +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +pragma solidity 0.8.14; + +import "./TeleportGUID.sol"; +import "./utils/EnumerableSet.sol"; + +interface TokenLike { + function transferFrom(address _from, address _to, uint256 _value) external returns (bool success); +} + +interface GatewayLike { + function requestMint( + TeleportGUID calldata teleportGUID, + uint256 maxFeePercentage, + uint256 operatorFee + ) external returns (uint256 postFeeAmount, uint256 totalFee); + function settle(bytes32 sourceDomain, uint256 batchedDaiToFlush) external; +} + +contract TeleportRouter { + + using EnumerableSet for EnumerableSet.Bytes32Set; + + mapping (address => uint256) public wards; // Auth + mapping (bytes32 => address) public gateways; // GatewayLike contracts called by the router for each domain + mapping (address => bytes32) public domains; // Domains for each gateway + + EnumerableSet.Bytes32Set private allDomains; + + TokenLike immutable public dai; // L1 DAI ERC20 token + + event Rely(address indexed usr); + event Deny(address indexed usr); + event File(bytes32 indexed what, bytes32 indexed domain, address data); + + modifier auth { + require(wards[msg.sender] == 1, "TeleportRouter/not-authorized"); + _; + } + + constructor(address dai_) { + dai = TokenLike(dai_); + wards[msg.sender] = 1; + emit Rely(msg.sender); + } + + function rely(address usr) external auth { + wards[usr] = 1; + emit Rely(usr); + } + + function deny(address usr) external auth { + wards[usr] = 0; + emit Deny(usr); + } + + /** + * @notice Allows auth to configure the router. The only supported operation is "gateway", + * which allows adding, replacing or removing a gateway contract for a given domain. The router forwards `settle()` + * and `requestMint()` calls to the gateway contract installed for a given domain. Gateway contracts must therefore + * conform to the GatewayLike interface. Examples of valid gateways include TeleportJoin (for the L1 domain) + * and L1 bridge contracts (for L2 domains). + * @dev In addition to updating the mapping `gateways` which maps GatewayLike contracts to domain names and + * the reverse mapping `domains` which maps domain names to GatewayLike contracts, this method also maintains + * the enumerable set `allDomains`. + * @param what The name of the operation. Only "gateway" is supported. + * @param domain The domain for which a GatewayLike contract is added, replaced or removed. + * @param data The address of the GatewayLike contract to install for the domain (or address(0) to remove a domain) + */ + function file(bytes32 what, bytes32 domain, address data) external auth { + if (what == "gateway") { + address prevGateway = gateways[domain]; + if(prevGateway == address(0)) { + // new domain => add it to allDomains + if(data != address(0)) { + allDomains.add(domain); + } + } else { + // existing domain + domains[prevGateway] = bytes32(0); + if(data == address(0)) { + // => remove domain from allDomains + allDomains.remove(domain); + } + } + + gateways[domain] = data; + if(data != address(0)) { + domains[data] = domain; + } + } else { + revert("TeleportRouter/file-unrecognized-param"); + } + emit File(what, domain, data); + } + + function numDomains() external view returns (uint256) { + return allDomains.length(); + } + function domainAt(uint256 index) external view returns (bytes32) { + return allDomains.at(index); + } + function hasDomain(bytes32 domain) external view returns (bool) { + return allDomains.contains(domain); + } + + /** + * @notice Call a GatewayLike contract to request the minting of DAI. The sender must be a supported gateway + * @param teleportGUID The teleport GUID to register + * @param maxFeePercentage Max percentage of the withdrawn amount (in WAD) to be paid as fee (e.g 1% = 0.01 * WAD) + * @param operatorFee The amount of DAI to pay to the operator + * @return postFeeAmount The amount of DAI sent to the receiver after taking out fees + * @return totalFee The total amount of DAI charged as fees + */ + function requestMint( + TeleportGUID calldata teleportGUID, + uint256 maxFeePercentage, + uint256 operatorFee + ) external returns (uint256 postFeeAmount, uint256 totalFee) { + require(msg.sender == gateways[teleportGUID.sourceDomain], "TeleportRouter/sender-not-gateway"); + address gateway = gateways[teleportGUID.targetDomain]; + require(gateway != address(0), "TeleportRouter/unsupported-target-domain"); + (postFeeAmount, totalFee) = GatewayLike(gateway).requestMint(teleportGUID, maxFeePercentage, operatorFee); + } + + /** + * @notice Call a GatewayLike contract to settle a batch of sourceDomain -> targetDomain DAI transfer. + * The sender must be a supported gateway + * @param targetDomain The domain receiving the batch of DAI (only L1 supported for now) + * @param batchedDaiToFlush The amount of DAI in the batch + */ + function settle(bytes32 targetDomain, uint256 batchedDaiToFlush) external { + bytes32 sourceDomain = domains[msg.sender]; + require(sourceDomain != bytes32(0), "TeleportRouter/sender-not-gateway"); + address gateway = gateways[targetDomain]; + require(gateway != address(0), "TeleportRouter/unsupported-target-domain"); + // Forward the DAI to settle to the gateway contract + dai.transferFrom(msg.sender, gateway, batchedDaiToFlush); + GatewayLike(gateway).settle(sourceDomain, batchedDaiToFlush); + } +} diff --git a/src/utils/EnumerableSet.sol b/src/utils/EnumerableSet.sol new file mode 100644 index 0000000..68148e9 --- /dev/null +++ b/src/utils/EnumerableSet.sol @@ -0,0 +1,357 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts v4.4.1 (utils/structs/EnumerableSet.sol) + +pragma solidity ^0.8.0; + +/** + * @dev Library for managing + * https://en.wikipedia.org/wiki/Set_(abstract_data_type)[sets] of primitive + * types. + * + * Sets have the following properties: + * + * - Elements are added, removed, and checked for existence in constant time + * (O(1)). + * - Elements are enumerated in O(n). No guarantees are made on the ordering. + * + * ``` + * contract Example { + * // Add the library methods + * using EnumerableSet for EnumerableSet.AddressSet; + * + * // Declare a set state variable + * EnumerableSet.AddressSet private mySet; + * } + * ``` + * + * As of v3.3.0, sets of type `bytes32` (`Bytes32Set`), `address` (`AddressSet`) + * and `uint256` (`UintSet`) are supported. + */ +library EnumerableSet { + // To implement this library for multiple types with as little code + // repetition as possible, we write it in terms of a generic Set type with + // bytes32 values. + // The Set implementation uses private functions, and user-facing + // implementations (such as AddressSet) are just wrappers around the + // underlying Set. + // This means that we can only create new EnumerableSets for types that fit + // in bytes32. + + struct Set { + // Storage of set values + bytes32[] _values; + // Position of the value in the `values` array, plus 1 because index 0 + // means a value is not in the set. + mapping(bytes32 => uint256) _indexes; + } + + /** + * @dev Add a value to a set. O(1). + * + * Returns true if the value was added to the set, that is if it was not + * already present. + */ + function _add(Set storage set, bytes32 value) private returns (bool) { + if (!_contains(set, value)) { + set._values.push(value); + // The value is stored at length-1, but we add 1 to all indexes + // and use 0 as a sentinel value + set._indexes[value] = set._values.length; + return true; + } else { + return false; + } + } + + /** + * @dev Removes a value from a set. O(1). + * + * Returns true if the value was removed from the set, that is if it was + * present. + */ + function _remove(Set storage set, bytes32 value) private returns (bool) { + // We read and store the value's index to prevent multiple reads from the same storage slot + uint256 valueIndex = set._indexes[value]; + + if (valueIndex != 0) { + // Equivalent to contains(set, value) + // To delete an element from the _values array in O(1), we swap the element to delete with the last one in + // the array, and then remove the last element (sometimes called as 'swap and pop'). + // This modifies the order of the array, as noted in {at}. + + uint256 toDeleteIndex = valueIndex - 1; + uint256 lastIndex = set._values.length - 1; + + if (lastIndex != toDeleteIndex) { + bytes32 lastvalue = set._values[lastIndex]; + + // Move the last value to the index where the value to delete is + set._values[toDeleteIndex] = lastvalue; + // Update the index for the moved value + set._indexes[lastvalue] = valueIndex; // Replace lastvalue's index to valueIndex + } + + // Delete the slot where the moved value was stored + set._values.pop(); + + // Delete the index for the deleted slot + delete set._indexes[value]; + + return true; + } else { + return false; + } + } + + /** + * @dev Returns true if the value is in the set. O(1). + */ + function _contains(Set storage set, bytes32 value) private view returns (bool) { + return set._indexes[value] != 0; + } + + /** + * @dev Returns the number of values on the set. O(1). + */ + function _length(Set storage set) private view returns (uint256) { + return set._values.length; + } + + /** + * @dev Returns the value stored at position `index` in the set. O(1). + * + * Note that there are no guarantees on the ordering of values inside the + * array, and it may change when more values are added or removed. + * + * Requirements: + * + * - `index` must be strictly less than {length}. + */ + function _at(Set storage set, uint256 index) private view returns (bytes32) { + return set._values[index]; + } + + /** + * @dev Return the entire set in an array + * + * WARNING: This operation will copy the entire storage to memory, which can be quite expensive. This is designed + * to mostly be used by view accessors that are queried without any gas fees. Developers should keep in mind that + * this function has an unbounded cost, and using it as part of a state-changing function may render the function + * uncallable if the set grows to a point where copying to memory consumes too much gas to fit in a block. + */ + function _values(Set storage set) private view returns (bytes32[] memory) { + return set._values; + } + + // Bytes32Set + + struct Bytes32Set { + Set _inner; + } + + /** + * @dev Add a value to a set. O(1). + * + * Returns true if the value was added to the set, that is if it was not + * already present. + */ + function add(Bytes32Set storage set, bytes32 value) internal returns (bool) { + return _add(set._inner, value); + } + + /** + * @dev Removes a value from a set. O(1). + * + * Returns true if the value was removed from the set, that is if it was + * present. + */ + function remove(Bytes32Set storage set, bytes32 value) internal returns (bool) { + return _remove(set._inner, value); + } + + /** + * @dev Returns true if the value is in the set. O(1). + */ + function contains(Bytes32Set storage set, bytes32 value) internal view returns (bool) { + return _contains(set._inner, value); + } + + /** + * @dev Returns the number of values in the set. O(1). + */ + function length(Bytes32Set storage set) internal view returns (uint256) { + return _length(set._inner); + } + + /** + * @dev Returns the value stored at position `index` in the set. O(1). + * + * Note that there are no guarantees on the ordering of values inside the + * array, and it may change when more values are added or removed. + * + * Requirements: + * + * - `index` must be strictly less than {length}. + */ + function at(Bytes32Set storage set, uint256 index) internal view returns (bytes32) { + return _at(set._inner, index); + } + + /** + * @dev Return the entire set in an array + * + * WARNING: This operation will copy the entire storage to memory, which can be quite expensive. This is designed + * to mostly be used by view accessors that are queried without any gas fees. Developers should keep in mind that + * this function has an unbounded cost, and using it as part of a state-changing function may render the function + * uncallable if the set grows to a point where copying to memory consumes too much gas to fit in a block. + */ + function values(Bytes32Set storage set) internal view returns (bytes32[] memory) { + return _values(set._inner); + } + + // AddressSet + + struct AddressSet { + Set _inner; + } + + /** + * @dev Add a value to a set. O(1). + * + * Returns true if the value was added to the set, that is if it was not + * already present. + */ + function add(AddressSet storage set, address value) internal returns (bool) { + return _add(set._inner, bytes32(uint256(uint160(value)))); + } + + /** + * @dev Removes a value from a set. O(1). + * + * Returns true if the value was removed from the set, that is if it was + * present. + */ + function remove(AddressSet storage set, address value) internal returns (bool) { + return _remove(set._inner, bytes32(uint256(uint160(value)))); + } + + /** + * @dev Returns true if the value is in the set. O(1). + */ + function contains(AddressSet storage set, address value) internal view returns (bool) { + return _contains(set._inner, bytes32(uint256(uint160(value)))); + } + + /** + * @dev Returns the number of values in the set. O(1). + */ + function length(AddressSet storage set) internal view returns (uint256) { + return _length(set._inner); + } + + /** + * @dev Returns the value stored at position `index` in the set. O(1). + * + * Note that there are no guarantees on the ordering of values inside the + * array, and it may change when more values are added or removed. + * + * Requirements: + * + * - `index` must be strictly less than {length}. + */ + function at(AddressSet storage set, uint256 index) internal view returns (address) { + return address(uint160(uint256(_at(set._inner, index)))); + } + + /** + * @dev Return the entire set in an array + * + * WARNING: This operation will copy the entire storage to memory, which can be quite expensive. This is designed + * to mostly be used by view accessors that are queried without any gas fees. Developers should keep in mind that + * this function has an unbounded cost, and using it as part of a state-changing function may render the function + * uncallable if the set grows to a point where copying to memory consumes too much gas to fit in a block. + */ + function values(AddressSet storage set) internal view returns (address[] memory) { + bytes32[] memory store = _values(set._inner); + address[] memory result; + + assembly { + result := store + } + + return result; + } + + // UintSet + + struct UintSet { + Set _inner; + } + + /** + * @dev Add a value to a set. O(1). + * + * Returns true if the value was added to the set, that is if it was not + * already present. + */ + function add(UintSet storage set, uint256 value) internal returns (bool) { + return _add(set._inner, bytes32(value)); + } + + /** + * @dev Removes a value from a set. O(1). + * + * Returns true if the value was removed from the set, that is if it was + * present. + */ + function remove(UintSet storage set, uint256 value) internal returns (bool) { + return _remove(set._inner, bytes32(value)); + } + + /** + * @dev Returns true if the value is in the set. O(1). + */ + function contains(UintSet storage set, uint256 value) internal view returns (bool) { + return _contains(set._inner, bytes32(value)); + } + + /** + * @dev Returns the number of values on the set. O(1). + */ + function length(UintSet storage set) internal view returns (uint256) { + return _length(set._inner); + } + + /** + * @dev Returns the value stored at position `index` in the set. O(1). + * + * Note that there are no guarantees on the ordering of values inside the + * array, and it may change when more values are added or removed. + * + * Requirements: + * + * - `index` must be strictly less than {length}. + */ + function at(UintSet storage set, uint256 index) internal view returns (uint256) { + return uint256(_at(set._inner, index)); + } + + /** + * @dev Return the entire set in an array + * + * WARNING: This operation will copy the entire storage to memory, which can be quite expensive. This is designed + * to mostly be used by view accessors that are queried without any gas fees. Developers should keep in mind that + * this function has an unbounded cost, and using it as part of a state-changing function may render the function + * uncallable if the set grows to a point where copying to memory consumes too much gas to fit in a block. + */ + function values(UintSet storage set) internal view returns (uint256[] memory) { + bytes32[] memory store = _values(set._inner); + uint256[] memory result; + + assembly { + result := store + } + + return result; + } +}