kleros/kleros-v2

View on GitHub
contracts/src/kleros-v1/kleros-liquid-xdai/WrappedPinakion.sol

Summary

Maintainability
Test Coverage
// SPDX-License-Identifier: MIT

pragma solidity 0.8.24;

import "@openzeppelin/contracts/proxy/utils/Initializable.sol";
import "../interfaces/ITokenController.sol";
import "./interfaces/ITokenBridge.sol";
import "./interfaces/IERC677.sol";

contract WrappedPinakion is Initializable {
    // ************************************* //
    // *              Events               * //
    // ************************************* //

    /// @notice Emitted when `value` tokens are moved from one account (`from`) to another (`to`).
    /// @dev Notice that `value` may be zero.
    event Transfer(address indexed from, address indexed to, uint256 value);

    /// @notice Emitted when the allowance of a `spender` for an `owner` is set by
    /// a call to {approve}. `value` is the new allowance.
    event Approval(address indexed owner, address indexed spender, uint256 value);

    // ************************************* //
    // *             Storage               * //
    // ************************************* //

    mapping(address => uint256) private balances;
    mapping(address => mapping(address => uint256)) public allowance;

    /// @notice Total supply of the token. Equals the total xPinakion deposit into the contract.
    uint256 public totalSupply;

    /// @notice Name of the token.
    string public name;

    /// @notice Symbol of the token.
    string public symbol;

    /// @notice Number of decimals of the token.
    uint8 public decimals;

    /// @notice The token's controller.
    address public controller;

    /// @notice Bridged PNK on xDai to be wrapped. This token is upgradeable.
    IERC677 public xPinakion;

    /// @notice xDai Token Bridge. The Token Bridge is upgradeable.
    ITokenBridge public tokenBridge;

    // ************************************* //
    // *        Function Modifiers         * //
    // ************************************* //

    /// @dev Verifies that the sender has ability to modify controlled parameters.
    modifier onlyController() {
        require(controller == msg.sender, "The caller is not the controller.");
        _;
    }

    // ************************************* //
    // *            Initializer            * //
    // ************************************* //

    /// @dev Initializer.
    /// @param _name for the wrapped PNK on the home chain.
    /// @param _symbol for wrapped PNK ticker on the home chain.
    /// @param _xPinakion the home PNK contract which is already bridged to the foreign PNK contract.
    /// @param _tokenBridge the TokenBridge contract.
    function initialize(
        string memory _name,
        string memory _symbol,
        IERC677 _xPinakion,
        ITokenBridge _tokenBridge
    ) public initializer {
        name = _name;
        symbol = _symbol;
        decimals = 18;
        xPinakion = _xPinakion;
        tokenBridge = _tokenBridge;
        controller = msg.sender;
    }

    // ************************************* //
    // *             Governance            * //
    // ************************************* //

    /// @param _controller The new controller of the contract
    function changeController(address _controller) external onlyController {
        controller = _controller;
    }

    // ************************************* //
    // *         State Modifiers           * //
    // ************************************* //

    /// @notice Converts bridged PNK (xPinakion) into wrapped PNK which can be staked in KlerosLiquid.
    /// @param _amount The amount of wrapped pinakions to mint.
    function deposit(uint256 _amount) external {
        _mint(msg.sender, _amount);
        require(
            xPinakion.transferFrom(msg.sender, address(this), _amount),
            "Sender does not have enough approved funds."
        );
    }

    /// @notice IERC20 Receiver functionality.
    /// @dev Converts bridged PNK (xPinakion) into wrapped PNK which can be staked in KlerosLiquid.
    /// If the tokenBridge is calling this function, then this contract has already received
    /// the xPinakion tokens. Notice that the Home bridge calls onTokenBridge as a result of
    /// someone invoking `relayTokensAndCall()` on the Foreign bridge contract.
    /// @param _token The token address the _amount belongs to.
    /// @param _amount The amount of wrapped PNK to mint.
    /// @param _data Calldata containing the address of the recipient.
    /// Notice that the address has to be padded to the right 32 bytes.
    function onTokenBridged(address _token, uint256 _amount, bytes calldata _data) external {
        require(msg.sender == address(tokenBridge), "Sender not authorized.");
        require(_token == address(xPinakion), "Token bridged is not xPinakion.");

        address recipient;
        assembly {
            recipient := calldataload(0x84)
        }
        _mint(recipient, _amount);
    }

    /// @notice Converts wrapped PNK back into bridged PNK (xPinakion).
    /// @param _amount The amount of bridged PNK to withdraw.
    function withdraw(uint256 _amount) external {
        _burn(_amount);
        require(xPinakion.transfer(msg.sender, _amount), "The `transfer` function must not fail.");
    }

    /// @notice Converts wrapped PNK back into PNK using the Token Bridge.
    /// @dev This function is not strictly needed, but it provides a good UX to users who want to get their Mainnet's PNK back.
    /// What normally takes 3 transactions, here is done in one go.
    /// Notice that the PNK have to be claimed on Mainnet's TokenBridge by the receiver.
    /// @param _amount The amount of PNK to withdraw.
    /// @param _receiver The address which will receive the PNK back in the foreign chain.
    function withdrawAndConvertToPNK(uint256 _amount, address _receiver) external {
        _burn(_amount);
        // Using approve is safe here, because this contract approves the bridge to spend the tokens and triggers the relay immediately.
        xPinakion.approve(address(tokenBridge), _amount);
        tokenBridge.relayTokens(xPinakion, _receiver, _amount);
    }

    /// @notice Moves `_amount` tokens from the caller's account to `_recipient`.
    /// @param _recipient The entity receiving the funds.
    /// @param _amount The amount to tranfer in base units.
    /// @return True on success.
    function transfer(address _recipient, uint256 _amount) public returns (bool) {
        if (isContract(controller)) {
            require(
                ITokenController(controller).onTransfer(msg.sender, _recipient, _amount),
                "Token controller rejects transfer."
            );
        }
        balances[msg.sender] = balances[msg.sender] - _amount; // ERC20: transfer amount exceeds balance
        balances[_recipient] = balances[_recipient] + _amount;
        emit Transfer(msg.sender, _recipient, _amount);
        return true;
    }

    /// @notice Moves `_amount` tokens from `_sender` to `_recipient` using the
    /// allowance mechanism. `_amount` is then deducted from the caller's allowance.
    /// @param _sender The entity to take the funds from.
    /// @param _recipient The entity receiving the funds.
    /// @param _amount The amount to tranfer in base units.
    /// @return True on success.
    function transferFrom(address _sender, address _recipient, uint256 _amount) public returns (bool) {
        if (isContract(controller)) {
            require(
                ITokenController(controller).onTransfer(_sender, _recipient, _amount),
                "Token controller rejects transfer."
            );
        }

        // The controller of this contract can move tokens around at will,
        // this is important to recognize! Confirm that you trust the
        // controller of this contract, which in most situations should be
        // another open source smart contract or 0x0.
        if (msg.sender != controller) {
            allowance[_sender][msg.sender] = allowance[_sender][msg.sender] - _amount; // ERC20: transfer amount exceeds allowance.
        }

        balances[_sender] = balances[_sender] - _amount; // ERC20: transfer amount exceeds balance
        balances[_recipient] = balances[_recipient] + _amount;
        emit Transfer(_sender, _recipient, _amount);
        return true;
    }

    /// @notice Approves `_spender` to spend `_amount`.
    /// @param _spender The entity allowed to spend funds.
    /// @param _amount The amount of base units the entity will be allowed to spend.
    /// @return True on success.
    function approve(address _spender, uint256 _amount) public returns (bool) {
        // Alerts the token controller of the approve function call
        if (isContract(controller)) {
            require(
                ITokenController(controller).onApprove(msg.sender, _spender, _amount),
                "Token controller does not approve."
            );
        }

        allowance[msg.sender][_spender] = _amount;
        emit Approval(msg.sender, _spender, _amount);
        return true;
    }

    /// @notice Increases the `_spender` allowance by `_addedValue`.
    /// @param _spender The entity allowed to spend funds.
    /// @param _addedValue The amount of extra base units the entity will be allowed to spend.
    /// @return True on success.
    function increaseAllowance(address _spender, uint256 _addedValue) public returns (bool) {
        uint256 newAllowance = allowance[msg.sender][_spender] + _addedValue;
        // Alerts the token controller of the approve function call
        if (isContract(controller)) {
            require(
                ITokenController(controller).onApprove(msg.sender, _spender, newAllowance),
                "Token controller does not approve."
            );
        }

        allowance[msg.sender][_spender] = newAllowance;
        emit Approval(msg.sender, _spender, newAllowance);
        return true;
    }

    /// @notice Decreases the `_spender` allowance by `_subtractedValue`.
    /// @param _spender The entity whose spending allocation will be reduced.
    /// @param _subtractedValue The reduction of spending allocation in base units.
    /// @return True on success.
    function decreaseAllowance(address _spender, uint256 _subtractedValue) public returns (bool) {
        uint256 newAllowance = allowance[msg.sender][_spender] - _subtractedValue; // ERC20: decreased allowance below zero
        // Alerts the token controller of the approve function call
        if (isContract(controller)) {
            require(
                ITokenController(controller).onApprove(msg.sender, _spender, newAllowance),
                "Token controller does not approve."
            );
        }

        allowance[msg.sender][_spender] = newAllowance;
        emit Approval(msg.sender, _spender, newAllowance);
        return true;
    }

    // ************************************* //
    // *            Internal               * //
    // ************************************* //

    /// @dev Internal function that mints an amount of the token and assigns it to
    /// an account. This encapsulates the modification of balances such that the
    /// proper events are emitted.
    /// @param _recipient The address which will receive the minted tokens.
    /// @param _amount The amount that will be created.
    function _mint(address _recipient, uint256 _amount) internal {
        totalSupply = totalSupply + _amount;
        balances[_recipient] = balances[_recipient] + _amount;
        emit Transfer(address(0x0), _recipient, _amount);
    }

    /// @dev Destroys `_amount` tokens from the caller. Cannot burn locked tokens.
    /// @param _amount The quantity of tokens to burn in base units.
    function _burn(uint256 _amount) internal {
        if (isContract(controller)) {
            require(
                ITokenController(controller).onTransfer(msg.sender, address(0x0), _amount),
                "Token controller rejects transfer."
            );
        }
        balances[msg.sender] = balances[msg.sender] - _amount; // ERC20: burn amount exceeds balance
        totalSupply = totalSupply - _amount;
        emit Transfer(msg.sender, address(0x0), _amount);
    }

    /// @dev Internal function to determine if an address is a contract.
    /// @param _addr The address being queried.
    /// @return True if `_addr` is a contract.
    function isContract(address _addr) internal view returns (bool) {
        uint256 size;
        if (_addr == address(0)) return false;
        assembly {
            size := extcodesize(_addr)
        }
        return size > 0;
    }

    // ************************************* //
    // *           Public Views            * //
    // ************************************* //

    /// @dev Gets the balance of the specified address.
    /// @param _owner The address to query the balance of.
    /// @return uint256 value representing the amount owned by the passed address.
    function balanceOf(address _owner) public view returns (uint256) {
        return balances[_owner];
    }
}