-
Notifications
You must be signed in to change notification settings - Fork 0
/
DagonTokenModule.sol
253 lines (219 loc) · 10.4 KB
/
DagonTokenModule.sol
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
// SPDX-License-Identifier: AGPL-3.0-only
pragma solidity ^0.8.23;
import { Dagon, IAuth } from "../lib/dagon/src/Dagon.sol";
/**
* TODO
* - set limits on exchange multiple and check against uint96 casting to dagon token
* - think about how contributions directly minting voting share can influence group voting security
* eg. if a user contributes a large amount of a token, they could quickly execute whatever they want
* - provide a better way to value contributions based on updated treasury value
* eg. if I contribute X, then the exchange could be adapted based on the new treasury value
* - optimise packing on trackedTokens mapping (pack address and exchange rate into uint256)
*/
/**
* @title DagonTokenModule
* @notice A module for Safes to enable tracking of contributions
* @dev Currently supports native tokens and ERC20 tokens via transferFrom only
*/
contract DagonTokenModule {
/// -----------------------------------------------------------------------
/// Events & Errors
/// -----------------------------------------------------------------------
event TrackedTokenSet(address token, uint256 exchange);
error InstallationFailed();
error InvalidOwner();
error TokenNotTracked();
error ContributionFailed();
error TokenTransferFailed();
/// -----------------------------------------------------------------------
/// DagonTokenModule Storage
/// -----------------------------------------------------------------------
/// @dev The immutable address of the dagon singleton
address public immutable DAGON_SINGLETON;
/// @dev Mapping of tokens for which contributions are tracked
/// - eg. for a safe with WETH => 2, owners are minted 2 x Dagon tokens for every 1 WETH contributed
mapping(address => mapping(address => uint256)) public safesTrackedTokenExchangeRates;
/// -----------------------------------------------------------------------
/// Constructor
/// -----------------------------------------------------------------------
constructor(address _dagonAddress) {
DAGON_SINGLETON = _dagonAddress;
}
/// -----------------------------------------------------------------------
/// Setup Functions
/// -----------------------------------------------------------------------
/**
* @notice Installs the Dagon token and adds the safe to the list of safes
* @param owners List of owners for the safe
* @param setting Settings for the safe
* @param meta Metadata for the safe
*/
function install(
Dagon.Ownership[] memory owners,
Dagon.Settings memory setting,
Dagon.Metadata memory meta
)
public
payable
{
if (Dagon(DAGON_SINGLETON).totalSupply(uint256(uint160(msg.sender))) != 0) revert InstallationFailed();
bytes memory installCalldata = abi.encodeWithSelector(Dagon.install.selector, owners, setting, meta);
if (
!GnosisSafe(msg.sender).execTransactionFromModule(
DAGON_SINGLETON, 0, installCalldata, GnosisSafe.Operation.Call
)
) {
revert InstallationFailed();
}
// Default native token exchange rate to 1
safesTrackedTokenExchangeRates[msg.sender][address(0)] = 1;
}
/**
* @notice Sets a token so that owners are minted Dagon tokens when they contribute that token
* @param token Address of the token to be tracked
* @param exchange Exchange rate for the token
* @dev To disable a token, set the exchange rate to 0
*/
function setTrackedToken(address token, uint256 exchange) public payable {
emit TrackedTokenSet(token, safesTrackedTokenExchangeRates[msg.sender][token] = exchange);
}
/// -----------------------------------------------------------------------
/// Contribution Functions
/// -----------------------------------------------------------------------
/**
* @notice Tracks owners contribution with Dagon tokens
* @param safe The safe to which the contribution is made
* @param standard Token type (where DAGON=0 is taken as native token)
* @param contributionCalldata Calldata for the transfer function of the token
* @dev contributionCalldata is abi.encodePacked(tokenAddress, tokenTransferCalldata)
* where tokenTransferCalldata is the relevant transferFrom or safeTransferFrom calldata
*/
function contribute(address safe, Dagon.Standard standard, bytes calldata contributionCalldata) public payable {
// Mint the owner a token representing their contribution based on the type of token contributed
if (standard == Dagon.Standard.DAGON) _handleNativeContribution(safe);
if (standard == Dagon.Standard.ERC20) _handleERC20Contribution(contributionCalldata);
}
/**
* @notice Handles the contribution of native tokens
* @param safe The safe to which the contribution is made
*/
function _handleNativeContribution(address safe) internal {
if (!GnosisSafe(safe).isOwner(msg.sender)) revert InvalidOwner();
// Mint the owner a token representing their contribution based on the type of token contributed
bytes memory mintCalldata = abi.encodeWithSelector(
Dagon.mint.selector, msg.sender, uint96(msg.value * safesTrackedTokenExchangeRates[safe][address(0)])
);
if (!GnosisSafe(safe).execTransactionFromModule(DAGON_SINGLETON, 0, mintCalldata, GnosisSafe.Operation.Call)) {
revert ContributionFailed();
}
// Forward the contribution to the safe
safe.call{ value: msg.value }("");
}
/**
* @notice Handles the contribution of an ERC20 token via transferFrom
* @param contributionCalldata The combined token and transfer calldata: abi.encodePacked(tokenAddress,
* tokenTransferCalldata)
*/
function _handleERC20Contribution(bytes calldata contributionCalldata) internal {
address from;
address to;
uint256 amount;
uint256 exchangeRate;
assembly {
// Extract the token address & transfer calldata
let tokenAddress := shr(96, calldataload(contributionCalldata.offset))
// Set transferCalldata
let transferCalldata := mload(0x40)
// Copying 96 bytes from calldata, skipping the first 24 bytes (20 for address & 4 for function selector)
calldatacopy(transferCalldata, add(contributionCalldata.offset, 0x18), 0xc0)
// Extract details from the transfer calldata
from := mload(transferCalldata)
to := mload(add(transferCalldata, 0x20))
amount := mload(add(transferCalldata, 0x40))
// Get the exchange rate from storage mapping
let memPtr := mload(0x40)
// safesTrackedTokenExchangeRates[to] in slot keccak256(abi.encode(to, uint256(slot)))
mstore(memPtr, to)
mstore(add(memPtr, 0x20), safesTrackedTokenExchangeRates.slot)
let mappingHash := keccak256(memPtr, 0x40)
// safesTrackedTokenExchangeRates[to][tokenId] in slot keccak256(abi.encode(tokenAddress,mappingHash))
mstore(memPtr, tokenAddress)
mstore(add(memPtr, 0x20), mappingHash)
let exchangeRateHash := keccak256(memPtr, 0x40)
exchangeRate := sload(exchangeRateHash)
// If exchange rate is 0
if iszero(exchangeRate) {
// Revert with 'TokenNotTracked' error
mstore(0x00, 0x63cf4410)
revert(0x1c, 0x04)
}
// Prepare and call `isOwner`
memPtr := mload(0x40)
// Function selector: keccak256("isOwner(address)") = 0x2f54bf6e
mstore(memPtr, hex"2f54bf6e")
mstore(add(memPtr, 0x04), from)
// Call `isOwner` and write return data to scratch space, reverting if failed
if iszero(call(gas(), to, 0, memPtr, 0x40, 0x00, 0x20)) {
// Revert with error
mstore(0x00, 0x11111111)
revert(0x1c, 0x04)
}
// If token sender is not owner
if iszero(mload(0x00)) {
// Revert with 'InvalidOwner' error
mstore(0x00, 0x49e27cff)
revert(0x1c, 0x04)
}
// Prepare and call `transferFrom`
memPtr := mload(0x40)
// 0x64 = length of transferFrom calldata + function sig
mstore(transferCalldata, 0x64)
calldatacopy(transferCalldata, add(contributionCalldata.offset, 0x14), 0x64)
// Call `transferFrom` and write return data to scratch space, reverting if failed
if iszero(call(gas(), tokenAddress, 0, transferCalldata, 0x64, 0x00, 0x20)) {
// Revert with error
mstore(0x00, 0x11111111)
revert(0x1c, 0x04)
}
// If transfer failed
if iszero(mload(0x00)) {
// Revert with 'TokenTransferFailed' error
mstore(0x00, hex"045c4b02")
revert(0x1c, 0x04)
}
}
// todo convert to assembly
// Mint the owner a token representing their contribution based on the type of token contributed
bytes memory mintCalldata = abi.encodeWithSelector(Dagon.mint.selector, from, uint96(amount * exchangeRate));
if (
!GnosisSafe(to).execTransactionFromModule(
address(DAGON_SINGLETON), 0, mintCalldata, GnosisSafe.Operation.Call
)
) {
revert ContributionFailed();
}
}
}
/// @notice Minimal interface for the module to interact with the Safe contract
interface GnosisSafe {
/// @dev Type of call the Safe will make
enum Operation {
Call,
DelegateCall
}
/// @dev Allows a Module to execute a Safe transaction without any further confirmations.
/// @param to Destination address of module transaction.
/// @param value Ether value of module transaction.
/// @param data Data payload of module transaction.
/// @param operation Operation type of module transaction.
function execTransactionFromModule(
address to,
uint256 value,
bytes calldata data,
GnosisSafe.Operation operation
)
external
returns (bool success);
/// @dev Returns whether an address is an owner of the Safe.
function isOwner(address owner) external view returns (bool);
}