-
Notifications
You must be signed in to change notification settings - Fork 18
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: add native token handler and adapter (#254)
- Loading branch information
Showing
12 changed files
with
1,210 additions
and
4 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,62 @@ | ||
// SPDX-License-Identifier: BUSL-1.1 | ||
pragma solidity ^0.8.11; | ||
|
||
import "@openzeppelin/contracts/access/AccessControl.sol"; | ||
import "../../interfaces/IBridge.sol"; | ||
import "../../interfaces/IFeeHandler.sol"; | ||
|
||
|
||
contract NativeTokenAdapter is AccessControl { | ||
IBridge public immutable _bridge; | ||
bytes32 public immutable _resourceID; | ||
|
||
event Withdrawal(address recipient, uint amount); | ||
|
||
error SenderNotAdmin(); | ||
error InsufficientMsgValueAmount(uint256 amount); | ||
error MsgValueLowerThanFee(uint256 amount); | ||
error TokenWithdrawalFailed(); | ||
error InsufficientBalance(); | ||
error FailedFundsTransfer(); | ||
|
||
modifier onlyAdmin() { | ||
if (!hasRole(DEFAULT_ADMIN_ROLE, msg.sender)) revert SenderNotAdmin(); | ||
_; | ||
} | ||
|
||
constructor(address bridge, bytes32 resourceID) { | ||
_bridge = IBridge(bridge); | ||
_resourceID = resourceID; | ||
_setupRole(DEFAULT_ADMIN_ROLE, msg.sender); | ||
} | ||
|
||
function deposit(uint8 destinationDomainID, string calldata recipientAddress) external payable { | ||
if (msg.value <= 0) revert InsufficientMsgValueAmount(msg.value); | ||
address feeHandlerRouter = _bridge._feeHandler(); | ||
(uint256 fee, ) = IFeeHandler(feeHandlerRouter).calculateFee( | ||
address(this), | ||
_bridge._domainID(), | ||
destinationDomainID, | ||
_resourceID, | ||
"", // depositData - not parsed | ||
"" // feeData - not parsed | ||
); | ||
|
||
if (msg.value < fee) revert MsgValueLowerThanFee(msg.value); | ||
uint256 transferAmount = msg.value - fee; | ||
|
||
bytes memory depositData = abi.encodePacked( | ||
transferAmount, | ||
bytes(recipientAddress).length, | ||
recipientAddress | ||
); | ||
|
||
_bridge.deposit{value: fee}(destinationDomainID, _resourceID, depositData, ""); | ||
|
||
address nativeHandlerAddress = _bridge._resourceIDToHandlerAddress(_resourceID); | ||
(bool success, ) = nativeHandlerAddress.call{value: transferAmount}(""); | ||
if(!success) revert FailedFundsTransfer(); | ||
} | ||
|
||
receive() external payable {} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,124 @@ | ||
// The Licensed Work is (c) 2022 Sygma | ||
// SPDX-License-Identifier: LGPL-3.0-only | ||
pragma solidity 0.8.11; | ||
|
||
import "../interfaces/IHandler.sol"; | ||
import "./ERCHandlerHelpers.sol"; | ||
|
||
/** | ||
@title Handles native token deposits and deposit executions. | ||
@author ChainSafe Systems. | ||
@notice This contract is intended to be used with the Bridge contract. | ||
*/ | ||
contract NativeTokenHandler is IHandler, ERCHandlerHelpers { | ||
|
||
address public immutable _nativeTokenAdapterAddress; | ||
|
||
/** | ||
@param bridgeAddress Contract address of previously deployed Bridge. | ||
*/ | ||
constructor( | ||
address bridgeAddress, | ||
address nativeTokenAdapterAddress | ||
) ERCHandlerHelpers(bridgeAddress) { | ||
_nativeTokenAdapterAddress = nativeTokenAdapterAddress; | ||
} | ||
|
||
event Withdrawal(address recipient, uint256 amount); | ||
event FundsTransferred(address recipient, uint256 amount); | ||
|
||
error FailedFundsTransfer(); | ||
error InsufficientBalance(); | ||
error InvalidSender(address sender); | ||
|
||
/** | ||
@notice A deposit is initiated by making a deposit to the NativeTokenAdapter which constructs the required | ||
deposit data and propagates it to the Bridge contract. | ||
@param resourceID ResourceID used to find address of token to be used for deposit. | ||
@param depositor Address of account making the deposit in the Bridge contract. | ||
@param data Consists of {amount} padded to 32 bytes. | ||
@notice Data passed into the function should be constructed as follows: | ||
amount uint256 bytes 0 - 32 | ||
destinationRecipientAddress length uint256 bytes 32 - 64 | ||
destinationRecipientAddress bytes bytes 64 - END | ||
@return deposit amount internal representation. | ||
*/ | ||
function deposit( | ||
bytes32 resourceID, | ||
address depositor, | ||
bytes calldata data | ||
) external override onlyBridge returns (bytes memory) { | ||
uint256 amount; | ||
(amount) = abi.decode(data, (uint256)); | ||
|
||
if(depositor != _nativeTokenAdapterAddress) revert InvalidSender(depositor); | ||
|
||
address tokenAddress = _resourceIDToTokenContractAddress[resourceID]; | ||
|
||
return abi.encodePacked(convertToInternalBalance(tokenAddress, amount)); | ||
} | ||
|
||
/** | ||
@notice Proposal execution should be initiated when a proposal is finalized in the Bridge contract | ||
by a relayer on the deposit's destination chain. | ||
@param resourceID ResourceID to be used when making deposits. | ||
@param data Consists of {amount}, {lenDestinationRecipientAddress} | ||
and {destinationRecipientAddress}. | ||
@notice Data passed into the function should be constructed as follows: | ||
amount uint256 bytes 0 - 32 | ||
destinationRecipientAddress length uint256 bytes 32 - 64 // not used | ||
destinationRecipientAddress bytes bytes 64 - 84 | ||
*/ | ||
function executeProposal(bytes32 resourceID, bytes calldata data) external override onlyBridge returns (bytes memory) { | ||
(uint256 amount) = abi.decode(data, (uint256)); | ||
address tokenAddress = _resourceIDToTokenContractAddress[resourceID]; | ||
address recipientAddress = address(bytes20(bytes(data[64:84]))); | ||
uint256 convertedAmount = convertToExternalBalance(tokenAddress, amount); | ||
|
||
(bool success, ) = address(recipientAddress).call{value: convertedAmount}(""); | ||
if(!success) revert FailedFundsTransfer(); | ||
emit FundsTransferred(recipientAddress, amount); | ||
|
||
return abi.encode(tokenAddress, address(recipientAddress), convertedAmount); | ||
} | ||
|
||
/** | ||
@notice Used to manually release ERC20 tokens from ERC20Safe. | ||
@param data Consists of {tokenAddress}, {recipient}, and {amount} all padded to 32 bytes. | ||
@notice Data passed into the function should be constructed as follows: | ||
tokenAddress address bytes 0 - 32 | ||
recipient address bytes 32 - 64 | ||
amount uint bytes 64 - 96 | ||
*/ | ||
function withdraw(bytes memory data) external override onlyBridge { | ||
address recipient; | ||
uint amount; | ||
|
||
if (address(this).balance <= amount) revert InsufficientBalance(); | ||
(, recipient, amount) = abi.decode(data, (address, address, uint)); | ||
|
||
(bool success, ) = address(recipient).call{value: amount}(""); | ||
if(!success) revert FailedFundsTransfer(); | ||
emit Withdrawal(recipient, amount); | ||
} | ||
|
||
/** | ||
@notice Sets {_resourceIDToContractAddress} with {contractAddress}, | ||
{_tokenContractAddressToTokenProperties[tokenAddress].resourceID} with {resourceID} and | ||
{_tokenContractAddressToTokenProperties[tokenAddress].isWhitelisted} to true for {contractAddress} in ERCHandlerHelpers contract. | ||
Sets decimals value for contractAddress if value is provided in args. | ||
@param resourceID ResourceID to be used when making deposits. | ||
@param contractAddress Address of contract to be called when a deposit is made and a deposited is executed. | ||
@param args Additional data passed to the handler - this should be 1 byte containing number of decimals places. | ||
*/ | ||
function setResource(bytes32 resourceID, address contractAddress, bytes calldata args) external onlyBridge { | ||
_setResource(resourceID, contractAddress); | ||
|
||
if (args.length > 0) { | ||
uint8 externalTokenDecimals = uint8(bytes1(args)); | ||
_setDecimals(contractAddress, externalTokenDecimals); | ||
} | ||
} | ||
|
||
receive() external payable {} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
// The Licensed Work is (c) 2022 Sygma | ||
// SPDX-License-Identifier: LGPL-3.0-only | ||
pragma solidity 0.8.11; | ||
|
||
interface IBasicFeeHandler { | ||
|
||
/** | ||
@notice Exposes getter function for _domainResourceIDToFee | ||
*/ | ||
function _domainResourceIDToFee(uint8 destinationDomainID, bytes32 resourceID) pure external returns (uint256); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,113 @@ | ||
// The Licensed Work is (c) 2022 Sygma | ||
// SPDX-License-Identifier: LGPL-3.0-only | ||
|
||
const TruffleAssert = require("truffle-assertions"); | ||
const Ethers = require("ethers"); | ||
|
||
const Helpers = require("../../helpers"); | ||
|
||
const NativeTokenHandlerContract = artifacts.require("NativeTokenHandler"); | ||
const NativeTokenAdapterContract = artifacts.require("NativeTokenAdapter"); | ||
const BasicFeeHandlerContract = artifacts.require("BasicFeeHandler"); | ||
const FeeHandlerRouterContract = artifacts.require("FeeHandlerRouter"); | ||
|
||
contract("Bridge - [collect fee - native token]", async (accounts) => { | ||
const originDomainID = 1; | ||
const destinationDomainID = 2; | ||
const adminAddress = accounts[0]; | ||
const depositorAddress = accounts[1]; | ||
|
||
const emptySetResourceData = "0x"; | ||
const resourceID = "0x0000000000000000000000000000000000000000000000000000000000000650"; | ||
const btcRecipientAddress = "bc1qs0fcdq73vgurej48yhtupzcv83un2p5qhsje7n"; | ||
const depositAmount = Ethers.utils.parseEther("1"); | ||
const fee = Ethers.utils.parseEther("0.1"); | ||
const transferredAmount = depositAmount.sub(fee); | ||
|
||
let BridgeInstance; | ||
let NativeTokenHandlerInstance; | ||
let BasicFeeHandlerInstance; | ||
let FeeHandlerRouterInstance; | ||
let NativeTokenAdapterInstance; | ||
|
||
beforeEach(async () => { | ||
await Promise.all([ | ||
(BridgeInstance = await Helpers.deployBridge( | ||
originDomainID, | ||
adminAddress | ||
)), | ||
]); | ||
|
||
|
||
FeeHandlerRouterInstance = await FeeHandlerRouterContract.new( | ||
BridgeInstance.address | ||
); | ||
BasicFeeHandlerInstance = await BasicFeeHandlerContract.new( | ||
BridgeInstance.address, | ||
FeeHandlerRouterInstance.address | ||
); | ||
NativeTokenAdapterInstance = await NativeTokenAdapterContract.new( | ||
BridgeInstance.address, | ||
resourceID | ||
); | ||
NativeTokenHandlerInstance = await NativeTokenHandlerContract.new( | ||
BridgeInstance.address, | ||
NativeTokenAdapterInstance.address, | ||
); | ||
|
||
await BridgeInstance.adminSetResource( | ||
NativeTokenHandlerInstance.address, | ||
resourceID, | ||
NativeTokenHandlerInstance.address, | ||
emptySetResourceData | ||
); | ||
await BasicFeeHandlerInstance.changeFee(destinationDomainID, resourceID, fee); | ||
await BridgeInstance.adminChangeFeeHandler(FeeHandlerRouterInstance.address), | ||
await FeeHandlerRouterInstance.adminSetResourceHandler( | ||
destinationDomainID, | ||
resourceID, | ||
BasicFeeHandlerInstance.address | ||
), | ||
|
||
// set MPC address to unpause the Bridge | ||
await BridgeInstance.endKeygen(Helpers.mpcAddress); | ||
}); | ||
|
||
it("Native token fee should be successfully deducted", async () => { | ||
const depositorBalanceBefore = await web3.eth.getBalance(depositorAddress); | ||
const adapterBalanceBefore = await web3.eth.getBalance(NativeTokenAdapterInstance.address); | ||
const handlerBalanceBefore = await web3.eth.getBalance(NativeTokenHandlerInstance.address); | ||
|
||
await TruffleAssert.passes( | ||
NativeTokenAdapterInstance.deposit( | ||
destinationDomainID, | ||
btcRecipientAddress, | ||
{ | ||
from: depositorAddress, | ||
value: depositAmount, | ||
} | ||
)); | ||
|
||
// check that correct ETH amount is successfully transferred to the adapter | ||
const adapterBalanceAfter = await web3.eth.getBalance(NativeTokenAdapterInstance.address); | ||
const handlerBalanceAfter = await web3.eth.getBalance(NativeTokenHandlerInstance.address); | ||
assert.strictEqual( | ||
new Ethers.BigNumber.from(transferredAmount).add(handlerBalanceBefore).toString(), handlerBalanceAfter | ||
); | ||
|
||
// check that adapter funds are transferred to the native handler contracts | ||
assert.strictEqual( | ||
adapterBalanceBefore, | ||
adapterBalanceAfter | ||
); | ||
|
||
// check that depositor before and after balances align | ||
const depositorBalanceAfter = await web3.eth.getBalance(depositorAddress); | ||
expect( | ||
Number(Ethers.utils.formatEther(new Ethers.BigNumber.from(depositorBalanceBefore).sub(depositAmount))) | ||
).to.be.within( | ||
Number(Ethers.utils.formatEther(depositorBalanceAfter))*0.99, | ||
Number(Ethers.utils.formatEther(depositorBalanceAfter))*1.01 | ||
) | ||
}); | ||
}); |
Oops, something went wrong.