Skip to content

Commit f09f9de

Browse files
committed
A contract to exchange vouchers for tokens related to allocations (#462)
Introduce an AllocationExchange contract where indexers can redeem vouchers signed by an authority to collect tokens for an allocation. Main features: - A function to redeem vouchers signed by the authority. - The contract is governed and only governance can change the authority. - Authority should be an EOA capable of signing the vouchers. - The AllocationExchange must be funded by GRT to be able to perform transfers. Anyone can fund this contract. - Only the governor can withdraw funds from the contract without presenting a voucher. - A redeemable voucher is always related to an allocationID, it can only be used once for an allocation.
1 parent 3352bea commit f09f9de

File tree

2 files changed

+421
-0
lines changed

2 files changed

+421
-0
lines changed
Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
// SPDX-License-Identifier: MIT
2+
3+
pragma solidity ^0.7.3;
4+
pragma experimental ABIEncoderV2;
5+
6+
import "@openzeppelin/contracts/cryptography/ECDSA.sol";
7+
8+
import "../governance/Governed.sol";
9+
import "../staking/IStaking.sol";
10+
import "../token/IGraphToken.sol";
11+
12+
/**
13+
* @title Allocation Exchange
14+
* @dev This contract holds tokens that anyone with a voucher signed by the
15+
* authority can redeem. The contract validates if the voucher presented is valid
16+
* and then sends tokens to the Staking contract by calling the collect() function
17+
* passing the voucher allocationID. The contract enforces that only one voucher for
18+
* an allocationID can be redeemed.
19+
* Only governance can change the authority.
20+
*/
21+
contract AllocationExchange is Governed {
22+
// An allocation voucher represents a signed message that allows
23+
// redeeming an amount of funds from this contract and collect
24+
// them as part of an allocation
25+
struct AllocationVoucher {
26+
address allocationID;
27+
uint256 amount;
28+
bytes signature; // 65 bytes
29+
}
30+
31+
// -- Constants --
32+
33+
uint256 private constant MAX_UINT256 = 2**256 - 1;
34+
uint256 private constant SIGNATURE_LENGTH = 65;
35+
36+
// -- State --
37+
38+
IStaking private immutable staking;
39+
IGraphToken private immutable graphToken;
40+
address public authority;
41+
mapping(address => bool) public allocationsRedeemed;
42+
43+
// -- Events
44+
45+
event AuthoritySet(address indexed account);
46+
event AllocationRedeemed(address indexed allocationID, uint256 amount);
47+
event TokensWithdrawn(address indexed to, uint256 amount);
48+
49+
// -- Functions
50+
51+
constructor(
52+
IGraphToken _graphToken,
53+
IStaking _staking,
54+
address _governor,
55+
address _authority
56+
) {
57+
Governed._initialize(_governor);
58+
59+
graphToken = _graphToken;
60+
staking = _staking;
61+
_setAuthority(_authority);
62+
}
63+
64+
/**
65+
* @notice Approve the staking contract to pull any amount of tokens from this contract.
66+
* @dev Increased gas efficiency instead of approving on each voucher redeem
67+
*/
68+
function approveAll() external {
69+
graphToken.approve(address(staking), MAX_UINT256);
70+
}
71+
72+
/**
73+
* @notice Withdraw tokens held in the contract.
74+
* @dev Only the governor can withdraw
75+
* @param _to Destination to send the tokens
76+
* @param _amount Amount of tokens to withdraw
77+
*/
78+
function withdraw(address _to, uint256 _amount) external onlyGovernor {
79+
require(_to != address(0), "Exchange: empty destination");
80+
require(_amount != 0, "Exchange: empty amount");
81+
require(graphToken.transfer(_to, _amount), "Exchange: cannot transfer");
82+
emit TokensWithdrawn(_to, _amount);
83+
}
84+
85+
/**
86+
* @notice Set the authority allowed to sign vouchers.
87+
* @dev Only the governor can set the authority
88+
* @param _authority Address of the signing authority
89+
*/
90+
function setAuthority(address _authority) external onlyGovernor {
91+
_setAuthority(_authority);
92+
}
93+
94+
/**
95+
* @notice Set the authority allowed to sign vouchers.
96+
* @param _authority Address of the signing authority
97+
*/
98+
function _setAuthority(address _authority) private {
99+
require(_authority != address(0), "Exchange: empty authority");
100+
authority = _authority;
101+
emit AuthoritySet(authority);
102+
}
103+
104+
/**
105+
* @notice Redeem a voucher signed by the authority. No voucher double spending is allowed.
106+
* @dev The voucher must be signed using an Ethereum signed message
107+
* @param _voucher Voucher data
108+
*/
109+
function redeem(AllocationVoucher memory _voucher) external {
110+
_redeem(_voucher);
111+
}
112+
113+
/**
114+
* @notice Redeem multiple vouchers.
115+
* @dev Each voucher must be signed using an Ethereum signed message
116+
* @param _vouchers An array of vouchers
117+
*/
118+
function redeemMany(AllocationVoucher[] memory _vouchers) external {
119+
for (uint256 i = 0; i < _vouchers.length; i++) {
120+
_redeem(_vouchers[i]);
121+
}
122+
}
123+
124+
/**
125+
* @notice Redeem a voucher signed by the authority. No voucher double spending is allowed.
126+
* @dev The voucher must be signed using an Ethereum signed message
127+
* @param _voucher Voucher data
128+
*/
129+
function _redeem(AllocationVoucher memory _voucher) private {
130+
require(_voucher.amount > 0, "Exchange: zero tokens voucher");
131+
require(_voucher.signature.length == SIGNATURE_LENGTH, "Exchange: invalid signature");
132+
133+
// Already redeemed check
134+
require(
135+
!allocationsRedeemed[_voucher.allocationID],
136+
"Exchange: allocation already redeemed"
137+
);
138+
139+
// Signature check
140+
bytes32 messageHash = keccak256(abi.encodePacked(_voucher.allocationID, _voucher.amount));
141+
require(
142+
authority == ECDSA.recover(messageHash, _voucher.signature),
143+
"Exchange: invalid signer"
144+
);
145+
146+
// Mark allocation as collected
147+
allocationsRedeemed[_voucher.allocationID] = true;
148+
149+
// Make the staking contract collect funds from this contract
150+
// The Staking contract will validate if the allocation is valid
151+
staking.collect(_voucher.amount, _voucher.allocationID);
152+
153+
emit AllocationRedeemed(_voucher.allocationID, _voucher.amount);
154+
}
155+
}

0 commit comments

Comments
 (0)