From bf7894ef14f5f7c5219a8c61e8552139c74e684c Mon Sep 17 00:00:00 2001
From: nmlinaric <nikola.mlinaric93@gmail.com>
Date: Wed, 6 Mar 2024 14:54:29 +0100
Subject: [PATCH] feat: add social network adapter

---
 contracts/TestContracts.sol                   |  20 +++
 contracts/adapters/SocialNetworkAdapter.sol   |  53 ++++++
 .../fee/SocialNetworkPercentageFeeHandler.sol | 129 ++++++++++++++
 .../interfaces/ISocialNetworkBitcoin.sol      |  11 ++
 .../interfaces/ISocialNetworkController.sol   |  12 ++
 .../ISocialNetworkPercentageFeeHandler.sol    |  12 ++
 .../adapters/socialNetwork/executeProposal.js | 163 ++++++++++++++++++
 7 files changed, 400 insertions(+)
 create mode 100644 contracts/adapters/SocialNetworkAdapter.sol
 create mode 100644 contracts/handlers/fee/SocialNetworkPercentageFeeHandler.sol
 create mode 100644 contracts/interfaces/ISocialNetworkBitcoin.sol
 create mode 100644 contracts/interfaces/ISocialNetworkController.sol
 create mode 100644 contracts/interfaces/ISocialNetworkPercentageFeeHandler.sol
 create mode 100644 test/adapters/socialNetwork/executeProposal.js

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
+      )
+    });
+  }
+);