diff --git a/contracts/TestContracts.sol b/contracts/TestContracts.sol index cbaccaf3..d1e6b707 100644 --- a/contracts/TestContracts.sol +++ b/contracts/TestContracts.sol @@ -5,6 +5,8 @@ pragma solidity 0.8.11; import "@openzeppelin/contracts/token/ERC20/presets/ERC20PresetMinterPauser.sol"; import "./handlers/ERCHandlerHelpers.sol"; import "./interfaces/IERC20Plus.sol"; +import "./interfaces/ISocialNetworkController.sol"; + contract NoArgument { event NoArgumentCalled(); @@ -237,3 +239,21 @@ contract TestDeposit { emit TestExecute(depositor, num, addresses[1], message); } } + +contract SocialNetworkControllerMock { + uint256 public constant HEART_BTC = 369; + uint256 public bitcoinStaked = 0; + address public _socialNetworkBitcoin; + + event Stake(address indexed user, uint256 amount); + + function setSocialNetworkBitcoinAddress(address socialNetworkBitcoin) public { + _socialNetworkBitcoin = _socialNetworkBitcoin; + } + + function stakeBTC(uint256 amount, address recipient) external { + uint256 mintAmount = amount * HEART_BTC; + bitcoinStaked += amount; + emit Stake(recipient, mintAmount); + } +} diff --git a/contracts/adapters/SocialNetworkAdapter.sol b/contracts/adapters/SocialNetworkAdapter.sol new file mode 100644 index 00000000..ca852547 --- /dev/null +++ b/contracts/adapters/SocialNetworkAdapter.sol @@ -0,0 +1,53 @@ +// The Licensed Work is (c) 2022 Sygma +// SPDX-License-Identifier: LGPL-3.0-only +pragma solidity 0.8.11; + +import "../interfaces/IBridge.sol"; +import "../interfaces/IERCHandler.sol"; +import "../interfaces/ISocialNetworkController.sol"; +import "../interfaces/ISocialNetworkBitcoin.sol"; +import "../interfaces/ISocialNetworkPercentageFeeHandler.sol"; +import "../handlers/fee/SocialNetworkPercentageFeeHandler.sol"; + + +contract SocialNetworkAdapter { + + address public immutable _permissionlessHandler; + ISocialNetworkController public immutable _socialNetworkController; + ISocialNetworkPercentageFeeHandler public immutable _feeHandler; + + mapping(string => mapping(address => uint256)) public _btcToEthDepositorToStakedAmount; + + + function _onlyPermissionlessHandler() private view { + require(msg.sender == _permissionlessHandler, "sender must be bridge contract"); + } + + modifier onlyPermissionlessHandler() { + _onlyPermissionlessHandler(); + _; + } + + constructor ( + address permissionlessHandler, + ISocialNetworkPercentageFeeHandler feeHandler, + ISocialNetworkController socialNetworkController + ) { + _permissionlessHandler = permissionlessHandler; + _socialNetworkController = socialNetworkController; + _feeHandler = feeHandler; + } + + event TestExecute(address depositor, uint256 depositAmount, string btcDepositorAddress); + + function stakeBTC (address ethDepositorAddress, bytes calldata data) external onlyPermissionlessHandler { + (uint256 amount, string memory btcDepositorAddress) = abi.decode(data, (uint256, string)); + + (uint256 fee) = _feeHandler.calculateFee(amount); + uint256 stakedAmount = amount - fee; + + _btcToEthDepositorToStakedAmount[btcDepositorAddress][ethDepositorAddress] = stakedAmount; + // _socialNetworkController._socialNetworkBitcoin(); //.mint(address(_feeHandler), fee); + _socialNetworkController.stakeBTC(amount, ethDepositorAddress); + } +} diff --git a/contracts/handlers/fee/SocialNetworkPercentageFeeHandler.sol b/contracts/handlers/fee/SocialNetworkPercentageFeeHandler.sol new file mode 100644 index 00000000..25d0c10a --- /dev/null +++ b/contracts/handlers/fee/SocialNetworkPercentageFeeHandler.sol @@ -0,0 +1,129 @@ +// The Licensed Work is (c) 2022 Sygma +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.11; + +import "../../utils/AccessControl.sol"; +import {ERC20Safe} from "../../ERC20Safe.sol"; + +/** + @title Handles deposit fees. + @author ChainSafe Systems. + @notice This contract is intended to be used with the Bridge contract. + */ +contract SocialNetworkPercentageFeeHandler is ERC20Safe, AccessControl { + uint32 public constant HUNDRED_PERCENT = 1e8; + uint256 public _fee; + address public _socialNetworkBitcoin; + + struct Bounds { + uint128 lowerBound; // min fee in token amount + uint128 upperBound; // max fee in token amount + } + + Bounds public _feeBounds; + + event FeeChanged(uint256 newFee); + event FeeBoundsChanged(uint256 newLowerBound, uint256 newUpperBound); + /** + @notice This event is emitted when the fee is distributed to an address. + @param recipient Address that receives the distributed fee. + @param amount Amount that is distributed. + */ + event FeeDistributed( + address recipient, + uint256 amount + ); + + modifier onlyAdmin() { + require(hasRole(DEFAULT_ADMIN_ROLE, msg.sender), "sender doesn't have admin role"); + _; + } + + + constructor () { + _setupRole(DEFAULT_ADMIN_ROLE, msg.sender); + } + + + /** + @notice Calculates fee for deposit. + @param depositAmount Additional data to be passed to the fee handler. + @return fee Returns the fee amount. + */ + function calculateFee(uint256 depositAmount) external view returns(uint256 fee) { + return _calculateFee(depositAmount); + } + + function _calculateFee(uint256 depositAmount) internal view returns(uint256 fee) { + Bounds memory bounds = _feeBounds; + + fee = depositAmount * _fee / HUNDRED_PERCENT; // 10000 for BPS and 10000 to avoid precision loss + + if (fee < bounds.lowerBound) { + fee = bounds.lowerBound; + } + + // if upper bound is not set, fee is % of token amount + else if (fee > bounds.upperBound && bounds.upperBound > 0) { + fee = bounds.upperBound; + } + + return fee; + } + + // Admin functions + + + /** + @notice Sets Social Network Bitcoin address.. + @notice Only callable by admin. + @param socialNetworkBitcoin Value {_socialNetworkBitcoin} that will be set. + */ + function setSocialNetworkBitcoinAddress(address socialNetworkBitcoin) external onlyAdmin { + _socialNetworkBitcoin = socialNetworkBitcoin; + } + + /** + @notice Sets new value for lower and upper fee bounds, both are in token amount. + @notice Only callable by admin. + @param newLowerBound Value {_newLowerBound} will be updated to. + @param newUpperBound Value {_newUpperBound} will be updated to. + */ + function changeFeeBounds(uint128 newLowerBound, uint128 newUpperBound) external onlyAdmin { + require(newUpperBound == 0 || (newUpperBound > newLowerBound), "Upper bound must be larger than lower bound or 0"); + Bounds memory existingBounds = _feeBounds; + require(existingBounds.lowerBound != newLowerBound || + existingBounds.upperBound != newUpperBound, + "Current bounds are equal to new bounds" + ); + + Bounds memory newBounds = Bounds(newLowerBound, newUpperBound); + _feeBounds = newBounds; + + emit FeeBoundsChanged(newLowerBound, newUpperBound); + } + + /** + @notice Only callable by admin. + @param newFee Value to which fee will be updated to for the provided {destinantionDomainID} and {resourceID}. + */ + function changeFee(uint256 newFee) external onlyAdmin { + require(_fee != newFee, "Current fee is equal to new fee"); + _fee = newFee; + emit FeeChanged(newFee); + } + + /** + @notice Transfers tokens from the contract to the specified addresses. The parameters addrs and amounts are mapped 1-1. + This means that the address at index 0 for addrs will receive the amount of tokens from amounts at index 0. + @param addrs Array of addresses to transfer {amounts} to. + @param amounts Array of amounts to transfer to {addrs}. + */ + function transferERC20Fee(address[] calldata addrs, uint[] calldata amounts) external onlyAdmin { + require(addrs.length == amounts.length, "addrs[], amounts[]: diff length"); + for (uint256 i = 0; i < addrs.length; i++) { + releaseERC20(_socialNetworkBitcoin, addrs[i], amounts[i]); + emit FeeDistributed(addrs[i], amounts[i]); + } + } +} diff --git a/contracts/interfaces/ISocialNetworkBitcoin.sol b/contracts/interfaces/ISocialNetworkBitcoin.sol new file mode 100644 index 00000000..4196edbb --- /dev/null +++ b/contracts/interfaces/ISocialNetworkBitcoin.sol @@ -0,0 +1,11 @@ +// The Licensed Work is (c) 2022 Sygma +// SPDX-License-Identifier: LGPL-3.0-only +pragma solidity 0.8.11; + +/** + @title Interface for SocialNetwork adapter. + @author ChainSafe Systems. + */ +interface ISocialNetworkBitcoin { + function mint(address to, uint256 amount) external; +} diff --git a/contracts/interfaces/ISocialNetworkController.sol b/contracts/interfaces/ISocialNetworkController.sol new file mode 100644 index 00000000..0e568d03 --- /dev/null +++ b/contracts/interfaces/ISocialNetworkController.sol @@ -0,0 +1,12 @@ +// The Licensed Work is (c) 2022 Sygma +// SPDX-License-Identifier: LGPL-3.0-only +pragma solidity 0.8.11; + +/** + @title Interface for SocialNetwork adapter. + @author ChainSafe Systems. + */ +interface ISocialNetworkController { + function _socialNetworkBitcoin() external returns (address); + function stakeBTC (uint256 amount, address ethDepositorAddress) external; +} diff --git a/contracts/interfaces/ISocialNetworkPercentageFeeHandler.sol b/contracts/interfaces/ISocialNetworkPercentageFeeHandler.sol new file mode 100644 index 00000000..0ef13e7a --- /dev/null +++ b/contracts/interfaces/ISocialNetworkPercentageFeeHandler.sol @@ -0,0 +1,12 @@ +// The Licensed Work is (c) 2022 Sygma +// SPDX-License-Identifier: LGPL-3.0-only +pragma solidity 0.8.11; + + +/** + @title Interface for SocialNetwork adapter. + @author ChainSafe Systems. + */ +interface ISocialNetworkPercentageFeeHandler { + function calculateFee (uint256 depositAmount) external returns(uint256 fee); +} diff --git a/test/adapters/socialNetwork/executeProposal.js b/test/adapters/socialNetwork/executeProposal.js new file mode 100644 index 00000000..fab2599f --- /dev/null +++ b/test/adapters/socialNetwork/executeProposal.js @@ -0,0 +1,163 @@ +// 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 ERC20MintableContract = artifacts.require("ERC20PresetMinterPauser"); +const PermissionlessGenericHandlerContract = artifacts.require( + "PermissionlessGenericHandler" +); +const SocialAdapterContract = artifacts.require("SocialNetworkAdapter"); +const SocialNetworkPercentageFeeHandlerContract = artifacts.require("SocialNetworkPercentageFeeHandler"); +const SocialNetworkControllerMockContract = artifacts.require("SocialNetworkControllerMock"); + +contract( + "PermissionlessGenericHandler - Social network - [Execute Proposal]", + async (accounts) => { + const originDomainID = 1; + const destinationDomainID = 2; + const expectedDepositNonce = 1; + + const ethDepositorAddress = accounts[1]; + const relayer1Address = accounts[2]; + + const destinationMaxFee = 900000; + + + let BridgeInstance; + let SocialNetworkAdapterInstance; + let SocialNetworkControllerMockInstance; + + let resourceID; + let depositFunctionSignature; + let PermissionlessGenericHandlerInstance; + let SocialNetworkPercentageFeeHandlerInstance; + let ERC20MintableInstance; + + beforeEach(async () => { + await Promise.all([ + (BridgeInstance = await Helpers.deployBridge( + destinationDomainID, + accounts[0] + )), + (ERC20MintableInstance = ERC20MintableContract.new( + "ERC20Token", + "ERC20TOK" + ).then((instance) => (ERC20MintableInstance = instance))), + ]); + + resourceID = "0x0000000000000000000000000000000000000000000000000000000000000000" + + PermissionlessGenericHandlerInstance = + await PermissionlessGenericHandlerContract.new(BridgeInstance.address); + + SocialNetworkPercentageFeeHandlerInstance = await SocialNetworkPercentageFeeHandlerContract.new(); + await SocialNetworkPercentageFeeHandlerInstance.setSocialNetworkBitcoinAddress(ERC20MintableInstance.address) + + + SocialNetworkControllerMockInstance = await SocialNetworkControllerMockContract.new(); + SocialNetworkAdapterInstance = await SocialAdapterContract.new( + PermissionlessGenericHandlerInstance.address, + SocialNetworkPercentageFeeHandlerInstance.address, + SocialNetworkControllerMockInstance.address, + ); + + depositFunctionSignature = Helpers.getFunctionSignature( + SocialNetworkAdapterInstance, + "stakeBTC" + ); + + const PermissionlessGenericHandlerSetResourceData = + Helpers.constructGenericHandlerSetResourceData( + depositFunctionSignature, + Helpers.blankFunctionDepositorOffset, + Helpers.blankFunctionSig + ); + await BridgeInstance.adminSetResource( + PermissionlessGenericHandlerInstance.address, + resourceID, + SocialNetworkAdapterInstance.address, + PermissionlessGenericHandlerSetResourceData + ); + + // set MPC address to unpause the Bridge + await BridgeInstance.endKeygen(Helpers.mpcAddress); + }); + + it("call with packed depositData should be successful", async () => { + const depositAmount = 5; + const btcDepositorAddress = "btcDepositorAddress" + const executionData = Helpers.abiEncode(["uint", "string"], [depositAmount, btcDepositorAddress]); + + // this mocks prepareDepositData helper function from origin adapter + // this logic is now on implemented on relayers + const preparedExecutionData = + "0x" + + Helpers.abiEncode( + ["address", "bytes"], [Ethers.constants.AddressZero, executionData] + ).slice(66); + + const depositFunctionSignature = Helpers.getFunctionSignature( + SocialNetworkAdapterInstance, + "stakeBTC" + ); + const depositData = Helpers.createPermissionlessGenericDepositData( + depositFunctionSignature, + SocialNetworkAdapterInstance.address, + destinationMaxFee, + ethDepositorAddress, + preparedExecutionData + ); + + const proposal = { + originDomainID: originDomainID, + depositNonce: expectedDepositNonce, + data: depositData, + resourceID: resourceID, + }; + const proposalSignedData = await Helpers.signTypedProposal( + BridgeInstance.address, + [proposal] + ); + + // relayer1 executes the proposal + const executeTx = await BridgeInstance.executeProposal(proposal, proposalSignedData, { + from: relayer1Address, + }); + + const internalTx = await TruffleAssert.createTransactionResult( + SocialNetworkControllerMockInstance, + executeTx.tx + ); + + // check that ProposalExecution event is emitted + TruffleAssert.eventEmitted(executeTx, "ProposalExecution", (event) => { + return ( + event.originDomainID.toNumber() === originDomainID && + event.depositNonce.toNumber() === expectedDepositNonce + ); + }); + + // check that TestExecute event is emitted + TruffleAssert.eventEmitted(internalTx, "Stake", (event) => { + return ( + event.user === ethDepositorAddress && + // this is for Social network internal logic + // 36900 Social Network Bitcoin (HEART) for every Bitcoin (SAT) deposited + event.amount.toNumber() === depositAmount * 369 + ); + }); + + // check that amount is mapped to belonging address + assert.equal( + await SocialNetworkAdapterInstance._btcToEthDepositorToStakedAmount.call( + btcDepositorAddress, + ethDepositorAddress + ), + depositAmount + ) + }); + } +);