diff --git a/contracts/token/oft/v2/NativeOFTV2.sol b/contracts/token/oft/v2/NativeOFTV2.sol index 4db4c04d..9e84fab3 100644 --- a/contracts/token/oft/v2/NativeOFTV2.sol +++ b/contracts/token/oft/v2/NativeOFTV2.sol @@ -6,6 +6,8 @@ import "@openzeppelin/contracts/security/ReentrancyGuard.sol"; import "./OFTV2.sol"; contract NativeOFTV2 is OFTV2, ReentrancyGuard { + uint public outboundAmount; + event Deposit(address indexed _dst, uint _amount); event Withdrawal(address indexed _src, uint _amount); @@ -110,6 +112,7 @@ contract NativeOFTV2 is OFTV2, ReentrancyGuard { } function _debitFromNative(address _from, uint _amount) internal returns (uint messageFee) { + outboundAmount += _amount; messageFee = msg.sender == _from ? _debitMsgSender(_amount) : _debitMsgFrom(_from, _amount); } @@ -165,6 +168,7 @@ contract NativeOFTV2 is OFTV2, ReentrancyGuard { address _toAddress, uint _amount ) internal override returns (uint) { + outboundAmount -= _amount; _burn(address(this), _amount); (bool success, ) = _toAddress.call{value: _amount}(""); require(success, "NativeOFTV2: failed to _creditTo"); diff --git a/contracts/token/oft/v2/fee/NativeOFTWithFee.sol b/contracts/token/oft/v2/fee/NativeOFTWithFee.sol index 0f5e7e8a..a0970075 100644 --- a/contracts/token/oft/v2/fee/NativeOFTWithFee.sol +++ b/contracts/token/oft/v2/fee/NativeOFTWithFee.sol @@ -6,6 +6,7 @@ import "@openzeppelin/contracts/security/ReentrancyGuard.sol"; import "./OFTWithFee.sol"; contract NativeOFTWithFee is OFTWithFee, ReentrancyGuard { + uint public outboundAmount; event Deposit(address indexed _dst, uint _amount); event Withdrawal(address indexed _src, uint _amount); @@ -25,12 +26,24 @@ contract NativeOFTWithFee is OFTWithFee, ReentrancyGuard { emit Withdrawal(msg.sender, _amount); } + /************************************************************************ + * public functions + ************************************************************************/ + function sendFrom(address _from, uint16 _dstChainId, bytes32 _toAddress, uint _amount, uint _minAmount, LzCallParams calldata _callParams) public payable virtual override { + _amount = _send(_from, _dstChainId, _toAddress, _amount, _callParams.refundAddress, _callParams.zroPaymentAddress, _callParams.adapterParams); + require(_amount >= _minAmount, "NativeOFTWithFee: amount is less than minAmount"); + } + + function sendAndCall(address _from, uint16 _dstChainId, bytes32 _toAddress, uint _amount, uint _minAmount, bytes calldata _payload, uint64 _dstGasForCall, LzCallParams calldata _callParams) public payable virtual override { + _amount = _sendAndCall(_from, _dstChainId, _toAddress, _amount, _payload, _dstGasForCall, _callParams.refundAddress, _callParams.zroPaymentAddress, _callParams.adapterParams); + require(_amount >= _minAmount, "NativeOFTWithFee: amount is less than minAmount"); + } + function _send(address _from, uint16 _dstChainId, bytes32 _toAddress, uint _amount, address payable _refundAddress, address _zroPaymentAddress, bytes memory _adapterParams) internal virtual override returns (uint amount) { _checkGasLimit(_dstChainId, PT_SEND, _adapterParams, NO_EXTRA_GAS); - (amount,) = _removeDust(_amount); - require(amount > 0, "NativeOFTWithFee: amount too small"); - uint messageFee = _debitFromNative(_from, amount); + uint messageFee; + (messageFee, amount) = _debitFromNative(_from, _amount, _dstChainId); bytes memory lzPayload = _encodeSendPayload(_toAddress, _ld2sd(amount)); _lzSend(_dstChainId, lzPayload, _refundAddress, _zroPaymentAddress, _adapterParams, messageFee); @@ -41,9 +54,8 @@ contract NativeOFTWithFee is OFTWithFee, ReentrancyGuard { function _sendAndCall(address _from, uint16 _dstChainId, bytes32 _toAddress, uint _amount, bytes memory _payload, uint64 _dstGasForCall, address payable _refundAddress, address _zroPaymentAddress, bytes memory _adapterParams) internal virtual override returns (uint amount) { _checkGasLimit(_dstChainId, PT_SEND_AND_CALL, _adapterParams, _dstGasForCall); - (amount,) = _removeDust(_amount); - require(amount > 0, "NativeOFTWithFee: amount too small"); - uint messageFee = _debitFromNative(_from, amount); + uint messageFee; + (messageFee, amount) = _debitFromNative(_from, _amount, _dstChainId); // encode the msg.sender into the payload instead of _from bytes memory lzPayload = _encodeSendAndCallPayload(msg.sender, _toAddress, _ld2sd(amount), _payload, _dstGasForCall); @@ -52,35 +64,55 @@ contract NativeOFTWithFee is OFTWithFee, ReentrancyGuard { emit SendToChain(_dstChainId, _from, _toAddress, amount); } - function _debitFromNative(address _from, uint _amount) internal returns (uint messageFee) { - messageFee = msg.sender == _from ? _debitMsgSender(_amount) : _debitMsgFrom(_from, _amount); + function _debitFromNative(address _from, uint _amount, uint16 _dstChainId) internal returns (uint messageFee, uint amount) { + uint fee = quoteOFTFee(_dstChainId, _amount); + uint newMsgValue = msg.value; + + if(fee > 0) { + // subtract fee from _amount + _amount -= fee; + + // pay fee and update newMsgValue + if(balanceOf(_from) >= fee) { + _transferFrom(_from, feeOwner, fee); + } else { + _mint(feeOwner, fee); + newMsgValue -= fee; + } + } + + (amount,) = _removeDust(_amount); + require(amount > 0, "NativeOFTWithFee: amount too small"); + outboundAmount += amount; + messageFee = msg.sender == _from ? _debitMsgSender(amount, newMsgValue) : _debitMsgFrom(_from, amount, newMsgValue); } - function _debitMsgSender(uint _amount) internal returns (uint messageFee) { + function _debitMsgSender(uint _amount, uint currentMsgValue) internal returns (uint messageFee) { uint msgSenderBalance = balanceOf(msg.sender); if (msgSenderBalance < _amount) { - require(msgSenderBalance + msg.value >= _amount, "NativeOFTWithFee: Insufficient msg.value"); + require(msgSenderBalance + currentMsgValue >= _amount, "NativeOFTWithFee: Insufficient msg.value"); // user can cover difference with additional msg.value ie. wrapping uint mintAmount = _amount - msgSenderBalance; + _mint(address(msg.sender), mintAmount); // update the messageFee to take out mintAmount - messageFee = msg.value - mintAmount; + messageFee = currentMsgValue - mintAmount; } else { - messageFee = msg.value; + messageFee = currentMsgValue; } _transfer(msg.sender, address(this), _amount); return messageFee; } - function _debitMsgFrom(address _from, uint _amount) internal returns (uint messageFee) { + function _debitMsgFrom(address _from, uint _amount, uint currentMsgValue) internal returns (uint messageFee) { uint msgFromBalance = balanceOf(_from); if (msgFromBalance < _amount) { - require(msgFromBalance + msg.value >= _amount, "NativeOFTWithFee: Insufficient msg.value"); + require(msgFromBalance + currentMsgValue >= _amount, "NativeOFTWithFee: Insufficient msg.value"); // user can cover difference with additional msg.value ie. wrapping uint mintAmount = _amount - msgFromBalance; @@ -93,9 +125,9 @@ contract NativeOFTWithFee is OFTWithFee, ReentrancyGuard { _amount = msgFromBalance; // update the messageFee to take out mintAmount - messageFee = msg.value - mintAmount; + messageFee = currentMsgValue - mintAmount; } else { - messageFee = msg.value; + messageFee = currentMsgValue; } _spendAllowance(_from, msg.sender, _amount); @@ -104,6 +136,7 @@ contract NativeOFTWithFee is OFTWithFee, ReentrancyGuard { } function _creditTo(uint16, address _toAddress, uint _amount) internal override returns(uint) { + outboundAmount -= _amount; _burn(address(this), _amount); (bool success, ) = _toAddress.call{value: _amount}(""); require(success, "NativeOFTWithFee: failed to _creditTo"); diff --git a/test/oft/v2/NativeOFTV2.test.js b/test/oft/v2/NativeOFTV2.test.js index 253f5a20..b991a313 100644 --- a/test/oft/v2/NativeOFTV2.test.js +++ b/test/oft/v2/NativeOFTV2.test.js @@ -86,6 +86,9 @@ describe("NativeOFTV2: ", function () { expect(await nativeOFTV2.balanceOf(nativeOFTV2.address)).to.be.equal(totalAmount) expect(await nativeOFTV2.balanceOf(owner.address)).to.be.equal(leftOverAmount) expect(await remoteOFTV2.balanceOf(owner.address)).to.be.equal(totalAmount) + expect(await nativeOFTV2.outboundAmount()).to.be.equal(totalAmount) + expect(await remoteOFTV2.totalSupply()).to.be.equal(totalAmount) + let ownerBalance2 = await ethers.provider.getBalance(owner.address) @@ -106,6 +109,8 @@ describe("NativeOFTV2: ", function () { expect(await ethers.provider.getBalance(owner.address)).to.be.equal(ownerBalance2.sub(nativeFee).sub(transFee)) expect(await nativeOFTV2.balanceOf(owner.address)).to.equal(leftOverAmount) expect(await remoteOFTV2.balanceOf(owner.address)).to.equal(0) + expect(await remoteOFTV2.totalSupply()).to.be.equal(leftOverAmount) + expect(await nativeOFTV2.outboundAmount()).to.be.equal(leftOverAmount) }) it("sendFrom() - with enough native", async function () { @@ -142,6 +147,8 @@ describe("NativeOFTV2: ", function () { expect(await nativeOFTV2.balanceOf(nativeOFTV2.address)).to.be.equal(totalAmountMinusDust) expect(await nativeOFTV2.balanceOf(owner.address)).to.be.equal(leftOverAmount) expect(await remoteOFTV2.balanceOf(owner.address)).to.be.equal(totalAmountMinusDust) + expect(await nativeOFTV2.outboundAmount()).to.be.equal(totalAmountMinusDust) + expect(await remoteOFTV2.totalSupply()).to.be.equal(totalAmountMinusDust) }) it("sendFrom() - from != sender with addition msg.value", async function () { @@ -180,6 +187,8 @@ describe("NativeOFTV2: ", function () { expect(await nativeOFTV2.balanceOf(nativeOFTV2.address)).to.be.equal(totalAmount) expect(await nativeOFTV2.balanceOf(owner.address)).to.be.equal(leftOverAmount) expect(await remoteOFTV2.balanceOf(owner.address)).to.be.equal(totalAmount) + expect(await nativeOFTV2.outboundAmount()).to.be.equal(totalAmount) + expect(await remoteOFTV2.totalSupply()).to.be.equal(totalAmount) }) it("sendFrom() - from != sender with not enough native", async function () { @@ -286,6 +295,8 @@ describe("NativeOFTV2: ", function () { // verify tokens burned on source chain and minted on destination chain expect(await nativeOFTV2.balanceOf(nativeOFTV2.address)).to.be.equal(amount) expect(await remoteOFTV2.balanceOf(owner.address)).to.be.equal(amount) + expect(await nativeOFTV2.outboundAmount()).to.be.equal(amount) + expect(await remoteOFTV2.totalSupply()).to.be.equal(amount) }) it("setMinDstGas() - when type is not set on destination chain", async function () { diff --git a/test/oft/v2/NativeOFTWithFee.test.js b/test/oft/v2/NativeOFTWithFee.test.js index 5a7b7fbd..58b45cce 100644 --- a/test/oft/v2/NativeOFTWithFee.test.js +++ b/test/oft/v2/NativeOFTWithFee.test.js @@ -97,6 +97,8 @@ describe("NativeOFTWithFee: ", function () { expect(await nativeOFTWithFee.balanceOf(nativeOFTWithFee.address)).to.be.equal(totalAmount) expect(await nativeOFTWithFee.balanceOf(owner.address)).to.be.equal(leftOverAmount) expect(await remoteOFTWithFee.balanceOf(owner.address)).to.be.equal(totalAmount) + expect(await nativeOFTWithFee.outboundAmount()).to.be.equal(totalAmount) + expect(await remoteOFTWithFee.totalSupply()).to.be.equal(totalAmount) let ownerBalance2 = await ethers.provider.getBalance(owner.address) @@ -119,11 +121,104 @@ describe("NativeOFTWithFee: ", function () { expect(await ethers.provider.getBalance(owner.address)).to.be.equal(ownerBalance2.sub(nativeFee).sub(transFee)) expect(await nativeOFTWithFee.balanceOf(owner.address)).to.equal(leftOverAmount) expect(await remoteOFTWithFee.balanceOf(owner.address)).to.equal(0) + expect(await nativeOFTWithFee.outboundAmount()).to.be.equal(leftOverAmount) + expect(await remoteOFTWithFee.totalSupply()).to.be.equal(leftOverAmount) }) it("sendFrom() w/ fee change - tokens from main to other chain", async function () { expect(await ethers.provider.getBalance(localEndpoint.address)).to.be.equal(ethers.utils.parseEther("0")) + // set default fee to 0.01% + await nativeOFTWithFee.setDefaultFeeBp(1) + await nativeOFTWithFee.setFeeOwner(bob.address) + + // ensure they're both allocated initial amounts + expect(await nativeOFTWithFee.balanceOf(owner.address)).to.equal(0) + expect(await remoteOFTWithFee.balanceOf(owner.address)).to.equal(0) + expect(await ethers.provider.getBalance(nativeOFTWithFee.address)).to.equal(0) + + let leftOverAmount = ethers.utils.parseEther("0") + let totalAmount = ethers.utils.parseEther("8") + + expect(await ethers.provider.getBalance(localEndpoint.address)).to.be.equal(0) + expect(await nativeOFTWithFee.balanceOf(nativeOFTWithFee.address)).to.be.equal(0) + expect(await nativeOFTWithFee.balanceOf(owner.address)).to.be.equal(0) + expect(await remoteOFTWithFee.balanceOf(owner.address)).to.be.equal(0) + + const aliceAddressBytes32 = ethers.utils.defaultAbiCoder.encode(["address"], [alice.address]) + // estimate nativeFees + let fee = await nativeOFTWithFee.quoteOFTFee(remoteChainId, totalAmount) + let nativeFee = (await nativeOFTWithFee.estimateSendFee(remoteChainId, aliceAddressBytes32, totalAmount, false, defaultAdapterParams)) + .nativeFee + await nativeOFTWithFee.sendFrom( + owner.address, + remoteChainId, // destination chainId + aliceAddressBytes32, // destination address to send tokens to + totalAmount, // quantity of tokens to send (in units of wei) + totalAmount.sub(fee), // quantity of tokens to send (in units of wei) + [owner.address, ethers.constants.AddressZero, defaultAdapterParams], + { value: nativeFee.add(totalAmount) } // pass a msg.value to pay the LayerZero message fee + ) + expect(await ethers.provider.getBalance(localEndpoint.address)).to.be.equal(nativeFee) // collects + expect(await nativeOFTWithFee.balanceOf(owner.address)).to.be.equal(leftOverAmount) + expect(await nativeOFTWithFee.balanceOf(alice.address)).to.be.equal(leftOverAmount) + expect(await nativeOFTWithFee.balanceOf(bob.address)).to.be.equal(fee) + expect(await nativeOFTWithFee.balanceOf(nativeOFTWithFee.address)).to.be.equal(totalAmount.sub(fee)) + expect(await nativeOFTWithFee.outboundAmount()).to.be.equal(totalAmount.sub(fee)) + expect(await remoteOFTWithFee.totalSupply()).to.be.equal(totalAmount.sub(fee)) + }) + + it("sendFrom() w/ fee change - tokens from main to other chain without taking dust", async function () { + expect(await ethers.provider.getBalance(localEndpoint.address)).to.be.equal(ethers.utils.parseEther("0")) + + // set default fee to 50% + await nativeOFTWithFee.setDefaultFeeBp(1) + await nativeOFTWithFee.setFeeOwner(bob.address) + + // ensure they're both allocated initial amounts + expect(await nativeOFTWithFee.balanceOf(owner.address)).to.equal(0) + expect(await remoteOFTWithFee.balanceOf(owner.address)).to.equal(0) + expect(await ethers.provider.getBalance(nativeOFTWithFee.address)).to.equal(0) + + let leftOverAmount = ethers.utils.parseEther("0") + let totalAmount = ethers.utils.parseEther("8.123456789") + + expect(await ethers.provider.getBalance(localEndpoint.address)).to.be.equal(0) + expect(await nativeOFTWithFee.balanceOf(nativeOFTWithFee.address)).to.be.equal(0) + expect(await nativeOFTWithFee.balanceOf(owner.address)).to.be.equal(0) + expect(await remoteOFTWithFee.balanceOf(owner.address)).to.be.equal(0) + + const aliceAddressBytes32 = ethers.utils.defaultAbiCoder.encode(["address"], [alice.address]) + // estimate nativeFees + let fee = await nativeOFTWithFee.quoteOFTFee(remoteChainId, totalAmount) + let nativeFee = (await nativeOFTWithFee.estimateSendFee(remoteChainId, aliceAddressBytes32, totalAmount, false, defaultAdapterParams)) + .nativeFee + + let ld2sdRate = 10 ** (18 - sharedDecimals) + let dust = totalAmount.sub(fee).mod(ld2sdRate) + let totalMintAmount = (totalAmount.sub(fee)).sub(dust) + + await nativeOFTWithFee.sendFrom( + owner.address, + remoteChainId, // destination chainId + aliceAddressBytes32, // destination address to send tokens to + totalAmount, // quantity of tokens to send (in units of wei) + totalMintAmount, // quantity of tokens to send (in units of wei) + [owner.address, ethers.constants.AddressZero, defaultAdapterParams], + { value: nativeFee.add(totalAmount) } // pass a msg.value to pay the LayerZero message fee + ) + expect(await ethers.provider.getBalance(localEndpoint.address)).to.be.equal(nativeFee) // collects + expect(await nativeOFTWithFee.balanceOf(owner.address)).to.be.equal(leftOverAmount) + expect(await nativeOFTWithFee.balanceOf(bob.address)).to.be.equal(fee) + expect(await nativeOFTWithFee.balanceOf(nativeOFTWithFee.address)).to.be.equal(totalMintAmount) + expect(await remoteOFTWithFee.balanceOf(alice.address)).to.be.equal(totalMintAmount) + expect(await nativeOFTWithFee.outboundAmount()).to.be.equal(totalMintAmount) + expect(await remoteOFTWithFee.totalSupply()).to.be.equal(totalMintAmount) + }) + + it("sendFrom() w/ fee change - deposit before send", async function () { + expect(await ethers.provider.getBalance(localEndpoint.address)).to.be.equal(ethers.utils.parseEther("0")) + // set default fee to 50% await nativeOFTWithFee.setDefaultFeeBp(5000) await nativeOFTWithFee.setFeeOwner(bob.address) @@ -156,13 +251,15 @@ describe("NativeOFTWithFee: ", function () { [owner.address, ethers.constants.AddressZero, defaultAdapterParams], { value: nativeFee.add(totalAmount.sub(depositAmount)) } // pass a msg.value to pay the LayerZero message fee ) - ).to.be.revertedWith("BaseOFTWithFee: amount is less than minAmount") + ).to.be.revertedWith("NativeOFTWithFee: amount is less than minAmount") expect(await ethers.provider.getBalance(nativeOFTWithFee.address)).to.be.equal(depositAmount) expect(await ethers.provider.getBalance(localEndpoint.address)).to.be.equal(0) expect(await nativeOFTWithFee.balanceOf(nativeOFTWithFee.address)).to.be.equal(0) expect(await nativeOFTWithFee.balanceOf(owner.address)).to.be.equal(depositAmount) expect(await remoteOFTWithFee.balanceOf(owner.address)).to.be.equal(0) + expect(await nativeOFTWithFee.outboundAmount()).to.be.equal(leftOverAmount) + expect(await remoteOFTWithFee.totalSupply()).to.be.equal(leftOverAmount) const aliceAddressBytes32 = ethers.utils.defaultAbiCoder.encode(["address"], [alice.address]) // estimate nativeFees @@ -184,6 +281,8 @@ describe("NativeOFTWithFee: ", function () { expect(await nativeOFTWithFee.balanceOf(alice.address)).to.be.equal(leftOverAmount) expect(await nativeOFTWithFee.balanceOf(bob.address)).to.be.equal(fee) expect(await nativeOFTWithFee.balanceOf(nativeOFTWithFee.address)).to.be.equal(totalAmount.sub(fee)) + expect(await nativeOFTWithFee.outboundAmount()).to.be.equal(totalAmount.div(2)) + expect(await remoteOFTWithFee.totalSupply()).to.be.equal(totalAmount.div(2)) }) it("quote oft fee", async function () { @@ -246,6 +345,8 @@ describe("NativeOFTWithFee: ", function () { expect(await nativeOFTWithFee.balanceOf(nativeOFTWithFee.address)).to.be.equal(totalAmountMinusDust) expect(await nativeOFTWithFee.balanceOf(owner.address)).to.be.equal(leftOverAmount) expect(await remoteOFTWithFee.balanceOf(owner.address)).to.be.equal(totalAmountMinusDust) + expect(await nativeOFTWithFee.outboundAmount()).to.be.equal(totalAmountMinusDust) + expect(await remoteOFTWithFee.totalSupply()).to.be.equal(totalAmountMinusDust) }) it("sendFrom() - from != sender with addition msg.value", async function () { @@ -285,6 +386,8 @@ describe("NativeOFTWithFee: ", function () { expect(await nativeOFTWithFee.balanceOf(nativeOFTWithFee.address)).to.be.equal(totalAmount) expect(await nativeOFTWithFee.balanceOf(owner.address)).to.be.equal(leftOverAmount) expect(await remoteOFTWithFee.balanceOf(owner.address)).to.be.equal(totalAmount) + expect(await nativeOFTWithFee.outboundAmount()).to.be.equal(totalAmount) + expect(await remoteOFTWithFee.totalSupply()).to.be.equal(totalAmount) }) it("sendFrom() - from != sender with not enough native", async function () { @@ -395,6 +498,8 @@ describe("NativeOFTWithFee: ", function () { // verify tokens burned on source chain and minted on destination chain expect(await nativeOFTWithFee.balanceOf(nativeOFTWithFee.address)).to.be.equal(amount) expect(await remoteOFTWithFee.balanceOf(owner.address)).to.be.equal(amount) + expect(await nativeOFTWithFee.outboundAmount()).to.be.equal(amount) + expect(await remoteOFTWithFee.totalSupply()).to.be.equal(amount) }) it("setMinDstGas() - when type is not set on destination chain", async function () {