Skip to content

Commit

Permalink
feat: add native token handler and adapter (#254)
Browse files Browse the repository at this point in the history
  • Loading branch information
nmlinaric authored Jul 15, 2024
1 parent 88fd294 commit d5cea35
Show file tree
Hide file tree
Showing 12 changed files with 1,210 additions and 4 deletions.
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

0 comments on commit d5cea35

Please sign in to comment.