Skip to content
This repository was archived by the owner on Mar 11, 2025. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 62 additions & 0 deletions contracts/adapters/nativeTokens/NativeTokenAdapter.sol
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 {}
}
2 changes: 1 addition & 1 deletion contracts/handlers/FeeHandlerRouter.sol
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ contract FeeHandlerRouter is IFeeHandler, AccessControl {
_setupRole(DEFAULT_ADMIN_ROLE, msg.sender);
}
/**
@notice Maps the {handlerAddress} to {resourceID} to {destinantionDomainID} in {_domainResourceIDToFeeHandlerAddress}.
@notice Maps the {handlerAddress} to {resourceID} to {destinationDomainID} in {_domainResourceIDToFeeHandlerAddress}.
@param destinationDomainID ID of chain FeeHandler contracts will be called.
@param resourceID ResourceID for which the corresponding FeeHandler will collect/calcualte fee.
@param handlerAddress Address of FeeHandler which will be called for specified resourceID.
Expand Down
124 changes: 124 additions & 0 deletions contracts/handlers/NativeTokenHandler.sol
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 {}
}
4 changes: 2 additions & 2 deletions contracts/handlers/fee/BasicFeeHandler.sol
Original file line number Diff line number Diff line change
Expand Up @@ -99,11 +99,11 @@ contract BasicFeeHandler is IFeeHandler, AccessControl {
}

/**
@notice Maps the {newFee} to {destinantionDomainID} to {resourceID} in {_domainResourceIDToFee}.
@notice Maps the {newFee} to {destinationDomainID} to {resourceID} in {_domainResourceIDToFee}.
@notice Only callable by admin.
@param destinationDomainID ID of chain fee will be set.
@param resourceID ResourceID for which fee will be set.
@param newFee Value to which fee will be updated to for the provided {destinantionDomainID} and {resourceID}.
@param newFee Value to which fee will be updated to for the provided {destinationDomainID} and {resourceID}.
*/
function changeFee(uint8 destinationDomainID, bytes32 resourceID, uint256 newFee) external onlyAdmin {
uint256 currentFee = _domainResourceIDToFee[destinationDomainID][resourceID];
Expand Down
11 changes: 11 additions & 0 deletions contracts/interfaces/IBasicFeeHandler.sol
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);
}
24 changes: 23 additions & 1 deletion contracts/interfaces/IBridge.sol
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,32 @@ interface IBridge {
*/
function _domainID() external returns (uint8);

/**
@notice Exposing getter for {_feeHandler} instead of forcing the use of call.
@return address The {_feeHandler} that is currently set for the Bridge contract.
*/
function _feeHandler() external returns (address);

/**
@notice Exposing getter for {_resourceIDToHandlerAddress}.
@param resourceID ResourceID to be used when making deposits.
@return address The {handlerAddress} that is currently set for the resourceID.
*/
function _resourceIDToHandlerAddress(bytes32 resourceID) external view returns (address);
}

/**
@notice Initiates a transfer using a specified handler contract.
@notice Only callable when Bridge is not paused.
@param destinationDomainID ID of chain deposit will be bridged to.
@param resourceID ResourceID used to find address of handler to be used for deposit.
@param depositData Additional data to be passed to specified handler.
@param feeData Additional data to be passed to the fee handler.
@notice Emits {Deposit} event with all necessary parameters.
*/
function deposit(
uint8 destinationDomainID,
bytes32 resourceID,
bytes calldata depositData,
bytes calldata feeData
) external payable returns (uint64 depositNonce, bytes memory handlerResponse);
}
113 changes: 113 additions & 0 deletions test/adapters/native/collectFee.js
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
)
});
});
Loading