Skip to content

Commit

Permalink
Add integration for Phase 2 (#9)
Browse files Browse the repository at this point in the history
* Add Uniswap integration

Allow to buy ETH or any other token.

* Add tests

Reach 100% coverage.

* Add Uniswap: fix fetching wrong exchange

Address PR #3 comments.

* Remove _from parameter from contributeExternalToken

Address PR #3 comments.

* Add extra-safety measures

Make sure no funds are left in the contract.
Address PR #3 comments.

* fixup! Add extra-safety measures

* Add check for non-existent Uniswap exchange

* test: Fix name in aux variable

* Add flag to signal activation of tokens in Registry

Fixes #4.

* Add integration for Phase 2

Allow to buy bonded tokens directly from Uniswap and stake them in the
Registry.
Fixes #5.

* Remove unnecessary isClosed function from IPresale interface

Address PR #9 comments.

* Update README.md

* UniswapWrapper: add msg.sender == _token check to receiveApproval()

* Remove duplicated Uniswap contracts for testing

* Add back Uniswap interfaces

* Uniswap wrapper phase 2: add tests for receiveApproval wrong sender

* Uniswap wrapper phase 2: add check for data length

... in receiveApproval

* UniswapWrapper: update outdated / misleading comments

* Add new refund mechanism to Uniswap wrapper

And abstract it into a shared base contract.

* Reuse refundable tests in subclasses

* 1.3.0

Co-authored-by: Brett Sun <[email protected]>
  • Loading branch information
ßingen and sohkai authored Feb 13, 2020
1 parent ac27f6d commit 663ff3d
Show file tree
Hide file tree
Showing 10 changed files with 638 additions and 126 deletions.
45 changes: 3 additions & 42 deletions contracts/CourtPresaleActivate.sol
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,12 @@ import "@aragon/court/contracts/standards/ERC900.sol";
import "./lib/IPresale.sol";
import "./lib/uniswap/interfaces/IUniswapExchange.sol";
import "./lib/uniswap/interfaces/IUniswapFactory.sol";
import "./Refundable.sol";


contract CourtPresaleActivate is IsContract, ApproveAndCallFallBack {
contract CourtPresaleActivate is Refundable, IsContract, ApproveAndCallFallBack {
using SafeERC20 for ERC20;

string private constant ERROR_NOT_GOVERNOR = "CPA_NOT_GOVERNOR";
string private constant ERROR_TOKEN_NOT_CONTRACT = "CPA_TOKEN_NOT_CONTRACT";
string private constant ERROR_REGISTRY_NOT_CONTRACT = "CPA_REGISTRY_NOT_CONTRACT";
string private constant ERROR_PRESALE_NOT_CONTRACT = "CPA_PRESALE_NOT_CONTRACT";
Expand All @@ -22,33 +22,23 @@ contract CourtPresaleActivate is IsContract, ApproveAndCallFallBack {
string private constant ERROR_TOKEN_TRANSFER_FAILED = "CPA_TOKEN_TRANSFER_FAILED";
string private constant ERROR_TOKEN_APPROVAL_FAILED = "CPA_TOKEN_APPROVAL_FAILED";
string private constant ERROR_WRONG_TOKEN = "CPA_WRONG_TOKEN";
string private constant ERROR_ETH_REFUND = "CPA_ETH_REFUND";
string private constant ERROR_TOKEN_REFUND = "CPA_TOKEN_REFUND";
string private constant ERROR_UNISWAP_UNAVAILABLE = "CPA_UNISWAP_UNAVAILABLE";
string private constant ERROR_NOT_ENOUGH_BALANCE = "CPA_NOT_ENOUGH_BALANCE";

bytes32 internal constant ACTIVATE_DATA = keccak256("activate(uint256)");

address public governor;
ERC20 public bondedToken;
ERC900 public registry;
IPresale public presale;
IUniswapFactory public uniswapFactory;

event Bought(address from, address contributionToken, uint256 buyAmount, uint256 stakedAmount, bool activated);

modifier onlyGovernor() {
require(msg.sender == governor, ERROR_NOT_GOVERNOR);
_;
}

constructor(address _governor, ERC20 _bondedToken, ERC900 _registry, IPresale _presale, IUniswapFactory _uniswapFactory) public {
constructor(address _governor, ERC20 _bondedToken, ERC900 _registry, IPresale _presale, IUniswapFactory _uniswapFactory) Refundable(_governor) public {
require(isContract(address(_bondedToken)), ERROR_TOKEN_NOT_CONTRACT);
require(isContract(address(_registry)), ERROR_REGISTRY_NOT_CONTRACT);
require(isContract(address(_presale)), ERROR_PRESALE_NOT_CONTRACT);
require(isContract(address(_uniswapFactory)), ERROR_UNISWAP_FACTORY_NOT_CONTRACT);

governor = _governor;
bondedToken = _bondedToken;
registry = _registry;
presale = _presale;
Expand Down Expand Up @@ -151,35 +141,6 @@ contract CourtPresaleActivate is IsContract, ApproveAndCallFallBack {
_contributeEth(_minTokens, _deadline, _activate);
}

/**
* @notice Refunds accidentally sent ETH. Only governor can do it
* @param _recipient Address to send funds to
* @param _amount Amount to be refunded
*/
function refundEth(address payable _recipient, uint256 _amount) external onlyGovernor {
require(_amount > 0, ERROR_ZERO_AMOUNT);
uint256 selfBalance = address(this).balance;
require(selfBalance >= _amount, ERROR_NOT_ENOUGH_BALANCE);

// solium-disable security/no-call-value
(bool result,) = _recipient.call.value(_amount)("");
require(result, ERROR_ETH_REFUND);
}

/**
* @notice Refunds accidentally sent ERC20 tokens. Only governor can do it
* @param _token Token to be refunded
* @param _recipient Address to send funds to
* @param _amount Amount to be refunded
*/
function refundToken(ERC20 _token, address _recipient, uint256 _amount) external onlyGovernor {
require(_amount > 0, ERROR_ZERO_AMOUNT);
uint256 selfBalance = _token.balanceOf(address(this));
require(selfBalance >= _amount, ERROR_NOT_ENOUGH_BALANCE);

require(_token.safeTransfer(_recipient, _amount), ERROR_TOKEN_REFUND);
}

function _contributeEth(uint256 _minTokens, uint256 _deadline, bool _activate) internal {
require(msg.value > 0, ERROR_ZERO_AMOUNT);

Expand Down
55 changes: 55 additions & 0 deletions contracts/Refundable.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
pragma solidity ^0.5.8;

import "@aragon/court/contracts/lib/os/ERC20.sol";
import "@aragon/court/contracts/lib/os/SafeERC20.sol";


contract Refundable {
using SafeERC20 for ERC20;

string private constant ERROR_NOT_GOVERNOR = "REF_NOT_GOVERNOR";
string private constant ERROR_ZERO_AMOUNT = "REF_ZERO_AMOUNT";
string private constant ERROR_NOT_ENOUGH_BALANCE = "REF_NOT_ENOUGH_BALANCE";
string private constant ERROR_ETH_REFUND = "REF_ETH_REFUND";
string private constant ERROR_TOKEN_REFUND = "REF_TOKEN_REFUND";

address public governor;

modifier onlyGovernor() {
require(msg.sender == governor, ERROR_NOT_GOVERNOR);
_;
}

constructor(address _governor) public {
governor = _governor;
}

/**
* @notice Refunds accidentally sent ETH. Only governor can do it
* @param _recipient Address to send funds to
* @param _amount Amount to be refunded
*/
function refundEth(address payable _recipient, uint256 _amount) external onlyGovernor {
require(_amount > 0, ERROR_ZERO_AMOUNT);
uint256 selfBalance = address(this).balance;
require(selfBalance >= _amount, ERROR_NOT_ENOUGH_BALANCE);

// solium-disable security/no-call-value
(bool result,) = _recipient.call.value(_amount)("");
require(result, ERROR_ETH_REFUND);
}

/**
* @notice Refunds accidentally sent ERC20 tokens. Only governor can do it
* @param _token Token to be refunded
* @param _recipient Address to send funds to
* @param _amount Amount to be refunded
*/
function refundToken(ERC20 _token, address _recipient, uint256 _amount) external onlyGovernor {
require(_amount > 0, ERROR_ZERO_AMOUNT);
uint256 selfBalance = _token.balanceOf(address(this));
require(selfBalance >= _amount, ERROR_NOT_ENOUGH_BALANCE);

require(_token.safeTransfer(_recipient, _amount), ERROR_TOKEN_REFUND);
}
}
160 changes: 160 additions & 0 deletions contracts/UniswapWrapper.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
pragma solidity ^0.5.8;

import "@aragon/court/contracts/lib/os/IsContract.sol";
import "@aragon/court/contracts/lib/os/ERC20.sol";
import "@aragon/court/contracts/lib/os/SafeERC20.sol";
import "@aragon/court/contracts/standards/ERC900.sol";
import "./lib/uniswap/interfaces/IUniswapExchange.sol";
import "./lib/uniswap/interfaces/IUniswapFactory.sol";
import "./Refundable.sol";


contract UniswapWrapper is Refundable, IsContract {
using SafeERC20 for ERC20;

string private constant ERROR_TOKEN_NOT_CONTRACT = "UW_TOKEN_NOT_CONTRACT";
string private constant ERROR_REGISTRY_NOT_CONTRACT = "UW_REGISTRY_NOT_CONTRACT";
string private constant ERROR_UNISWAP_FACTORY_NOT_CONTRACT = "UW_UNISWAP_FACTORY_NOT_CONTRACT";
string private constant ERROR_RECEIVED_WRONG_TOKEN = "UW_RECEIVED_WRONG_TOKEN";
string private constant ERROR_WRONG_DATA_LENGTH = "UW_WRONG_DATA_LENGTH";
string private constant ERROR_ZERO_AMOUNT = "UW_ZERO_AMOUNT";
string private constant ERROR_TOKEN_TRANSFER_FAILED = "UW_TOKEN_TRANSFER_FAILED";
string private constant ERROR_TOKEN_APPROVAL_FAILED = "UW_TOKEN_APPROVAL_FAILED";
string private constant ERROR_UNISWAP_UNAVAILABLE = "UW_UNISWAP_UNAVAILABLE";

bytes32 internal constant ACTIVATE_DATA = keccak256("activate(uint256)");

ERC20 public bondedToken;
ERC900 public registry;
IUniswapFactory public uniswapFactory;

constructor(address _governor, ERC20 _bondedToken, ERC900 _registry, IUniswapFactory _uniswapFactory) Refundable(_governor) public {
require(isContract(address(_bondedToken)), ERROR_TOKEN_NOT_CONTRACT);
require(isContract(address(_registry)), ERROR_REGISTRY_NOT_CONTRACT);
require(isContract(address(_uniswapFactory)), ERROR_UNISWAP_FACTORY_NOT_CONTRACT);

bondedToken = _bondedToken;
registry = _registry;
uniswapFactory = _uniswapFactory;
}

/**
* @dev This function must be triggered by an external token's approve-and-call fallback.
* It will pull the approved tokens and convert them in Uniswap, and activate the converted
* tokens into a jurors registry instance of an Aragon Court.
* @param _from Address of the original caller (juror) converting and activating the tokens
* @param _amount Amount of external tokens to be converted and activated
* @param _token Address of the external token triggering the approve-and-call fallback
* @param _data Contains:
* - 1st word: activate If non-zero, it will signal token activation in the registry
* - 2nd word: minTokens Uniswap param
* - 3rd word: minEth Uniswap param
* - 4th word: deadline Uniswap param
*/
function receiveApproval(address _from, uint256 _amount, address _token, bytes calldata _data) external {
require(_token == msg.sender, ERROR_RECEIVED_WRONG_TOKEN);
// data must have 4 words
require(_data.length == 128, ERROR_WRONG_DATA_LENGTH);

bool activate;
uint256 minTokens;
uint256 minEth;
uint256 deadline;
bytes memory data = _data;
assembly {
activate := mload(add(data, 0x20))
minTokens := mload(add(data, 0x40))
minEth := mload(add(data, 0x60))
deadline := mload(add(data, 0x80))
}

_contributeExternalToken(_from, _amount, _token, minTokens, minEth, deadline, activate);
}

/**
* @dev This function needs a previous approval on the external token used for the contributed amount.
* It will pull the approved tokens, convert them in Uniswap to the bonded token,
* and activate the converted tokens in the jurors registry instance of the Aragon Court.
* @param _amount Amount of contribution tokens to be converted and activated
* @param _token Address of the external contribution token used
* @param _minTokens Minimum amount of bonded tokens obtained in Uniswap
* @param _minEth Minimum amount of ETH obtained in Uniswap (Uniswap internally converts first to ETH and then to target token)
* @param _deadline Transaction deadline for Uniswap
* @param _activate Signal activation of tokens in the registry
*/
function contributeExternalToken(
uint256 _amount,
address _token,
uint256 _minTokens,
uint256 _minEth,
uint256 _deadline,
bool _activate
)
external
{
_contributeExternalToken(msg.sender, _amount, _token, _minTokens, _minEth, _deadline, _activate);
}

/**
* @dev It will send the received ETH to Uniswap to get bonded tokens,
* and activate the converted tokens in the jurors registry instance of the Aragon Court.
* @param _minTokens Minimum amount of bonded tokens obtained in Uniswap
* @param _deadline Transaction deadline for Uniswap
* @param _activate Signal activation of tokens in the registry
*/
function contributeEth(uint256 _minTokens, uint256 _deadline, bool _activate) external payable {
require(msg.value > 0, ERROR_ZERO_AMOUNT);

// get the Uniswap exchange for the bonded token
address payable uniswapExchangeAddress = uniswapFactory.getExchange(address(bondedToken));
require(uniswapExchangeAddress != address(0), ERROR_UNISWAP_UNAVAILABLE);
IUniswapExchange uniswapExchange = IUniswapExchange(uniswapExchangeAddress);

// swap tokens
uint256 bondedTokenAmount = uniswapExchange.ethToTokenSwapInput.value(msg.value)(_minTokens, _deadline);

// stake and activate in the registry
_stakeAndActivate(msg.sender, bondedTokenAmount, _activate);
}

function _contributeExternalToken(
address _from,
uint256 _amount,
address _token,
uint256 _minTokens,
uint256 _minEth,
uint256 _deadline,
bool _activate
)
internal
{
require(_amount > 0, ERROR_ZERO_AMOUNT);

// move tokens to this contract
ERC20 token = ERC20(_token);
require(token.safeTransferFrom(_from, address(this), _amount), ERROR_TOKEN_TRANSFER_FAILED);

// get the Uniswap exchange for the external token
address payable uniswapExchangeAddress = uniswapFactory.getExchange(_token);
require(uniswapExchangeAddress != address(0), ERROR_UNISWAP_UNAVAILABLE);
IUniswapExchange uniswapExchange = IUniswapExchange(uniswapExchangeAddress);

require(token.safeApprove(address(uniswapExchange), _amount), ERROR_TOKEN_APPROVAL_FAILED);

// swap tokens
uint256 bondedTokenAmount = uniswapExchange.tokenToTokenSwapInput(_amount, _minTokens, _minEth, _deadline, address(bondedToken));

// stake and activate in the registry
_stakeAndActivate(_from, bondedTokenAmount, _activate);
}

function _stakeAndActivate(address _from, uint256 _amount, bool _activate) internal {
// activate in registry
bondedToken.approve(address(registry), _amount);
bytes memory data;
if (_activate) {
data = abi.encodePacked(ACTIVATE_DATA);
}
registry.stakeFor(_from, _amount, data);
}
}
2 changes: 2 additions & 0 deletions contracts/test/PresaleMock.sol
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ contract PresaleMock is IPresale {
ERC20Mock public bondedToken;
uint256 public exchangeRate;
uint256 public totalRaised;
bool public isClosed;

event Contribute (address indexed contributor, uint256 value, uint256 amount);

Expand All @@ -32,6 +33,7 @@ contract PresaleMock is IPresale {
}

function close() external {
isClosed = true;
}

function contribute(address _contributor, uint256 _value) external payable {
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@aragon/court-presale-activate",
"version": "1.2.0",
"version": "1.3.0",
"description": "Wrapper to buy tokens in the presale and activate them in the Court in one transaction",
"scripts": {
"compile": "truffle compile",
Expand Down
4 changes: 2 additions & 2 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,6 @@

> For deployment, see [`aragon-network-deploy`](https://github.com/aragon/aragon-network-deploy).
This repo provides a wrapper to allow jurors of Aragon Court converting ANT into ANJ and activating it in a single transaction.
This repo provides wrappers to allow jurors of Aragon Court converting ANT into ANJ and activating it in a single transaction.

The wrapper is also integrated with Uniswap, so you are able to convert any token that has an associated Uniswap market into ANJ the ANJ presale.
The wrappers are integrated with Uniswap, so you are able to convert any token that has an associated Uniswap market into ANJ the ANJ presale.
Loading

0 comments on commit 663ff3d

Please sign in to comment.