diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index e9be239..7a90aa0 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -114,7 +114,7 @@ jobs: - name: Export deployments run: | - for NETWORK in bsctestnet bscmainnet ethereum sepolia; do + for NETWORK in bsctestnet bscmainnet ethereum sepolia opbnbtestnet opbnbmainnet; do EXPORT=true yarn hardhat export --network ${NETWORK} --export ./deployments/${NETWORK}.json jq -M '{name, chainId, addresses: .contracts | map_values(.address)}' ./deployments/${NETWORK}.json > ./deployments/${NETWORK}_addresses.json done diff --git a/contracts/Bridge/BaseXVSProxyOFT.sol b/contracts/Bridge/BaseXVSProxyOFT.sol index 4138ba6..c26e077 100644 --- a/contracts/Bridge/BaseXVSProxyOFT.sol +++ b/contracts/Bridge/BaseXVSProxyOFT.sol @@ -62,12 +62,12 @@ abstract contract BaseXVSProxyOFT is Pausable, ExponentialNoError, BaseOFTV2 { */ mapping(uint16 => uint256) public chainIdToLast24HourReceiveWindowStart; /** - * @notice Address on which cap check and bound limit is not appicable. + * @notice Address on which cap check and bound limit is not applicable. */ mapping(address => bool) public whitelist; /** - * @notice Emmited when address is added to whitelist. + * @notice Emitted when address is added to whitelist. */ event SetWhitelist(address indexed addr, bool isWhitelist); /** @@ -98,20 +98,29 @@ abstract contract BaseXVSProxyOFT is Pausable, ExponentialNoError, BaseOFTV2 { * @notice Event emitted when inner token set successfully. */ event InnerTokenAdded(address indexed innerToken); - + /** + *@notice Emitted on sweep token success + */ + event SweepToken(address indexed token, address indexed to, uint256 sweepAmount); /** * @notice Event emitted when SendAndCallEnabled updated successfully. */ event UpdateSendAndCallEnabled(bool indexed enabled); + /** + *@notice Error thrown when this contract balance is less than sweep amount + */ + error InsufficientBalance(uint256 sweepAmount, uint256 balance); /** * @param tokenAddress_ Address of the inner token. - * @param sharedDecimals_ No of shared decimals. + * @param sharedDecimals_ Number of shared decimals. * @param lzEndpoint_ Address of the layer zero endpoint contract. * @param oracle_ Address of the price oracle. * @custom:error ZeroAddressNotAllowed is thrown when token contract address is zero. * @custom:error ZeroAddressNotAllowed is thrown when lzEndpoint contract address is zero. * @custom:error ZeroAddressNotAllowed is thrown when oracle contract address is zero. + * @custom:event Emits InnerTokenAdded with token address. + * @custom:event Emits OracleChanged with zero address and oracle address. */ constructor( address tokenAddress_, @@ -121,6 +130,7 @@ abstract contract BaseXVSProxyOFT is Pausable, ExponentialNoError, BaseOFTV2 { ) BaseOFTV2(sharedDecimals_, lzEndpoint_) { ensureNonzeroAddress(tokenAddress_); ensureNonzeroAddress(lzEndpoint_); + ensureNonzeroAddress(oracle_); innerToken = IERC20(tokenAddress_); @@ -131,8 +141,6 @@ abstract contract BaseXVSProxyOFT is Pausable, ExponentialNoError, BaseOFTV2 { require(sharedDecimals_ <= decimals, "ProxyOFT: sharedDecimals must be <= decimals"); ld2sdRate = 10 ** (decimals - sharedDecimals_); - ensureNonzeroAddress(oracle_); - emit InnerTokenAdded(tokenAddress_); emit OracleChanged(address(0), oracle_); @@ -209,7 +217,7 @@ abstract contract BaseXVSProxyOFT is Pausable, ExponentialNoError, BaseOFTV2 { /** * @notice Sets the whitelist address to skip checks on transaction limit. - * @param user_ Adress to be add in whitelist. + * @param user_ Address to be add in whitelist. * @param val_ Boolean to be set (true for user_ address is whitelisted). * @custom:access Only owner. * @custom:event Emits setWhitelist. @@ -235,9 +243,31 @@ abstract contract BaseXVSProxyOFT is Pausable, ExponentialNoError, BaseOFTV2 { _unpause(); } + /** + * @notice A public function to sweep accidental ERC-20 transfers to this contract. Tokens are sent to user + * @param token_ The address of the ERC-20 token to sweep + * @param to_ The address of the recipient + * @param amount_ The amount of tokens needs to transfer + * @custom:event Emits SweepToken event + * @custom:error Throw InsufficientBalance if amount_ is greater than the available balance of the token in the contract + * @custom:access Only Owner + */ + function sweepToken(IERC20 token_, address to_, uint256 amount_) external onlyOwner { + uint256 balance = token_.balanceOf(address(this)); + if (amount_ > balance) { + revert InsufficientBalance(amount_, balance); + } + + emit SweepToken(address(token_), to_, amount_); + + token_.safeTransfer(to_, amount_); + } + /** * @notice Remove trusted remote from storage. * @param remoteChainId_ The chain's id corresponds to setting the trusted remote to empty. + * @custom:access Only owner. + * @custom:event Emits TrustedRemoteRemoved once chain id is removed from trusted remote. */ function removeTrustedRemote(uint16 remoteChainId_) external onlyOwner { delete trustedRemoteLookup[remoteChainId_]; @@ -267,6 +297,61 @@ abstract contract BaseXVSProxyOFT is Pausable, ExponentialNoError, BaseOFTV2 { * and adapter params. */ + /** + * @notice Checks the eligibility of a sender to initiate a cross-chain token transfer. + * @dev This external view function assesses whether the specified sender is eligible to transfer the given amount + * to the specified destination chain. It considers factors such as whitelisting, transaction limits, and a 24-hour window. + * @param from_ The sender's address initiating the transfer. + * @param dstChainId_ Indicates destination chain. + * @param amount_ The quantity of tokens to be transferred. + * @return eligibleToSend A boolean indicating whether the sender is eligible to transfer the tokens. + * @return maxSingleTransactionLimit The maximum limit for a single transaction. + * @return maxDailyLimit The maximum daily limit for transactions. + * @return amountInUsd The equivalent amount in USD based on the oracle price. + * @return transferredInWindow The total amount transferred in the current 24-hour window. + * @return last24HourWindowStart The timestamp when the current 24-hour window started. + * @return isWhiteListedUser A boolean indicating whether the sender is whitelisted. + */ + function isEligibleToSend( + address from_, + uint16 dstChainId_, + uint256 amount_ + ) + external + view + returns ( + bool eligibleToSend, + uint256 maxSingleTransactionLimit, + uint256 maxDailyLimit, + uint256 amountInUsd, + uint256 transferredInWindow, + uint256 last24HourWindowStart, + bool isWhiteListedUser + ) + { + // Check if the sender's address is whitelisted + isWhiteListedUser = whitelist[from_]; + + // Calculate the amount in USD using the oracle price + Exp memory oraclePrice = Exp({ mantissa: oracle.getPrice(token()) }); + amountInUsd = mul_ScalarTruncate(oraclePrice, amount_); + + // Load values for the 24-hour window checks + uint256 currentBlockTimestamp = block.timestamp; + last24HourWindowStart = chainIdToLast24HourWindowStart[dstChainId_]; + transferredInWindow = chainIdToLast24HourTransferred[dstChainId_]; + maxSingleTransactionLimit = chainIdToMaxSingleTransactionLimit[dstChainId_]; + maxDailyLimit = chainIdToMaxDailyLimit[dstChainId_]; + if (currentBlockTimestamp - last24HourWindowStart > 1 days) { + transferredInWindow = amountInUsd; + last24HourWindowStart = currentBlockTimestamp; + } else { + transferredInWindow += amountInUsd; + } + eligibleToSend = (isWhiteListedUser || + ((amountInUsd <= maxSingleTransactionLimit) && (transferredInWindow <= maxDailyLimit))); + } + function sendAndCall( address from_, uint16 dstChainId_, @@ -281,6 +366,23 @@ abstract contract BaseXVSProxyOFT is Pausable, ExponentialNoError, BaseOFTV2 { super.sendAndCall(from_, dstChainId_, toAddress_, amount_, payload_, dstGasForCall_, callparams_); } + function retryMessage( + uint16 _srcChainId, + bytes calldata _srcAddress, + uint64 _nonce, + bytes calldata _payload + ) public payable override { + bytes memory trustedRemote = trustedRemoteLookup[_srcChainId]; + // it will still block the message pathway from (srcChainId, srcAddress). should not receive message from untrusted remote. + require( + _srcAddress.length == trustedRemote.length && + trustedRemote.length > 0 && + keccak256(_srcAddress) == keccak256(trustedRemote), + "LzApp: invalid source sending contract" + ); + super.retryMessage(_srcChainId, _srcAddress, _nonce, _payload); + } + /** * @notice Empty implementation of renounce ownership to avoid any mishappening. */ @@ -288,11 +390,18 @@ abstract contract BaseXVSProxyOFT is Pausable, ExponentialNoError, BaseOFTV2 { /** * @notice Return's the address of the inner token of this bridge. + * @return Address of the inner token of this bridge. */ function token() public view override returns (address) { return address(innerToken); } + /** + * @notice Checks if the sender is eligible to send tokens + * @param from_ Sender's address sending tokens + * @param dstChainId_ Chain id on which tokens should be sent + * @param amount_ Amount of tokens to be sent + */ function _isEligibleToSend(address from_, uint16 dstChainId_, uint256 amount_) internal { // Check if the sender's address is whitelisted bool isWhiteListedUser = whitelist[from_]; @@ -331,6 +440,12 @@ abstract contract BaseXVSProxyOFT is Pausable, ExponentialNoError, BaseOFTV2 { chainIdToLast24HourTransferred[dstChainId_] = transferredInWindow; } + /** + * @notice Checks if receiver is able to receive tokens + * @param toAddress_ Receiver address + * @param srcChainId_ Source chain id from which token is send + * @param receivedAmount_ Amount of tokens received + */ function _isEligibleToReceive(address toAddress_, uint16 srcChainId_, uint256 receivedAmount_) internal { // Check if the recipient's address is whitelisted bool isWhiteListedUser = whitelist[toAddress_]; @@ -370,6 +485,13 @@ abstract contract BaseXVSProxyOFT is Pausable, ExponentialNoError, BaseOFTV2 { chainIdToLast24HourReceived[srcChainId_] = receivedInWindow; } + /** + * @notice Transfer tokens from sender to receiver account. + * @param from_ Address from which token has to be transferred(Sender). + * @param to_ Address on which token will be tranferred(Receiver). + * @param amount_ Amount of token to be transferred. + * @return Actual balance difference. + */ function _transferFrom( address from_, address to_, @@ -384,23 +506,10 @@ abstract contract BaseXVSProxyOFT is Pausable, ExponentialNoError, BaseOFTV2 { return innerToken.balanceOf(to_) - before; } - function _nonblockingLzReceive( - uint16 _srcChainId, - bytes memory _srcAddress, - uint64 _nonce, - bytes memory _payload - ) internal override { - bytes memory trustedRemote = trustedRemoteLookup[_srcChainId]; - // if will still block the message pathway from (srcChainId, srcAddress). should not receive message from untrusted remote. - require( - _srcAddress.length == trustedRemote.length && - trustedRemote.length > 0 && - keccak256(_srcAddress) == keccak256(trustedRemote), - "LzApp: invalid source sending contract" - ); - super._nonblockingLzReceive(_srcChainId, _srcAddress, _nonce, _payload); - } - + /** + * @notice Returns Conversion rate factor from large decimals to shared decimals. + * @return Conversion rate factor. + */ function _ld2sdRate() internal view override returns (uint256) { return ld2sdRate; } diff --git a/contracts/Bridge/XVSBridgeAdmin.sol b/contracts/Bridge/XVSBridgeAdmin.sol index fb7cba0..ac61317 100644 --- a/contracts/Bridge/XVSBridgeAdmin.sol +++ b/contracts/Bridge/XVSBridgeAdmin.sol @@ -21,7 +21,9 @@ contract XVSBridgeAdmin is AccessControlledV8 { */ mapping(bytes4 => string) public functionRegistry; - // Event emitted when function registry updated + /** + * @notice emitted when function registry updated + */ event FunctionRegistryChanged(string signature, bool active); /// @custom:oz-upgrades-unsafe-allow constructor @@ -33,15 +35,14 @@ contract XVSBridgeAdmin is AccessControlledV8 { /** * @param accessControlManager_ Address of access control manager contract. - * @custom:error ZeroAddressNotAllowed is thrown when accessControlManager contract address is zero. */ function initialize(address accessControlManager_) external initializer { - ensureNonzeroAddress(accessControlManager_); __AccessControlled_init(accessControlManager_); } /** - * @notice Invoked when called function does not exist in the contract + * @notice Invoked when called function does not exist in the contract. + * @return Response of low level call. * @custom:access Controlled by AccessControlManager. */ fallback(bytes calldata data) external returns (bytes memory) { @@ -54,8 +55,10 @@ contract XVSBridgeAdmin is AccessControlledV8 { } /** + * @notice Sets trusted remote on particular chain. * @param remoteChainId_ Chain Id of the destination chain. * @param remoteAddress_ Address of the destination bridge. + * @custom:access Controlled by AccessControlManager. * @custom:error ZeroAddressNotAllowed is thrown when remoteAddress_ contract address is zero. */ function setTrustedRemoteAddress(uint16 remoteChainId_, bytes calldata remoteAddress_) external { @@ -66,14 +69,16 @@ contract XVSBridgeAdmin is AccessControlledV8 { } /** - * @notice A registry of functions that are allowed to be executed from proposals + * @notice A setter for the registry of functions that are allowed to be executed from proposals. * @param signatures_ Function signature to be added or removed. * @param active_ bool value, should be true to add function. + * @custom:access Only owner. + * @custom:event Emits FunctionRegistryChanged if bool value of function changes. */ function upsertSignature(string[] calldata signatures_, bool[] calldata active_) external onlyOwner { uint256 signatureLength = signatures_.length; require(signatureLength == active_.length, "Input arrays must have the same length"); - for (uint256 i; i < signatureLength; i++) { + for (uint256 i; i < signatureLength; ) { bytes4 sigHash = bytes4(keccak256(bytes(signatures_[i]))); bytes memory signature = bytes(functionRegistry[sigHash]); if (active_[i] && signature.length == 0) { @@ -83,25 +88,28 @@ contract XVSBridgeAdmin is AccessControlledV8 { delete functionRegistry[sigHash]; emit FunctionRegistryChanged(signatures_[i], false); } + unchecked { + ++i; + } } } /** - * @notice This function transfer the ownership of the bridge from this contract to new owner. + * @notice This function transfers the ownership of the bridge from this contract to new owner. * @param newOwner_ New owner of the XVS Bridge. * @custom:access Controlled by AccessControlManager. */ function transferBridgeOwnership(address newOwner_) external { _checkAccessAllowed("transferBridgeOwnership(address)"); - ensureNonzeroAddress(newOwner_); XVSBridge.transferOwnership(newOwner_); } /** - * @notice Returns bool = true if srcAddress_ is trustedRemote corresponds to chainId_. + * @notice Returns true if remote address is trustedRemote corresponds to chainId_. * @param remoteChainId_ Chain Id of the destination chain. * @param remoteAddress_ Address of the destination bridge. * @custom:error ZeroAddressNotAllowed is thrown when remoteAddress_ contract address is zero. + * @return Bool indicating whether the remote chain is trusted or not. */ function isTrustedRemote(uint16 remoteChainId_, bytes calldata remoteAddress_) external returns (bool) { require(remoteChainId_ != 0, "ChainId must not be zero"); @@ -116,11 +124,18 @@ contract XVSBridgeAdmin is AccessControlledV8 { /** * @dev Returns function name string associated with function signature. + * @param signature_ Four bytes of function signature. + * @return Function signature corresponding to its hash. */ function _getFunctionName(bytes4 signature_) internal view returns (string memory) { return functionRegistry[signature_]; } + /** + * @notice Converts given bytes into address. + * @param b Bytes to be converted into address. + * @return Converted address of given bytes. + */ function bytesToAddress(bytes calldata b) private pure returns (address) { return address(uint160(bytes20(b))); } diff --git a/contracts/Bridge/XVSProxyOFTDest.sol b/contracts/Bridge/XVSProxyOFTDest.sol index 16ec43b..5f1aa3e 100644 --- a/contracts/Bridge/XVSProxyOFTDest.sol +++ b/contracts/Bridge/XVSProxyOFTDest.sol @@ -14,7 +14,7 @@ import { BaseXVSProxyOFT } from "./BaseXVSProxyOFT.sol"; contract XVSProxyOFTDest is BaseXVSProxyOFT { /** - * @notice Emits when stored message dropped without successfull retrying. + * @notice Emits when stored message dropped without successful retrying. */ event DropFailedMessage(uint16 srcChainId, bytes indexed srcAddress, uint64 nonce); @@ -25,10 +25,13 @@ contract XVSProxyOFTDest is BaseXVSProxyOFT { address oracle_ ) BaseXVSProxyOFT(tokenAddress_, sharedDecimals_, lzEndpoint_, oracle_) {} - /** @notice Clear failed messages from the storage. + /** + * @notice Clear failed messages from the storage. * @param srcChainId_ Chain id of source * @param srcAddress_ Address of source followed by current bridge address * @param nonce_ Nonce_ of the transaction + * @custom:access Only owner + * @custom:event Emits DropFailedMessage on clearance of failed message. */ function dropFailedMessage(uint16 srcChainId_, bytes memory srcAddress_, uint64 nonce_) external onlyOwner { failedMessages[srcChainId_][srcAddress_][nonce_] = bytes32(0); @@ -37,11 +40,19 @@ contract XVSProxyOFTDest is BaseXVSProxyOFT { /** * @notice Returns the total circulating supply of the token on the destination chain i.e (total supply). + * @return total circulating supply of the token on the destination chain. */ function circulatingSupply() public view override returns (uint256) { return innerToken.totalSupply(); } + /** + * @notice Debit tokens from the given address + * @param from_ Address from which tokens to be debited + * @param dstChainId_ Destination chain id + * @param amount_ Amount of tokens to be debited + * @return Actual amount debited + */ function _debitFrom( address from_, uint16 dstChainId_, @@ -54,6 +65,13 @@ contract XVSProxyOFTDest is BaseXVSProxyOFT { return amount_; } + /** + * @notice Credit tokens in the given account + * @param srcChainId_ Source chain id + * @param toAddress_ Address on which token will be credited + * @param amount_ Amount of tokens to be credited + * @return Actual amount credited + */ function _creditTo( uint16 srcChainId_, address toAddress_, diff --git a/contracts/Bridge/XVSProxyOFTSrc.sol b/contracts/Bridge/XVSProxyOFTSrc.sol index eef4e88..04aadd9 100644 --- a/contracts/Bridge/XVSProxyOFTSrc.sol +++ b/contracts/Bridge/XVSProxyOFTSrc.sol @@ -16,7 +16,7 @@ import { BaseXVSProxyOFT } from "./BaseXVSProxyOFT.sol"; contract XVSProxyOFTSrc is BaseXVSProxyOFT { using SafeERC20 for IERC20; /** - * @notice total amount is transferred from this chain to other chains. + * @notice Total amount that is transferred from this chain to other chains. */ uint256 public outboundAmount; @@ -25,7 +25,7 @@ contract XVSProxyOFTSrc is BaseXVSProxyOFT { */ event FallbackWithdraw(address indexed to, uint256 amount); /** - * @notice Emits when stored message dropped without successfull retrying. + * @notice Emits when stored message dropped without successful retrying. */ event DropFailedMessage(uint16 srcChainId, bytes indexed srcAddress, uint64 nonce); @@ -36,21 +36,30 @@ contract XVSProxyOFTSrc is BaseXVSProxyOFT { address oracle_ ) BaseXVSProxyOFT(tokenAddress_, sharedDecimals_, lzEndpoint_, oracle_) {} - /** @notice Only call it when there is no way to recover the failed message. + /** + * @notice Only call it when there is no way to recover the failed message. * `dropFailedMessage` must be called first if transaction is from remote->local chain to avoid double spending. * @param to_ The address to withdraw to * @param amount_ The amount of withdrawal + * @custom:access Only owner. + * @custom:event Emits FallbackWithdraw, once done with transfer. */ function fallbackWithdraw(address to_, uint256 amount_) external onlyOwner { - outboundAmount -= amount_; + require(outboundAmount >= amount_, "Withdraw amount should be less than outbound amount"); + unchecked { + outboundAmount -= amount_; + } _transferFrom(address(this), to_, amount_); emit FallbackWithdraw(to_, amount_); } - /** @notice Clear failed messages from the storage. + /** + * @notice Clear failed messages from the storage. * @param srcChainId_ Chain id of source * @param srcAddress_ Address of source followed by current bridge address * @param nonce_ Nonce_ of the transaction + * @custom:access Only owner. + * @custom:event Emits DropFailedMessage on clearance of failed message. */ function dropFailedMessage(uint16 srcChainId_, bytes memory srcAddress_, uint64 nonce_) external onlyOwner { failedMessages[srcChainId_][srcAddress_][nonce_] = bytes32(0); @@ -59,11 +68,19 @@ contract XVSProxyOFTSrc is BaseXVSProxyOFT { /** * @notice Returns the total circulating supply of the token on the source chain i.e (total supply - locked in this contract). + * @return Returns difference in total supply and the outbound amount. */ function circulatingSupply() public view override returns (uint256) { return innerToken.totalSupply() - outboundAmount; } + /** + * @notice Debit tokens from the given address + * @param from_ Address from which tokens to be debited + * @param dstChainId_ Destination chain id + * @param amount_ Amount of tokens to be debited + * @return Actual amount debited + */ function _debitFrom( address from_, uint16 dstChainId_, @@ -78,6 +95,13 @@ contract XVSProxyOFTSrc is BaseXVSProxyOFT { return amount; } + /** + * @notice Credit tokens in the given account + * @param srcChainId_ Source chain id + * @param toAddress_ Address on which token will be credited + * @param amount_ Amount of tokens to be credited + * @return Actual amount credited + */ function _creditTo( uint16 srcChainId_, address toAddress_, diff --git a/contracts/Bridge/token/TokenController.sol b/contracts/Bridge/token/TokenController.sol index a5dbe50..761e2bd 100644 --- a/contracts/Bridge/token/TokenController.sol +++ b/contracts/Bridge/token/TokenController.sol @@ -21,13 +21,13 @@ contract TokenController is Ownable, Pausable { /** * @notice A Mapping used to keep track of the blacklist status of addresses. */ - mapping(address => bool) public _blacklist; + mapping(address => bool) internal _blacklist; /** * @notice A mapping is used to keep track of the maximum amount a minter is permitted to mint. */ mapping(address => uint256) public minterToCap; /** - * @notice A Mapping used to keep track of the amount i.e already m inted by minter. + * @notice A Mapping used to keep track of the amount i.e already minted by minter. */ mapping(address => uint256) public minterToMintedAmount; @@ -57,13 +57,17 @@ contract TokenController is Ownable, Pausable { */ error MintLimitExceed(); /** - * @notice This error is used to indicate that minting is not allowed for the specified addresses. + * @notice This error is used to indicate that `mint` `burn` and `transfer` actions are not allowed for the user address. */ - error MintNotAllowed(address from, address to); + error AccountBlacklisted(address user); /** * @notice This error is used to indicate that sender is not allowed to perform this action. */ error Unauthorized(); + /** + * @notice This error is used to indicate that the new cap is greater than the previously minted tokens for the minter. + */ + error NewCapNotGreaterThanMintedTokens(); /** * @param accessControlManager_ Address of access control manager contract. @@ -106,7 +110,7 @@ contract TokenController is Ownable, Pausable { } /** - * @notice Sets the minitng cap for minter. + * @notice Sets the minting cap for minter. * @param minter_ Minter address. * @param amount_ Cap for the minter. * @custom:access Controlled by AccessControlManager. @@ -114,6 +118,11 @@ contract TokenController is Ownable, Pausable { */ function setMintCap(address minter_, uint256 amount_) external { _ensureAllowed("setMintCap(address,uint256)"); + + if (amount_ < minterToMintedAmount[minter_]) { + revert NewCapNotGreaterThanMintedTokens(); + } + minterToCap[minter_] = amount_; emit MintCapChanged(minter_, amount_); } @@ -146,11 +155,10 @@ contract TokenController is Ownable, Pausable { * @param from_ Minter address. * @param to_ Receiver address. * @param amount_ Amount to be mint. + * @custom:error MintLimitExceed is thrown when minting limit exceeds the cap. + * @custom:event Emits MintLimitDecreased with minter address and available limits. */ function _isEligibleToMint(address from_, address to_, uint256 amount_) internal { - if (_blacklist[to_]) { - revert MintNotAllowed(from_, to_); - } uint256 mintingCap = minterToCap[from_]; uint256 totalMintedOld = minterToMintedAmount[from_]; uint256 totalMintedNew = totalMintedOld + amount_; @@ -159,14 +167,18 @@ contract TokenController is Ownable, Pausable { revert MintLimitExceed(); } minterToMintedAmount[from_] = totalMintedNew; - uint256 availableLimit = mintingCap - totalMintedNew; + uint256 availableLimit; + unchecked { + availableLimit = mintingCap - totalMintedNew; + } emit MintLimitDecreased(from_, availableLimit); } /** - * @dev This is post hook of burn function, increases minitng limit of the minter. + * @dev This is post hook of burn function, increases minting limit of the minter. * @param from_ Minter address. * @param amount_ Amount burned. + * @custom:event Emits MintLimitIncreased with minter address and availabe limit. */ function _increaseMintLimit(address from_, uint256 amount_) internal { uint256 totalMintedOld = minterToMintedAmount[from_]; @@ -176,7 +188,11 @@ contract TokenController is Ownable, Pausable { emit MintLimitIncreased(from_, availableLimit); } - /// @dev Checks the caller is allowed to call the specified fuction + /** + * @dev Checks the caller is allowed to call the specified fuction. + * @param functionSig_ Function signatureon which access is to be checked. + * @custom:error Unauthorized, thrown when unauthorised user try to access function. + */ function _ensureAllowed(string memory functionSig_) internal view { if (!IAccessControlManagerV8(accessControlManager).isAllowedToCall(msg.sender, functionSig_)) { revert Unauthorized(); diff --git a/contracts/Bridge/token/XVS.sol b/contracts/Bridge/token/XVS.sol index cb5c932..b1a64ff 100644 --- a/contracts/Bridge/token/XVS.sol +++ b/contracts/Bridge/token/XVS.sol @@ -18,12 +18,11 @@ contract XVS is ERC20, TokenController { /** * @notice Creates `amount_` tokens and assigns them to `account_`, increasing * the total supply. Checks access and eligibility. - * @param account_ Address to which tokens be assigned. + * @param account_ Address to which tokens are assigned. * @param amount_ Amount of tokens to be assigned. * @custom:access Controlled by AccessControlManager. * @custom:event Emits MintLimitDecreased with new available limit. - * @custom:error MintNotAllowed is thrown when minting is not allowed to from_ address. - * @custom:error MintLimitExceed is thrown when minting amount exceed the maximum cap. + * @custom:error MintLimitExceed is thrown when minting amount exceeds the maximum cap. */ function mint(address account_, uint256 amount_) external whenNotPaused { _ensureAllowed("mint(address,uint256)"); @@ -44,4 +43,21 @@ contract XVS is ERC20, TokenController { _burn(account_, amount_); _increaseMintLimit(msg.sender, amount_); } + + /** + * @notice Hook that is called before any transfer of tokens. This includes + * minting and burning. + * @param from_ Address of account from which tokens are to be transferred. + * @param to_ Address of the account to which tokens are to be transferred. + * @param amount_ The amount of tokens to be transferred. + * @custom:error AccountBlacklisted is thrown when either `from` or `to` address is blacklisted. + */ + function _beforeTokenTransfer(address from_, address to_, uint256 amount_) internal override whenNotPaused { + if (_blacklist[to_]) { + revert AccountBlacklisted(to_); + } + if (_blacklist[from_]) { + revert AccountBlacklisted(from_); + } + } } diff --git a/deployments/opbnbmainnet.json b/deployments/opbnbmainnet.json new file mode 100644 index 0000000..6a503c8 --- /dev/null +++ b/deployments/opbnbmainnet.json @@ -0,0 +1,5 @@ +{ + "name": "opbnbmainnet", + "chainId": "204", + "contracts": {} +} diff --git a/deployments/opbnbmainnet_addresses.json b/deployments/opbnbmainnet_addresses.json new file mode 100644 index 0000000..cf3419c --- /dev/null +++ b/deployments/opbnbmainnet_addresses.json @@ -0,0 +1,5 @@ +{ + "name": "opbnbmainnet", + "chainId": "204", + "addresses": {} +} diff --git a/deployments/opbnbtestnet_addresses.json b/deployments/opbnbtestnet_addresses.json new file mode 100644 index 0000000..7c62449 --- /dev/null +++ b/deployments/opbnbtestnet_addresses.json @@ -0,0 +1,7 @@ +{ + "name": "opbnbtestnet", + "chainId": "5611", + "addresses": { + "XVS": "0x3d0e20D4caD958bc848B045e1da19Fe378f86f03" + } +} diff --git a/helpers/deploymentConfig.ts b/helpers/deploymentConfig.ts index 4486900..d91df19 100644 --- a/helpers/deploymentConfig.ts +++ b/helpers/deploymentConfig.ts @@ -52,6 +52,7 @@ export const xvsBridgeMethods = [ "setPayloadSizeLimit(uint16,uint256)", "setWhitelist(address,bool)", "setConfig(uint16,uint16,uint256,bytes)", + "sweepToken(address,address,uint256)", "updateSendAndCallEnabled(bool)", ]; diff --git a/package.json b/package.json index 74ab04c..a5a3e77 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@venusprotocol/token-bridge", - "version": "1.0.0-dev.4", + "version": "1.0.0-dev.7", "description": "Contracts to bridge tokens using LayerZero technology and applying some rules on top of it", "files": [ "artifacts", diff --git a/test/bridgeAdmin.ts b/test/bridgeAdmin.ts index e006566..47c5c73 100644 --- a/test/bridgeAdmin.ts +++ b/test/bridgeAdmin.ts @@ -219,7 +219,7 @@ describe("Bridge Admin: ", function () { ).to.be.revertedWith("Function not found"); }); - it("Success on trnafer bridge owner", async function () { + it("Success on transfer bridge owner", async function () { await bridgeAdmin.connect(acc2).transferBridgeOwnership(acc2.address); expect(await remoteOFT.owner()).equals(acc2.address); }); diff --git a/test/proxyOFT.ts b/test/proxyOFT.ts index cd46d1e..a91b777 100644 --- a/test/proxyOFT.ts +++ b/test/proxyOFT.ts @@ -509,6 +509,62 @@ describe("Proxy OFTV2: ", function () { accessControlManager.isAllowedToCall.returns(true); }); + it("Reverts transfer of remote token to blacklist address", async function () { + const amount = ethers.utils.parseEther("10", 18); + await localToken.connect(acc2).faucet(amount); + await localToken.connect(acc2).approve(localOFT.address, amount); + + const acc3AddressBytes32 = ethers.utils.defaultAbiCoder.encode(["address"], [acc3.address]); + const nativeFee = ( + await localOFT.estimateSendFee(remoteChainId, acc3AddressBytes32, amount, false, defaultAdapterParams) + ).nativeFee; + + await expect( + localOFT + .connect(acc2) + .sendFrom( + acc2.address, + remoteChainId, + acc3AddressBytes32, + amount, + [acc2.address, ethers.constants.AddressZero, defaultAdapterParams], + { value: nativeFee }, + ), + ).to.emit(remoteOFT, "ReceiveFromChain"); + await remoteToken.updateBlacklist(acc3.address, true); + await expect(remoteToken.connect(acc2).transfer(acc3.address, amount)).to.be.revertedWithCustomError( + remoteToken, + "AccountBlacklisted", + ); + }); + + it("Reverts if try to set cap less than already minted tokens", async function () { + const initialAmount = ethers.utils.parseEther("20", 18); + await localToken.connect(acc2).faucet(initialAmount); + await localToken.connect(acc2).approve(localOFT.address, initialAmount); + const amount = ethers.utils.parseEther("10", 18); + const acc3AddressBytes32 = ethers.utils.defaultAbiCoder.encode(["address"], [acc3.address]); + const nativeFee = ( + await localOFT.estimateSendFee(remoteChainId, acc3AddressBytes32, amount, false, defaultAdapterParams) + ).nativeFee; + + await localOFT + .connect(acc2) + .sendFrom( + acc2.address, + remoteChainId, + acc3AddressBytes32, + amount, + [acc2.address, ethers.constants.AddressZero, defaultAdapterParams], + { value: nativeFee }, + ), + // Msg should reach remote chain + expect(await remoteEndpoint.inboundNonce(localChainId, localPath)).equals(1); + await expect( + remoteToken.connect(acc1).setMintCap(remoteOFT.address, convertToUnit(1, 18)), + ).to.be.revertedWithCustomError(remoteToken, "NewCapNotGreaterThanMintedTokens"); + }); + it("Reverts on remote chain if minting cap is reached", async function () { await remoteToken.connect(acc1).setMintCap(remoteOFT.address, convertToUnit(10, 18)); expect(await remoteEndpoint.inboundNonce(localChainId, localPath)).lessThanOrEqual(0); @@ -719,6 +775,86 @@ describe("Proxy OFTV2: ", function () { }); expect(await localOFT.trustedRemoteLookup(remoteChainId)).equals("0x"); }); + it("Returns correct limits and eligibility of user initially", async function () { + const amount = ethers.utils.parseEther("10", 18); + const { + eligibleToSend, + maxSingleTransactionLimit, + maxDailyLimit, + amountInUsd, + transferredInWindow, + last24HourWindowStart, + isWhiteListedUser, + } = await localOFT.connect(acc1).isEligibleToSend(acc2.address, remoteChainId, amount); + expect(eligibleToSend).to.be.true; + expect(await localOFT.chainIdToMaxSingleTransactionLimit(remoteChainId)).to.be.equals(maxSingleTransactionLimit); + expect(await localOFT.chainIdToMaxDailyLimit(remoteChainId)).to.be.equals(maxDailyLimit); + const oraclePrice = await oracle.getPrice(await localOFT.token()); + const expectedAmount = BigInt((oraclePrice * amount) / 1e18); + expect(expectedAmount).to.be.equals(amountInUsd); + expect((await localOFT.chainIdToLast24HourTransferred(remoteChainId)).add(amountInUsd)).to.be.equals( + transferredInWindow, + ); + const currentTimestamp = (await ethers.provider.getBlock("latest")).timestamp; + expect((await localOFT.chainIdToLast24HourWindowStart(remoteChainId)).add(currentTimestamp)).to.be.equals( + last24HourWindowStart, + ); + expect(await localOFT.whitelist(acc2.address)).to.be.equals(isWhiteListedUser); + }); + + it("Returns upadted value of limits and eligibility of user", async function () { + const data = localOFT.interface.encodeFunctionData("setMaxSingleTransactionLimit", [ + remoteChainId, + singleTransactionLimit, + ]); + await acc1.sendTransaction({ + to: bridgeAdminLocal.address, + data: data, + }); + + const initialAmount = ethers.utils.parseEther("1", 18); + await localToken.connect(acc2).faucet(initialAmount); + expect(await localToken.balanceOf(acc2.address)).to.be.equal(initialAmount); + expect(await remoteToken.balanceOf(acc3.address)).to.be.equal(0); + await localToken.connect(acc2).approve(localOFT.address, initialAmount); + const acc3AddressBytes32 = ethers.utils.defaultAbiCoder.encode(["address"], [acc3.address]); + const nativeFee = ( + await localOFT.estimateSendFee(remoteChainId, acc3AddressBytes32, initialAmount, false, defaultAdapterParams) + ).nativeFee; + + await localOFT + .connect(acc2) + .sendFrom( + acc2.address, + remoteChainId, + acc3AddressBytes32, + initialAmount, + [acc2.address, ethers.constants.AddressZero, defaultAdapterParams], + { value: nativeFee }, + ); + + const { + eligibleToSend, + maxSingleTransactionLimit, + maxDailyLimit, + amountInUsd, + transferredInWindow, + last24HourWindowStart, + isWhiteListedUser, + } = await localOFT.connect(acc1).isEligibleToSend(acc2.address, remoteChainId, initialAmount); + + expect(eligibleToSend).to.be.true; + expect(await localOFT.chainIdToMaxSingleTransactionLimit(remoteChainId)).to.be.equals(maxSingleTransactionLimit); + expect(await localOFT.chainIdToMaxDailyLimit(remoteChainId)).to.be.equals(maxDailyLimit); + const oraclePrice = await oracle.getPrice(await localOFT.token()); + const expectedAmount = BigInt((oraclePrice * initialAmount) / 1e18); + expect(expectedAmount).to.be.equals(amountInUsd); + expect((await localOFT.chainIdToLast24HourTransferred(remoteChainId)).add(amountInUsd)).to.be.equals( + transferredInWindow, + ); + expect(await localOFT.chainIdToLast24HourWindowStart(remoteChainId)).to.be.equals(last24HourWindowStart); + expect(await localOFT.whitelist(acc2.address)).to.be.equals(isWhiteListedUser); + }); it("Reverts when sendAndCall is disabled", async function () { const amount = ethers.utils.parseEther("2", 18); const dstGasForCall_ = 0;